diff --git a/MODULE.bazel b/MODULE.bazel index 04c5c69ebd..be00937946 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,8 +1,8 @@ module(name = "codex") -bazel_dep(name = "bazel_skylib", version = "1.8.2") +bazel_dep(name = "bazel_skylib", version = "1.9.0") bazel_dep(name = "platforms", version = "1.0.0") -bazel_dep(name = "llvm", version = "0.6.8") +bazel_dep(name = "llvm", version = "0.7.1") # The upstream LLVM archive contains a few unix-only symlink entries and is # missing a couple of MinGW compatibility archives that windows-gnullvm needs # during extraction and linking, so patch it until upstream grows native support. @@ -78,8 +78,8 @@ use_repo(osx, "macos_sdk") bazel_dep(name = "apple_support", version = "2.1.0") bazel_dep(name = "rules_cc", version = "0.2.16") bazel_dep(name = "rules_platform", version = "0.1.0") -bazel_dep(name = "rules_rs", version = "0.0.43") -# `rules_rs` 0.0.43 does not model `windows-gnullvm` as a distinct Windows exec +bazel_dep(name = "rules_rs", version = "0.0.58") +# `rules_rs` still does not model `windows-gnullvm` as a distinct Windows exec # platform, so patch it until upstream grows that support for both x86_64 and # aarch64. single_version_override( @@ -87,10 +87,9 @@ single_version_override( patch_strip = 1, patches = [ "//patches:rules_rs_windows_gnullvm_exec.patch", - "//patches:rules_rs_delete_git_worktree_pointer.patch", "//patches:rules_rs_windows_exec_linker.patch", ], - version = "0.0.43", + version = "0.0.58", ) rules_rust = use_extension("@rules_rs//rs/experimental:rules_rust.bzl", "rules_rust") @@ -108,7 +107,6 @@ rules_rust.patch( "//patches:rules_rust_windows_exec_bin_target.patch", "//patches:rules_rust_windows_exec_std.patch", "//patches:rules_rust_windows_exec_rustc_dev_rlib.patch", - "//patches:rules_rust_repository_set_exec_constraints.patch", ], strip = 1, ) diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index 7a1acc3816..d58736aed5 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -41,12 +41,12 @@ "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", - "https://bcr.bazel.build/modules/bazel_features/1.32.0/MODULE.bazel": "095d67022a58cb20f7e20e1aefecfa65257a222c18a938e2914fd257b5f1ccdc", "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", "https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e", "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", "https://bcr.bazel.build/modules/bazel_features/1.42.0/MODULE.bazel": "e8ca15cb2639c5f12183db6dcb678735555d0cdd739b32a0418b6532b5e565f8", - "https://bcr.bazel.build/modules/bazel_features/1.42.0/source.json": "f2ea90e5dd0322481147114c7d5e4608c4b3fae2eeccae655e4d76a382389f6f", + "https://bcr.bazel.build/modules/bazel_features/1.45.0/MODULE.bazel": "7daec6d87ab0703417486d4cb948af0b06f55d4d7c08cbb5978c80e79b538edf", + "https://bcr.bazel.build/modules/bazel_features/1.45.0/source.json": "635e4536e09ff125b8972e0fa239c135fde5f18701f7d5115680560651dfb41d", "https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b", "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", "https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d", @@ -65,7 +65,8 @@ "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", - "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7", + "https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0", "https://bcr.bazel.build/modules/buildozer/8.2.1/MODULE.bazel": "61e9433c574c2bd9519cad7fa66b9c1d2b8e8d5f3ae5d6528a2c2d26e68d874d", "https://bcr.bazel.build/modules/buildozer/8.2.1/source.json": "7c33f6a26ee0216f85544b4bca5e9044579e0219b6898dd653f5fb449cf2e484", "https://bcr.bazel.build/modules/bzip2/1.0.8.bcr.3/MODULE.bazel": "29ecf4babfd3c762be00d7573c288c083672ab60e79c833ff7f49ee662e54471", @@ -85,15 +86,14 @@ "https://bcr.bazel.build/modules/libcap/2.27.bcr.1/MODULE.bazel": "7c034d7a4d92b2293294934377f5d1cbc88119710a11079fa8142120f6f08768", "https://bcr.bazel.build/modules/libcap/2.27.bcr.1/source.json": "3b116cbdbd25a68ffb587b672205f6d353a4c19a35452e480d58fc89531e0a10", "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", - "https://bcr.bazel.build/modules/llvm/0.6.7/MODULE.bazel": "d37a2e10571864dc6a5bb53c29216d90b9400bbcadb422337f49107fd2eaf0d2", - "https://bcr.bazel.build/modules/llvm/0.6.8/MODULE.bazel": "53468e4a4be409c2d34e5b7331d2e1fef982151b777655ca3c0047225b333629", - "https://bcr.bazel.build/modules/llvm/0.6.8/source.json": "b673af466f716e01d6243f59e47729e99f37dc5e17026d2bf18c98206f09b6c5", + "https://bcr.bazel.build/modules/llvm/0.7.0/MODULE.bazel": "3c07a4e5734b0ad41fe24dedaacbf3a35ce4377b7e1a21f24488a0c9ac4f1e6b", + "https://bcr.bazel.build/modules/llvm/0.7.1/MODULE.bazel": "74ac75efc6385b8a95d83bfa36ad399500f747c3d0f50287f9b6f9e854ec4814", + "https://bcr.bazel.build/modules/llvm/0.7.1/source.json": "0cac59d04dafa0ca1f70ea21cf1569e034ef8736c2c7510e834a9e8141d7e631", "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", - "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", "https://bcr.bazel.build/modules/openssl/3.5.4.bcr.0/MODULE.bazel": "0f6b8f20b192b9ff0781406256150bcd46f19e66d807dcb0c540548439d6fc35", "https://bcr.bazel.build/modules/openssl/3.5.4.bcr.0/source.json": "543ed7627cc18e6460b9c1ae4a1b6b1debc5a5e0aca878b00f7531c7186b73da", - "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", - "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/MODULE.bazel": "7adb03933fc8401f495800cf4eafcff0edc6da0ff55c7db223ef69d19f689486", + "https://bcr.bazel.build/modules/package_metadata/0.0.7/source.json": "50639625e937b56115012674c797cca7a05a96b4878c87d803c13dc2b31de8a0", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", @@ -113,7 +113,8 @@ "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", - "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", + "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/MODULE.bazel": "74e541b0ba877813da786a11707d4e394433c157841d5111a36be0d44b907931", + "https://bcr.bazel.build/modules/protobuf/34.0.bcr.1/source.json": "fc174b3d6215aa14197d1bd779f98bb72d9fd666ee5ec0d6bba6ae986baa4535", "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", @@ -125,7 +126,6 @@ "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", - "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", @@ -201,8 +201,8 @@ "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", - "https://bcr.bazel.build/modules/rules_rs/0.0.43/MODULE.bazel": "7adfc2a97d90218ebeb9882de9eb18d9c6b0b41d2884be6ab92c9daadb17c78d", - "https://bcr.bazel.build/modules/rules_rs/0.0.43/source.json": "c315361abf625411f506ab935e660f49f14dc64fa30c125ca0a177c34cd63a2a", + "https://bcr.bazel.build/modules/rules_rs/0.0.58/MODULE.bazel": "72269bad768fbf5e00ea20d1fc5ad2193667d290c2c0ebefe80e787ef416d2a7", + "https://bcr.bazel.build/modules/rules_rs/0.0.58/source.json": "fdc6d257b388c4a0671563e259bea98b3401e86f2df884d16883922d91ffc668", "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", "https://bcr.bazel.build/modules/rules_shell/0.4.1/MODULE.bazel": "00e501db01bbf4e3e1dd1595959092c2fadf2087b2852d3f553b5370f5633592", @@ -212,7 +212,6 @@ "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", - "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", "https://bcr.bazel.build/modules/sed/4.9.bcr.3/MODULE.bazel": "3aca45895b85b6ef65366cc12a45217ba6870f8931d2d62e09c99c772d9736ab", "https://bcr.bazel.build/modules/sed/4.9.bcr.3/source.json": "31c0cf4c135ed3fa58298cd7bcfd4301c54ea4cf59d7c4e2ea0a180ce68eb34f", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", @@ -222,7 +221,6 @@ "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", - "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", "https://bcr.bazel.build/modules/tar.bzl/0.9.0/MODULE.bazel": "452a22d7f02b1c9d7a22ab25edf20f46f3e1101f0f67dc4bfbf9a474ddf02445", "https://bcr.bazel.build/modules/tar.bzl/0.9.0/source.json": "c732760a374831a2cf5b08839e4be75017196b4d796a5aa55235272ee17cd839", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", @@ -243,7 +241,7 @@ "@@aspect_tools_telemetry+//:extension.bzl%telemetry": { "general": { "bzlTransitiveDigest": "dnnhvKMf9MIXMulhbhHBblZdDAfAkiSVjApIXpUz9Y8=", - "usagesDigest": "aAcu2vTLy2HUXbcYIow0P6OHLLog/f5FFk8maEC/fpQ=", + "usagesDigest": "7RqoFfyAk9gopozpmkZeHxhtuqELOLYzmqQ4uuF30BU=", "recordedInputs": [ "REPO_MAPPING:aspect_tools_telemetry+,bazel_lib bazel_lib+", "REPO_MAPPING:aspect_tools_telemetry+,bazel_skylib bazel_skylib+" @@ -266,7 +264,7 @@ "googletest": "1.17.0", "jsoncpp": "1.9.6", "libcap": "2.27.bcr.1", - "llvm": "0.6.7", + "llvm": "0.6.8", "nlohmann_json": "3.6.1", "openssl": "3.5.4.bcr.0", "package_metadata": "0.0.5", @@ -286,15 +284,15 @@ "rules_platform": "0.1.0", "rules_proto": "7.1.0", "rules_python": "1.7.0", - "rules_rs": "0.0.40", + "rules_rs": "0.0.43", "rules_shell": "0.6.1", "rules_swift": "3.1.2", "sed": "4.9.bcr.3", "stardoc": "0.7.2", "swift_argument_parser": "1.3.1.2", "tar.bzl": "0.9.0", - "toolchains_llvm_bootstrapped": "0.5.2", "with_cfg.bzl": "0.12.0", + "xz": "5.4.5.bcr.8", "zlib": "1.3.1.bcr.8", "zstd": "1.5.7" } @@ -303,6 +301,21 @@ } } }, + "@@protobuf+//python/dist:system_python.bzl%system_python_extension": { + "general": { + "bzlTransitiveDigest": "pmsA+awieucfllLc2n7k8xEoPp0i5LF9Hw6mGX0cqSQ=", + "usagesDigest": "A+RWmbKdBBwZcBbNGNvfPbqG2vYZRjVrFp6x1iRUrAk=", + "recordedInputs": [], + "generatedRepoSpecs": { + "system_python": { + "repoRuleId": "@@protobuf+//python/dist:system_python.bzl%system_python", + "attributes": { + "minimum_python_version": "3.9" + } + } + } + } + }, "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": { "general": { "bzlTransitiveDigest": "06cynZ1bCvvy8zHPrrDlXq+Z68xmjctHpfFxi+zEpJY=", @@ -609,7 +622,9 @@ "annotate-snippets_0.9.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"difference\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"toml\",\"req\":\"^0.5\"},{\"name\":\"unicode-width\",\"req\":\"^0.1\"},{\"name\":\"yansi-term\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"yansi-term\",\"req\":\"^0.1\"}],\"features\":{\"color\":[\"yansi-term\"],\"default\":[]}}", "ansi-to-tui_7.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"nom\",\"req\":\"^7.1\"},{\"kind\":\"dev\",\"name\":\"pretty_assertions\",\"req\":\"^1.4.0\"},{\"name\":\"simdutf8\",\"optional\":true,\"req\":\"^0.1\"},{\"features\":[\"const_generics\"],\"name\":\"smallvec\",\"req\":\"^1.10.0\"},{\"name\":\"thiserror\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tui\",\"package\":\"ratatui\",\"req\":\"^0.29\"}],\"features\":{\"default\":[\"zero-copy\",\"simd\"],\"simd\":[\"dep:simdutf8\"],\"zero-copy\":[]}}", "anstream_0.6.21": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"name\":\"anstyle-parse\",\"req\":\"^0.2.0\"},{\"name\":\"anstyle-query\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"anstyle-wincon\",\"optional\":true,\"req\":\"^3.0.5\",\"target\":\"cfg(windows)\"},{\"name\":\"colorchoice\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.16\"},{\"name\":\"is_terminal_polyfill\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"owo-colors\",\"req\":\"^4.0.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"strip-ansi-escapes\",\"req\":\"^0.2.1\"},{\"name\":\"utf8parse\",\"req\":\"^0.2.2\"}],\"features\":{\"auto\":[\"dep:anstyle-query\"],\"default\":[\"auto\",\"wincon\"],\"test\":[],\"wincon\":[\"dep:anstyle-wincon\"]}}", + "anstream_1.0.0": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"name\":\"anstyle-parse\",\"req\":\"^1.0.0\"},{\"name\":\"anstyle-query\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"anstyle-wincon\",\"optional\":true,\"req\":\"^3.0.5\",\"target\":\"cfg(windows)\"},{\"name\":\"colorchoice\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.16\"},{\"name\":\"is_terminal_polyfill\",\"req\":\"^1.48\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"owo-colors\",\"req\":\"^4.0.0\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"strip-ansi-escapes\",\"req\":\"^0.2.1\"},{\"name\":\"utf8parse\",\"req\":\"^0.2.2\"}],\"features\":{\"auto\":[\"dep:anstyle-query\"],\"default\":[\"auto\",\"wincon\"],\"test\":[],\"wincon\":[\"dep:anstyle-wincon\"]}}", "anstyle-parse_0.2.7": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.7.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"codegenrs\",\"req\":\"^3.0.1\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.14\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.5\"},{\"name\":\"utf8parse\",\"optional\":true,\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"vte_generate_state_changes\",\"req\":\"^0.1.1\"}],\"features\":{\"core\":[\"dep:arrayvec\"],\"default\":[\"utf8\"],\"utf8\":[\"dep:utf8parse\"]}}", + "anstyle-parse_1.0.0": "{\"dependencies\":[{\"default_features\":false,\"name\":\"arrayvec\",\"optional\":true,\"req\":\"^0.7.6\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"codegenrs\",\"req\":\"^3.0.0\"},{\"kind\":\"dev\",\"name\":\"divan\",\"req\":\"^0.1.16\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.23\"},{\"name\":\"utf8parse\",\"optional\":true,\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"vte_generate_state_changes\",\"req\":\"^0.1.2\"}],\"features\":{\"core\":[\"dep:arrayvec\"],\"default\":[\"utf8\"],\"utf8\":[\"dep:utf8parse\"]}}", "anstyle-query_1.1.5": "{\"dependencies\":[{\"features\":[\"Win32_System_Console\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.60.2, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "anstyle-wincon_3.0.11": "{\"dependencies\":[{\"name\":\"anstyle\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.1\"},{\"name\":\"once_cell_polyfill\",\"req\":\"^1.56.1\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_System_Console\",\"Win32_Foundation\"],\"name\":\"windows-sys\",\"req\":\">=0.60.2, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "anstyle_1.0.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lexopt\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.5\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", @@ -647,7 +662,9 @@ "autocfg_1.5.0": "{\"dependencies\":[],\"features\":{}}", "aws-lc-rs_1.16.2": "{\"dependencies\":[{\"name\":\"aws-lc-fips-sys\",\"optional\":true,\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"aws-lc-sys\",\"optional\":true,\"req\":\"^0.39.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.4\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.11.1\"},{\"name\":\"untrusted\",\"optional\":true,\"req\":\"^0.7.1\"},{\"name\":\"zeroize\",\"req\":\"^1.8.1\"}],\"features\":{\"alloc\":[],\"asan\":[\"aws-lc-sys?/asan\",\"aws-lc-fips-sys?/asan\"],\"bindgen\":[\"aws-lc-sys?/bindgen\",\"aws-lc-fips-sys?/bindgen\"],\"default\":[\"aws-lc-sys\",\"alloc\",\"ring-io\",\"ring-sig-verify\"],\"dev-tests-only\":[],\"fips\":[\"dep:aws-lc-fips-sys\"],\"non-fips\":[\"aws-lc-sys\"],\"prebuilt-nasm\":[\"aws-lc-sys?/prebuilt-nasm\"],\"ring-io\":[\"dep:untrusted\"],\"ring-sig-verify\":[\"dep:untrusted\"],\"test_logging\":[],\"unstable\":[]}}", "aws-lc-sys_0.39.0": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"bindgen\",\"optional\":true,\"req\":\"^0.72.0\"},{\"features\":[\"parallel\"],\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.2.26\"},{\"kind\":\"build\",\"name\":\"cmake\",\"req\":\"^0.1.54\"},{\"kind\":\"build\",\"name\":\"dunce\",\"req\":\"^1.0.5\"},{\"kind\":\"build\",\"name\":\"fs_extra\",\"req\":\"^1.3.0\"}],\"features\":{\"all-bindings\":[],\"asan\":[],\"bindgen\":[\"dep:bindgen\"],\"default\":[\"all-bindings\"],\"disable-prebuilt-nasm\":[],\"fips\":[\"dep:bindgen\"],\"prebuilt-nasm\":[],\"ssl\":[\"bindgen\",\"all-bindings\"]}}", + "axum-core_0.4.5": "{\"dependencies\":[{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"kind\":\"dev\",\"name\":\"axum\",\"req\":\"^0.7.2\"},{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", "axum-core_0.5.6": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1.2\"},{\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.0\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"limit\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"limit\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"}],\"features\":{\"__private_docs\":[\"dep:tower-http\"],\"tracing\":[\"dep:tracing\"]}}", + "axum_0.7.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"axum-core\",\"req\":\"^0.4.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.4.2\"},{\"features\":[\"__private\"],\"kind\":\"dev\",\"name\":\"axum-macros\",\"req\":\"^0.4.1\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"^0.7\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.25.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.24.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.24.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.1\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:serde_urlencoded\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:serde_urlencoded\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "axum_0.8.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"axum-core\",\"req\":\"^0.5.5\"},{\"name\":\"axum-macros\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22.1\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"form_urlencoded\",\"optional\":true,\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"req\":\"^0.3\"},{\"name\":\"http\",\"req\":\"^1.0.0\"},{\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"hyper\",\"optional\":true,\"req\":\"^1.1.0\"},{\"features\":[\"client\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.1.0\"},{\"features\":[\"tokio\",\"server\",\"service\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.3\"},{\"name\":\"itoa\",\"req\":\"^1.0.5\"},{\"name\":\"matchit\",\"req\":\"=0.8.4\"},{\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"mime\",\"req\":\"^0.3.16\"},{\"name\":\"multer\",\"optional\":true,\"req\":\"^3.0.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"name\":\"reqwest\",\"optional\":true,\"req\":\"^0.12\"},{\"default_features\":false,\"features\":[\"json\",\"stream\",\"multipart\"],\"kind\":\"dev\",\"name\":\"reqwest\",\"req\":\"^0.12\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.211\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.221\"},{\"name\":\"serde_core\",\"req\":\"^1.0.221\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"raw_value\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"name\":\"serde_path_to_error\",\"optional\":true,\"req\":\"^0.1.8\"},{\"name\":\"serde_urlencoded\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"sha1\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.0\"},{\"features\":[\"serde-human-readable\"],\"kind\":\"dev\",\"name\":\"time\",\"req\":\"^0.3\"},{\"features\":[\"time\"],\"name\":\"tokio\",\"optional\":true,\"package\":\"tokio\",\"req\":\"^1.44\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"net\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"package\":\"tokio\",\"req\":\"^1.44.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"name\":\"tokio-tungstenite\",\"optional\":true,\"req\":\"^0.28.0\"},{\"kind\":\"dev\",\"name\":\"tokio-tungstenite\",\"req\":\"^0.28.0\"},{\"default_features\":false,\"features\":[\"util\"],\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"util\",\"timeout\",\"limit\",\"load-shed\",\"steer\",\"filter\"],\"kind\":\"dev\",\"name\":\"tower\",\"package\":\"tower\",\"req\":\"^0.5.2\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"name\":\"tower-http\",\"optional\":true,\"req\":\"^0.6.0\"},{\"features\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"cors\",\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"kind\":\"dev\",\"name\":\"tower-http\",\"req\":\"^0.6.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.2\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"json\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"serde\",\"v4\"],\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.0\"}],\"features\":{\"__private\":[\"tokio\",\"http1\",\"dep:reqwest\"],\"__private_docs\":[\"axum-core/__private_docs\",\"tower/full\",\"dep:serde\",\"dep:tower-http\"],\"default\":[\"form\",\"http1\",\"json\",\"matched-path\",\"original-uri\",\"query\",\"tokio\",\"tower-log\",\"tracing\"],\"form\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"http1\":[\"dep:hyper\",\"hyper?/http1\",\"hyper-util?/http1\"],\"http2\":[\"dep:hyper\",\"hyper?/http2\",\"hyper-util?/http2\"],\"json\":[\"dep:serde_json\",\"dep:serde_path_to_error\"],\"macros\":[\"dep:axum-macros\"],\"matched-path\":[],\"multipart\":[\"dep:multer\"],\"original-uri\":[],\"query\":[\"dep:form_urlencoded\",\"dep:serde_urlencoded\",\"dep:serde_path_to_error\"],\"tokio\":[\"dep:hyper-util\",\"dep:tokio\",\"tokio/net\",\"tokio/rt\",\"tower/make\",\"tokio/macros\"],\"tower-log\":[\"tower/log\"],\"tracing\":[\"dep:tracing\",\"axum-core/tracing\"],\"ws\":[\"dep:hyper\",\"tokio\",\"dep:tokio-tungstenite\",\"dep:sha1\",\"dep:base64\"]}}", "backtrace_0.3.76": "{\"dependencies\":[{\"default_features\":false,\"name\":\"addr2line\",\"req\":\"^0.25.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"cpp_demangle\",\"optional\":true,\"req\":\"^0.5.0\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.156\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"kind\":\"dev\",\"name\":\"libloading\",\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"miniz_oxide\",\"req\":\"^0.8\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"default_features\":false,\"features\":[\"read_core\",\"elf\",\"macho\",\"pe\",\"xcoff\",\"unaligned\",\"archive\"],\"name\":\"object\",\"req\":\"^0.37.0\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"name\":\"rustc-demangle\",\"req\":\"^0.1.24\"},{\"default_features\":false,\"name\":\"ruzstd\",\"optional\":true,\"req\":\"^0.8.1\",\"target\":\"cfg(not(all(windows, target_env = \\\"msvc\\\", not(target_vendor = \\\"uwp\\\"))))\"},{\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\"^0.2\",\"target\":\"cfg(any(windows, target_os = \\\"cygwin\\\"))\"}],\"features\":{\"coresymbolication\":[],\"dbghelp\":[],\"default\":[\"std\"],\"dl_iterate_phdr\":[],\"dladdr\":[],\"kernel32\":[],\"libunwind\":[],\"ruzstd\":[\"dep:ruzstd\"],\"serialize-serde\":[\"serde\"],\"std\":[],\"unix-backtrace\":[]}}", "base64_0.21.7": "{\"dependencies\":[{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.2.25\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.13.0\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.6.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"strum\",\"req\":\"^0.25\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", @@ -706,10 +723,14 @@ "cipher_0.4.4": "{\"dependencies\":[{\"name\":\"blobby\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"crypto-common\",\"req\":\"^0.1.6\"},{\"name\":\"inout\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.5\"}],\"features\":{\"alloc\":[],\"block-padding\":[\"inout/block-padding\"],\"dev\":[\"blobby\"],\"rand_core\":[\"crypto-common/rand_core\"],\"std\":[\"alloc\",\"crypto-common/std\",\"inout/std\"]}}", "clang-sys_1.8.1": "{\"dependencies\":[{\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"build\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.39\"},{\"name\":\"libloading\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\">=3.0.0, <3.7.0\"}],\"features\":{\"clang_10_0\":[\"clang_9_0\"],\"clang_11_0\":[\"clang_10_0\"],\"clang_12_0\":[\"clang_11_0\"],\"clang_13_0\":[\"clang_12_0\"],\"clang_14_0\":[\"clang_13_0\"],\"clang_15_0\":[\"clang_14_0\"],\"clang_16_0\":[\"clang_15_0\"],\"clang_17_0\":[\"clang_16_0\"],\"clang_18_0\":[\"clang_17_0\"],\"clang_3_5\":[],\"clang_3_6\":[\"clang_3_5\"],\"clang_3_7\":[\"clang_3_6\"],\"clang_3_8\":[\"clang_3_7\"],\"clang_3_9\":[\"clang_3_8\"],\"clang_4_0\":[\"clang_3_9\"],\"clang_5_0\":[\"clang_4_0\"],\"clang_6_0\":[\"clang_5_0\"],\"clang_7_0\":[\"clang_6_0\"],\"clang_8_0\":[\"clang_7_0\"],\"clang_9_0\":[\"clang_8_0\"],\"libcpp\":[],\"runtime\":[\"libloading\"],\"static\":[]}}", "clap_4.5.58": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"clap-cargo\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"name\":\"clap_builder\",\"req\":\"=4.5.58\"},{\"name\":\"clap_derive\",\"optional\":true,\"req\":\"=4.5.55\"},{\"kind\":\"dev\",\"name\":\"jiff\",\"req\":\"^0.2.3\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.15\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.26\"},{\"kind\":\"dev\",\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"features\":[\"term-svg\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.91\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.3\"}],\"features\":{\"cargo\":[\"clap_builder/cargo\"],\"color\":[\"clap_builder/color\"],\"debug\":[\"clap_builder/debug\",\"clap_derive?/debug\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[\"clap_builder/deprecated\",\"clap_derive?/deprecated\"],\"derive\":[\"dep:clap_derive\"],\"env\":[\"clap_builder/env\"],\"error-context\":[\"clap_builder/error-context\"],\"help\":[\"clap_builder/help\"],\"std\":[\"clap_builder/std\"],\"string\":[\"clap_builder/string\"],\"suggestions\":[\"clap_builder/suggestions\"],\"unicode\":[\"clap_builder/unicode\"],\"unstable-derive-ui-tests\":[],\"unstable-doc\":[\"clap_builder/unstable-doc\",\"derive\"],\"unstable-ext\":[\"clap_builder/unstable-ext\"],\"unstable-markdown\":[\"clap_derive/unstable-markdown\"],\"unstable-styles\":[\"clap_builder/unstable-styles\"],\"unstable-v5\":[\"clap_builder/unstable-v5\",\"clap_derive?/unstable-v5\",\"deprecated\"],\"usage\":[\"clap_builder/usage\"],\"wrap_help\":[\"clap_builder/wrap_help\"]}}", + "clap_4.6.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.16\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"clap-cargo\",\"req\":\"^0.15.2\"},{\"default_features\":false,\"name\":\"clap_builder\",\"req\":\"=4.6.0\"},{\"name\":\"clap_derive\",\"optional\":true,\"req\":\"=4.6.0\"},{\"kind\":\"dev\",\"name\":\"jiff\",\"req\":\"^0.2.23\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.22\"},{\"kind\":\"dev\",\"name\":\"semver\",\"req\":\"^1.0.27\"},{\"kind\":\"dev\",\"name\":\"shlex\",\"req\":\"^1.3.0\"},{\"features\":[\"term-svg\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.116\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^1.1.1\"}],\"features\":{\"cargo\":[\"clap_builder/cargo\"],\"color\":[\"clap_builder/color\"],\"debug\":[\"clap_builder/debug\",\"clap_derive?/debug\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[\"clap_builder/deprecated\",\"clap_derive?/deprecated\"],\"derive\":[\"dep:clap_derive\"],\"env\":[\"clap_builder/env\"],\"error-context\":[\"clap_builder/error-context\"],\"help\":[\"clap_builder/help\"],\"std\":[\"clap_builder/std\"],\"string\":[\"clap_builder/string\"],\"suggestions\":[\"clap_builder/suggestions\"],\"unicode\":[\"clap_builder/unicode\"],\"unstable-derive-ui-tests\":[],\"unstable-doc\":[\"clap_builder/unstable-doc\",\"derive\"],\"unstable-ext\":[\"clap_builder/unstable-ext\"],\"unstable-markdown\":[\"clap_derive/unstable-markdown\"],\"unstable-styles\":[\"clap_builder/unstable-styles\"],\"unstable-v5\":[\"clap_builder/unstable-v5\",\"clap_derive?/unstable-v5\",\"deprecated\"],\"usage\":[\"clap_builder/usage\"],\"wrap_help\":[\"clap_builder/wrap_help\"]}}", "clap_builder_4.5.58": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.7\"},{\"name\":\"anstyle\",\"req\":\"^1.0.8\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.73\"},{\"name\":\"clap_lex\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"color-print\",\"req\":\"^0.3.6\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.16\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"optional\":true,\"req\":\"^0.11.0\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"unic-emoji-char\",\"req\":\"^0.9.0\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.6.0\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.0\"}],\"features\":{\"cargo\":[],\"color\":[\"dep:anstream\"],\"debug\":[\"dep:backtrace\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[],\"env\":[],\"error-context\":[],\"help\":[],\"std\":[\"anstyle/std\"],\"string\":[],\"suggestions\":[\"dep:strsim\",\"error-context\"],\"unicode\":[\"dep:unicode-width\",\"dep:unicase\"],\"unstable-doc\":[\"cargo\",\"wrap_help\",\"env\",\"unicode\",\"string\",\"unstable-ext\"],\"unstable-ext\":[],\"unstable-styles\":[\"color\"],\"unstable-v5\":[\"deprecated\"],\"usage\":[],\"wrap_help\":[\"help\",\"dep:terminal_size\"]}}", + "clap_builder_4.6.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"anstyle\",\"req\":\"^1.0.13\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.76\"},{\"name\":\"clap_lex\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"color-print\",\"req\":\"^0.3.7\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^1.1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"name\":\"strsim\",\"optional\":true,\"req\":\"^0.11.1\"},{\"name\":\"terminal_size\",\"optional\":true,\"req\":\"^0.4.3\"},{\"kind\":\"dev\",\"name\":\"unic-emoji-char\",\"req\":\"^0.9.0\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.9.0\"},{\"name\":\"unicode-width\",\"optional\":true,\"req\":\"^0.2.2\"}],\"features\":{\"cargo\":[],\"color\":[\"dep:anstream\"],\"debug\":[\"dep:backtrace\"],\"default\":[\"std\",\"color\",\"help\",\"usage\",\"error-context\",\"suggestions\"],\"deprecated\":[],\"env\":[],\"error-context\":[],\"help\":[],\"std\":[\"anstyle/std\"],\"string\":[],\"suggestions\":[\"dep:strsim\",\"error-context\"],\"unicode\":[\"dep:unicode-width\",\"dep:unicase\"],\"unstable-doc\":[\"cargo\",\"wrap_help\",\"env\",\"unicode\",\"string\",\"unstable-ext\"],\"unstable-ext\":[],\"unstable-styles\":[\"color\"],\"unstable-v5\":[\"deprecated\"],\"usage\":[],\"wrap_help\":[\"help\",\"dep:terminal_size\"]}}", "clap_complete_4.5.65": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"default_features\":false,\"features\":[\"std\",\"derive\",\"help\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.5.20\"},{\"name\":\"clap_lex\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"completest\",\"optional\":true,\"req\":\"^0.4.2\"},{\"name\":\"completest-pty\",\"optional\":true,\"req\":\"^0.5.5\"},{\"name\":\"is_executable\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"shlex\",\"optional\":true,\"req\":\"^1.3.0\"},{\"features\":[\"diff\",\"dir\",\"examples\"],\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"features\":[\"color-auto\",\"diff\",\"examples\"],\"kind\":\"dev\",\"name\":\"trycmd\",\"req\":\"^0.15.1\"}],\"features\":{\"debug\":[\"clap/debug\"],\"default\":[],\"unstable-doc\":[\"unstable-dynamic\"],\"unstable-dynamic\":[\"dep:clap_lex\",\"dep:shlex\",\"dep:is_executable\",\"clap/unstable-ext\"],\"unstable-shell-tests\":[\"dep:completest\",\"dep:completest-pty\"]}}", "clap_derive_4.5.55": "{\"dependencies\":[{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.10\"},{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.69\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.0\"},{\"name\":\"quote\",\"req\":\"^1.0.9\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.8\"}],\"features\":{\"debug\":[],\"default\":[],\"deprecated\":[],\"raw-deprecated\":[\"deprecated\"],\"unstable-markdown\":[\"dep:pulldown-cmark\",\"dep:anstyle\"],\"unstable-v5\":[\"deprecated\"]}}", + "clap_derive_4.6.0": "{\"dependencies\":[{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.13\"},{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.106\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13.1\"},{\"name\":\"quote\",\"req\":\"^1.0.45\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.117\"}],\"features\":{\"debug\":[],\"default\":[],\"deprecated\":[],\"raw-deprecated\":[\"deprecated\"],\"unstable-markdown\":[\"dep:pulldown-cmark\",\"dep:anstyle\"],\"unstable-v5\":[\"deprecated\"]}}", "clap_lex_1.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.14\"}],\"features\":{}}", + "clap_lex_1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"automod\",\"req\":\"^1.0.16\"}],\"features\":{}}", "clipboard-win_5.4.1": "{\"dependencies\":[{\"name\":\"error-code\",\"req\":\"^3\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-win\",\"optional\":true,\"req\":\"^3\",\"target\":\"cfg(windows)\"}],\"features\":{\"monitor\":[\"windows-win\"],\"std\":[\"error-code/std\"]}}", "cmake_0.1.57": "{\"dependencies\":[{\"name\":\"cc\",\"req\":\"^1.2.46\"}],\"features\":{}}", "cmp_any_0.8.1": "{\"dependencies\":[],\"features\":{}}", @@ -836,7 +857,9 @@ "enumflags2_derive_0.7.12": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"derive\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{}}", "env-flags_0.1.1": "{\"dependencies\":[],\"features\":{}}", "env_filter_1.0.0": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.8\"},{\"default_features\":false,\"features\":[\"std\",\"perf\"],\"name\":\"regex\",\"optional\":true,\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6\"}],\"features\":{\"default\":[\"regex\"],\"regex\":[\"dep:regex\"]}}", + "env_filter_1.0.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.29\"},{\"default_features\":false,\"features\":[\"std\",\"perf\"],\"name\":\"regex\",\"optional\":true,\"req\":\"^1.12.3\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"regex\"],\"regex\":[\"dep:regex\"]}}", "env_home_0.1.0": "{\"dependencies\":[],\"features\":{}}", + "env_logger_0.11.10": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"wincon\"],\"name\":\"anstream\",\"optional\":true,\"req\":\"^1.0.0\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"env_filter\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"jiff\",\"optional\":true,\"req\":\"^0.2.22\"},{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.29\"}],\"features\":{\"auto-color\":[\"color\",\"anstream/auto\"],\"color\":[\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"auto-color\",\"humantime\",\"regex\"],\"humantime\":[\"dep:jiff\"],\"kv\":[\"log/kv\"],\"regex\":[\"env_filter/regex\"],\"unstable-kv\":[\"kv\"]}}", "env_logger_0.11.9": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"wincon\"],\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.11\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.6\"},{\"default_features\":false,\"name\":\"env_filter\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"jiff\",\"optional\":true,\"req\":\"^0.2.3\"},{\"features\":[\"std\"],\"name\":\"log\",\"req\":\"^0.4.21\"}],\"features\":{\"auto-color\":[\"color\",\"anstream/auto\"],\"color\":[\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"auto-color\",\"humantime\",\"regex\"],\"humantime\":[\"dep:jiff\"],\"kv\":[\"log/kv\"],\"regex\":[\"env_filter/regex\"],\"unstable-kv\":[\"kv\"]}}", "equivalent_1.0.2": "{\"dependencies\":[],\"features\":{}}", "erased-serde_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.13\"},{\"default_features\":false,\"name\":\"serde\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_cbor\",\"req\":\"^0.11.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.166\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.99\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.83\"}],\"features\":{\"alloc\":[\"serde/alloc\"],\"default\":[\"std\"],\"std\":[\"serde/std\"],\"unstable-debug\":[]}}", @@ -880,15 +903,20 @@ "fsevent-sys_4.1.0": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.68\"}],\"features\":{}}", "fslock_0.2.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2.66\",\"target\":\"cfg(unix)\"},{\"features\":[\"minwindef\",\"minwinbase\",\"winbase\",\"errhandlingapi\",\"winerror\",\"winnt\",\"synchapi\",\"handleapi\",\"fileapi\",\"processthreadsapi\"],\"name\":\"winapi\",\"req\":\"^0.3.8\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "futures-channel_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", + "futures-channel_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\"],\"unstable\":[]}}", "futures-core_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", + "futures-core_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-executor_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"name\":\"num_cpus\",\"optional\":true,\"req\":\"^1.8.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"futures-core/std\",\"futures-task/std\",\"futures-util/std\"],\"thread-pool\":[\"std\",\"num_cpus\"]}}", "futures-intrusive_0.5.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-std\",\"req\":\"^1.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"crossbeam\",\"req\":\"^0.7\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"lock_api\",\"req\":\"^0.4.1\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"kind\":\"dev\",\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.1.11\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.14\"}],\"features\":{\"alloc\":[\"futures-core/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"parking_lot\"]}}", "futures-io_0.3.31": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[],\"unstable\":[]}}", "futures-lite_2.6.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"fastrand\",\"optional\":true,\"req\":\"^2.0.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.5\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.5\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.3.3\"},{\"name\":\"parking\",\"optional\":true,\"req\":\"^2.2.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"spin_on\",\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"waker-fn\",\"req\":\"^1.0.0\"}],\"features\":{\"alloc\":[],\"default\":[\"race\",\"std\"],\"race\":[\"fastrand\"],\"std\":[\"alloc\",\"fastrand/std\",\"futures-io\",\"parking\"]}}", "futures-macro_0.3.31": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.52\"}],\"features\":{}}", "futures-sink_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "futures-sink_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", "futures-task_0.3.31": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", + "futures-task_0.3.32": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"cfg-target-has-atomic\":[],\"default\":[\"std\"],\"std\":[\"alloc\"],\"unstable\":[]}}", "futures-util_0.3.31": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"name\":\"pin-utils\",\"req\":\"^0.1.0\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", + "futures-util_0.3.32": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-channel\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.32\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-macro\",\"optional\":true,\"req\":\"=0.3.32\"},{\"default_features\":false,\"name\":\"futures-sink\",\"optional\":true,\"req\":\"^0.3.32\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.32\"},{\"name\":\"futures_01\",\"optional\":true,\"package\":\"futures\",\"req\":\"^0.1.25\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.26\"},{\"name\":\"memchr\",\"optional\":true,\"req\":\"^2.2\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.6\"},{\"default_features\":false,\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.7\"},{\"name\":\"spin\",\"optional\":true,\"req\":\"^0.10.0\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"},{\"name\":\"tokio-io\",\"optional\":true,\"req\":\"^0.1.9\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"slab\"],\"async-await\":[],\"async-await-macro\":[\"async-await\",\"futures-macro\"],\"bilock\":[],\"cfg-target-has-atomic\":[],\"channel\":[\"std\",\"futures-channel\"],\"compat\":[\"std\",\"futures_01\",\"libc\"],\"default\":[\"std\",\"async-await\",\"async-await-macro\"],\"io\":[\"std\",\"futures-io\",\"memchr\"],\"io-compat\":[\"io\",\"compat\",\"tokio-io\",\"libc\"],\"portable-atomic\":[\"futures-core/portable-atomic\"],\"sink\":[\"futures-sink\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"slab/std\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\"],\"write-all-vectored\":[\"io\"]}}", "futures_0.3.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"assert_matches\",\"req\":\"^1.3.0\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-channel\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-executor\",\"optional\":true,\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-io\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"futures-task\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"features\":[\"sink\"],\"name\":\"futures-util\",\"req\":\"^0.3.31\"},{\"kind\":\"dev\",\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^0.1.11\"}],\"features\":{\"alloc\":[\"futures-core/alloc\",\"futures-task/alloc\",\"futures-sink/alloc\",\"futures-channel/alloc\",\"futures-util/alloc\"],\"async-await\":[\"futures-util/async-await\",\"futures-util/async-await-macro\"],\"bilock\":[\"futures-util/bilock\"],\"cfg-target-has-atomic\":[],\"compat\":[\"std\",\"futures-util/compat\"],\"default\":[\"std\",\"async-await\",\"executor\"],\"executor\":[\"std\",\"futures-executor/std\"],\"io-compat\":[\"compat\",\"futures-util/io-compat\"],\"std\":[\"alloc\",\"futures-core/std\",\"futures-task/std\",\"futures-io/std\",\"futures-sink/std\",\"futures-util/std\",\"futures-util/io\",\"futures-util/channel\"],\"thread-pool\":[\"executor\",\"futures-executor/thread-pool\"],\"unstable\":[\"futures-core/unstable\",\"futures-task/unstable\",\"futures-channel/unstable\",\"futures-io/unstable\",\"futures-util/unstable\"],\"write-all-vectored\":[\"futures-util/write-all-vectored\"]}}", "fxhash_0.2.1": "{\"dependencies\":[{\"name\":\"byteorder\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"kind\":\"dev\",\"name\":\"seahash\",\"req\":\"^3.0.5\"}],\"features\":{}}", "generator_0.8.8": "{\"dependencies\":[{\"kind\":\"build\",\"name\":\"cc\",\"req\":\"^1.0\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"name\":\"libc\",\"req\":\"^0.2.100\",\"target\":\"cfg(unix)\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"kind\":\"build\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"name\":\"windows-link\",\"req\":\">=0.1, <=0.2\",\"target\":\"cfg(windows)\"},{\"name\":\"windows-result\",\"req\":\">=0.3.1, <=0.4\",\"target\":\"cfg(windows)\"}],\"features\":{}}", @@ -923,6 +951,7 @@ "gobject-sys_0.21.5": "{\"dependencies\":[{\"name\":\"glib-sys\",\"req\":\"^0.21\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"shell-words\",\"req\":\"^1.0.0\"},{\"kind\":\"build\",\"name\":\"system-deps\",\"req\":\"^7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"v2_58\":[],\"v2_62\":[\"v2_58\"],\"v2_66\":[\"v2_62\"],\"v2_68\":[\"v2_66\"],\"v2_70\":[\"v2_68\"],\"v2_72\":[\"v2_70\"],\"v2_74\":[\"v2_72\"],\"v2_76\":[\"v2_74\"],\"v2_78\":[\"v2_74\"],\"v2_80\":[\"v2_78\"],\"v2_82\":[\"v2_80\"],\"v2_84\":[\"v2_82\"],\"v2_86\":[\"v2_84\"]}}", "gzip-header_1.0.0": "{\"dependencies\":[{\"name\":\"crc32fast\",\"req\":\"^1.2.1\"}],\"features\":{}}", "h2_0.4.13": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", + "h2_0.4.6": "{\"dependencies\":[{\"name\":\"atomic-waker\",\"req\":\"^1.0.0\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"fnv\",\"req\":\"^1.0.5\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-sink\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"hex\",\"req\":\"^0.4.3\"},{\"name\":\"http\",\"req\":\"^1\"},{\"features\":[\"std\"],\"name\":\"indexmap\",\"req\":\"^2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.0\"},{\"name\":\"slab\",\"req\":\"^0.4.2\"},{\"features\":[\"io-util\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\",\"sync\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-rustls\",\"req\":\"^0.26\"},{\"features\":[\"codec\",\"io\"],\"name\":\"tokio-util\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.3.2\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^0.26\"}],\"features\":{\"stream\":[],\"unstable\":[]}}", "half_2.7.1": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4.1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"bytemuck\",\"optional\":true,\"req\":\"^1.4.1\"},{\"name\":\"cfg-if\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"name\":\"crunchy\",\"req\":\"^0.2.2\",\"target\":\"cfg(target_arch = \\\"spirv\\\")\"},{\"kind\":\"dev\",\"name\":\"crunchy\",\"req\":\"^0.2.2\"},{\"default_features\":false,\"features\":[\"libm\"],\"name\":\"num-traits\",\"optional\":true,\"req\":\"^0.2.16\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"thread_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.9.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.0\"},{\"default_features\":false,\"name\":\"rand_distr\",\"optional\":true,\"req\":\"^0.5.0\"},{\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.8.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"derive\",\"simd\"],\"name\":\"zerocopy\",\"req\":\"^0.8.26\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"nightly\":[],\"rand_distr\":[\"dep:rand\",\"dep:rand_distr\"],\"std\":[\"alloc\"],\"use-intrinsics\":[],\"zerocopy\":[]}}", "hashbrown_0.12.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"name\":\"bumpalo\",\"optional\":true,\"req\":\"^3.5.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"ahash-compile-time-rng\":[\"ahash/compile-time-rng\"],\"default\":[\"ahash\",\"inline-more\"],\"inline-more\":[],\"nightly\":[],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", "hashbrown_0.14.5": "{\"dependencies\":[{\"default_features\":false,\"name\":\"ahash\",\"optional\":true,\"req\":\"^0.8.7\"},{\"name\":\"alloc\",\"optional\":true,\"package\":\"rustc-std-workspace-alloc\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"allocator-api2\",\"optional\":true,\"req\":\"^0.2.9\"},{\"features\":[\"allocator-api2\"],\"kind\":\"dev\",\"name\":\"bumpalo\",\"req\":\"^3.13.0\"},{\"name\":\"compiler_builtins\",\"optional\":true,\"req\":\"^0.1.2\"},{\"name\":\"core\",\"optional\":true,\"package\":\"rustc-std-workspace-core\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"doc-comment\",\"req\":\"^0.3.1\"},{\"default_features\":false,\"name\":\"equivalent\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"fnv\",\"req\":\"^1.0.7\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.3\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rayon\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rkyv\",\"optional\":true,\"req\":\"^0.7.42\"},{\"features\":[\"validation\"],\"kind\":\"dev\",\"name\":\"rkyv\",\"req\":\"^0.7.42\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"ahash\",\"inline-more\",\"allocator-api2\"],\"inline-more\":[],\"nightly\":[\"allocator-api2?/nightly\",\"bumpalo/allocator_api\"],\"raw\":[],\"rustc-dep-of-std\":[\"nightly\",\"core\",\"compiler_builtins\",\"alloc\",\"rustc-internal-api\"],\"rustc-internal-api\":[]}}", @@ -1009,6 +1038,7 @@ "itertools_0.13.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itertools_0.14.0": "{\"dependencies\":[{\"features\":[\"html_reports\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"either\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"permutohedron\",\"req\":\"^0.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^0.9\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.7\"}],\"features\":{\"default\":[\"use_std\"],\"use_alloc\":[],\"use_std\":[\"use_alloc\",\"either/use_std\"]}}", "itoa_1.0.17": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{}}", + "itoa_1.0.18": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8\",\"target\":\"cfg(not(miri))\"},{\"name\":\"no-panic\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{}}", "ixdtf_0.6.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"serde-json-core\",\"req\":\"^0.6.0\"}],\"features\":{\"default\":[\"duration\"],\"duration\":[]}}", "jiff-static_0.2.18": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.93\"},{\"name\":\"quote\",\"req\":\"^1.0.38\"},{\"name\":\"syn\",\"req\":\"^2.0.98\"}],\"features\":{\"default\":[],\"perf-inline\":[],\"tz-fat\":[],\"tzdb\":[\"dep:jiff-tzdb\"]}}", "jiff-static_0.2.23": "{\"dependencies\":[{\"name\":\"jiff-tzdb\",\"optional\":true,\"req\":\"^0.1.6\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.93\"},{\"name\":\"quote\",\"req\":\"^1.0.38\"},{\"name\":\"syn\",\"req\":\"^2.0.98\"}],\"features\":{\"default\":[],\"perf-inline\":[],\"tz-fat\":[],\"tzdb\":[\"dep:jiff-tzdb\"]}}", @@ -1062,6 +1092,7 @@ "mach2_0.4.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"}],\"features\":{\"default\":[],\"unstable\":[]}}", "maplit_1.0.2": "{\"dependencies\":[],\"features\":{}}", "matchers_0.2.0": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"syntax\",\"dfa-build\",\"dfa-search\"],\"name\":\"regex-automata\",\"req\":\"^0.4\"}],\"features\":{\"unicode\":[\"regex-automata/unicode\"]}}", + "matchit_0.7.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "matchit_0.8.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.4\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.2.2\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.5.4\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5.2\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "matchit_0.9.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"actix-router\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"gonzales\",\"req\":\"^0.0.3-beta\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"server\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"path-tree\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"route-recognizer\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"routefinder\",\"req\":\"^0.5\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"make\",\"util\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5.2\"},{\"kind\":\"dev\",\"name\":\"wayfind\",\"req\":\"^0.8\"}],\"features\":{\"__test_helpers\":[],\"default\":[]}}", "md-5_0.10.6": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1.0\"},{\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"features\":[\"dev\"],\"kind\":\"dev\",\"name\":\"digest\",\"req\":\"^0.10.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.2.2\"},{\"name\":\"md5-asm\",\"optional\":true,\"req\":\"^0.5\",\"target\":\"cfg(any(target_arch = \\\"x86\\\", target_arch = \\\"x86_64\\\"))\"}],\"features\":{\"asm\":[\"md5-asm\"],\"default\":[\"std\"],\"force-soft\":[],\"loongarch64_asm\":[],\"oid\":[\"digest/oid\"],\"std\":[\"digest/std\"]}}", @@ -1174,9 +1205,11 @@ "petgraph_0.8.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.3\"},{\"kind\":\"dev\",\"name\":\"defmac\",\"req\":\"^0.2.1\"},{\"name\":\"dot-parser\",\"optional\":true,\"req\":\"^0.5.1\"},{\"name\":\"dot-parser-macros\",\"optional\":true,\"req\":\"^0.5.1\"},{\"default_features\":false,\"name\":\"fixedbitset\",\"req\":\"^0.5.7\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2.1\"},{\"default_features\":false,\"features\":[\"default-hasher\",\"inline-more\"],\"name\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.12.1\"},{\"kind\":\"dev\",\"name\":\"odds\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.5.5\"},{\"name\":\"rayon\",\"optional\":true,\"req\":\"^1.5.3\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"all\":[\"unstable\",\"quickcheck\",\"matrix_graph\",\"stable_graph\",\"graphmap\",\"rayon\",\"dot_parser\"],\"default\":[\"std\",\"graphmap\",\"stable_graph\",\"matrix_graph\"],\"dot_parser\":[\"std\",\"dep:dot-parser\",\"dep:dot-parser-macros\"],\"generate\":[],\"graphmap\":[],\"matrix_graph\":[],\"quickcheck\":[\"std\",\"dep:quickcheck\",\"graphmap\",\"stable_graph\"],\"rayon\":[\"std\",\"dep:rayon\",\"indexmap/rayon\",\"hashbrown/rayon\"],\"serde-1\":[\"serde\",\"serde_derive\"],\"stable_graph\":[\"serde?/alloc\"],\"std\":[\"indexmap/std\"],\"unstable\":[\"generate\"]}}", "phf_shared_0.11.3": "{\"dependencies\":[{\"name\":\"siphasher\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"uncased\",\"optional\":true,\"req\":\"^0.9.9\"},{\"name\":\"unicase\",\"optional\":true,\"req\":\"^2.4.0\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}", "pin-project-internal_1.1.10": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0.25\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\",\"full\",\"visit-mut\"],\"name\":\"syn\",\"req\":\"^2.0.1\"}],\"features\":{}}", + "pin-project-internal_1.1.11": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0.25\"},{\"default_features\":false,\"features\":[\"parsing\",\"printing\",\"clone-impls\",\"proc-macro\",\"full\",\"visit-mut\"],\"name\":\"syn\",\"req\":\"^2.0.1\"}],\"features\":{}}", "pin-project-lite_0.2.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "pin-project-lite_0.2.17": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "pin-project_1.1.10": "{\"dependencies\":[{\"name\":\"pin-project-internal\",\"req\":\"=1.1.10\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", + "pin-project_1.1.11": "{\"dependencies\":[{\"name\":\"pin-project-internal\",\"req\":\"=1.1.11\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"}],\"features\":{}}", "pin-utils_0.1.0": "{\"dependencies\":[],\"features\":{}}", "piper_0.2.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-channel\",\"req\":\"^2.0.0\"},{\"kind\":\"dev\",\"name\":\"async-executor\",\"req\":\"^1.5.1\"},{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.0.0\"},{\"name\":\"atomic-waker\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.2.0\"},{\"default_features\":false,\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.28\"},{\"kind\":\"dev\",\"name\":\"futures-lite\",\"req\":\"^2.0.0\"},{\"features\":[\"alloc\"],\"name\":\"portable-atomic-util\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"portable_atomic_crate\",\"optional\":true,\"package\":\"portable-atomic\",\"req\":\"^1.2.0\"}],\"features\":{\"default\":[\"std\"],\"portable-atomic\":[\"atomic-waker/portable-atomic\",\"portable_atomic_crate\",\"portable-atomic-util\"],\"std\":[\"fastrand/std\",\"futures-io\"]}}", "pkcs1_0.7.5": "{\"dependencies\":[{\"features\":[\"db\"],\"kind\":\"dev\",\"name\":\"const-oid\",\"req\":\"^0.9\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"pkcs8\",\"optional\":true,\"req\":\"^0.10\"},{\"name\":\"spki\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"der/alloc\",\"zeroize\",\"pkcs8?/alloc\"],\"pem\":[\"alloc\",\"der/pem\",\"pkcs8?/pem\"],\"std\":[\"der/std\",\"alloc\"],\"zeroize\":[\"der/zeroize\"]}}", @@ -1188,6 +1221,7 @@ "polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}", "poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}", "portable-atomic-util_0.2.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"req\":\"^1.5.1\"}],\"features\":{\"alloc\":[],\"default\":[],\"std\":[\"alloc\"]}}", + "portable-atomic-util_0.2.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"default_features\":false,\"features\":[\"require-cas\"],\"name\":\"portable-atomic\",\"req\":\"^1.5.1\"}],\"features\":{\"alloc\":[],\"default\":[],\"std\":[\"alloc\"]}}", "portable-atomic_1.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"build-context\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"crabgrind\",\"req\":\"^0.1\",\"target\":\"cfg(valgrind)\"},{\"name\":\"critical-section\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"=0.8.16\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"=0.2.163\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"paste\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.60\"},{\"kind\":\"dev\",\"name\":\"sptr\",\"req\":\"^0.3\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1\"},{\"features\":[\"Win32_Foundation\",\"Win32_System_Threading\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[\"fallback\"],\"disable-fiq\":[],\"fallback\":[],\"float\":[],\"force-amo\":[],\"require-cas\":[],\"s-mode\":[],\"std\":[],\"unsafe-assume-privileged\":[],\"unsafe-assume-single-core\":[]}}", "portable-pty_0.9.0": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0\"},{\"name\":\"bitflags\",\"req\":\"^1.3\",\"target\":\"cfg(windows)\"},{\"name\":\"downcast-rs\",\"req\":\"^1.0\"},{\"name\":\"filedescriptor\",\"req\":\"^0.8.3\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"lazy_static\",\"req\":\"^1.4\",\"target\":\"cfg(windows)\"},{\"name\":\"libc\",\"req\":\"^0.2\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"features\":[\"term\",\"fs\"],\"name\":\"nix\",\"req\":\"^0.28\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde_derive\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serial2\",\"req\":\"^0.2\"},{\"name\":\"shared_library\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"name\":\"shell-words\",\"req\":\"^1.1\"},{\"kind\":\"dev\",\"name\":\"smol\",\"req\":\"^2.0\"},{\"features\":[\"winuser\",\"consoleapi\",\"handleapi\",\"fileapi\",\"namedpipeapi\",\"synchapi\"],\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"},{\"name\":\"winreg\",\"req\":\"^0.10\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"serde_support\":[\"serde\",\"serde_derive\"]}}", "potential_utf_0.1.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"writeable\",\"optional\":true,\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"writeable/alloc\",\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\"],\"writeable\":[\"dep:writeable\"],\"zerovec\":[\"dep:zerovec\"]}}", @@ -1206,13 +1240,19 @@ "process-wrap_9.0.1": "{\"dependencies\":[{\"name\":\"futures\",\"optional\":true,\"req\":\"^0.3.30\"},{\"name\":\"indexmap\",\"req\":\"^2.9.0\"},{\"default_features\":false,\"features\":[\"fs\",\"poll\",\"signal\"],\"name\":\"nix\",\"optional\":true,\"req\":\"^0.30.1\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"remoteprocess\",\"req\":\"^0.5.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.20.0\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.38.2\"},{\"features\":[\"io-util\",\"macros\",\"process\",\"rt\",\"rt-multi-thread\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.38.2\"},{\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.40\"},{\"name\":\"windows\",\"optional\":true,\"req\":\"^0.62.2\",\"target\":\"cfg(windows)\"}],\"features\":{\"creation-flags\":[\"dep:windows\",\"windows/Win32_System_Threading\"],\"default\":[\"creation-flags\",\"job-object\",\"kill-on-drop\",\"process-group\",\"process-session\",\"tracing\"],\"job-object\":[\"dep:windows\",\"windows/Win32_Security\",\"windows/Win32_System_Diagnostics_ToolHelp\",\"windows/Win32_System_IO\",\"windows/Win32_System_JobObjects\",\"windows/Win32_System_Threading\"],\"kill-on-drop\":[],\"process-group\":[],\"process-session\":[\"process-group\"],\"reset-sigmask\":[],\"std\":[\"dep:nix\"],\"tokio1\":[\"dep:nix\",\"dep:futures\",\"dep:tokio\"],\"tracing\":[\"dep:tracing\"]}}", "proptest_1.9.0": "{\"dependencies\":[{\"name\":\"bit-set\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.8.0\"},{\"name\":\"bitflags\",\"req\":\"^2.9\"},{\"default_features\":false,\"name\":\"num-traits\",\"req\":\"^0.2.15\"},{\"name\":\"proptest-macro\",\"optional\":true,\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"rand_chacha\",\"req\":\"^0.9\"},{\"name\":\"rand_xorshift\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"regex\",\"req\":\"^1.0\"},{\"name\":\"regex-syntax\",\"optional\":true,\"req\":\"^0.8\"},{\"default_features\":false,\"name\":\"rusty-fork\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"tempfile\",\"optional\":true,\"req\":\"^3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.112\"},{\"name\":\"unarray\",\"req\":\"^0.1.4\"},{\"name\":\"x86\",\"optional\":true,\"req\":\"^0.52.0\"}],\"features\":{\"alloc\":[],\"atomic64bit\":[],\"attr-macro\":[\"proptest-macro\"],\"bit-set\":[\"dep:bit-set\",\"dep:bit-vec\"],\"default\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"default-code-coverage\":[\"std\",\"fork\",\"timeout\",\"bit-set\"],\"fork\":[\"std\",\"rusty-fork\",\"tempfile\"],\"handle-panics\":[\"std\"],\"hardware-rng\":[\"x86\"],\"no_std\":[\"num-traits/libm\"],\"std\":[\"rand/std\",\"rand/os_rng\",\"regex-syntax\",\"num-traits/std\"],\"timeout\":[\"fork\",\"rusty-fork/timeout\"],\"unstable\":[]}}", "prost-build_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"name\":\"once_cell\",\"req\":\"^1.17.1\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.6\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.12.6\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.9.1\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^10.0.1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}", + "prost-build_0.13.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.13\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"name\":\"once_cell\",\"req\":\"^1.17.1\"},{\"default_features\":false,\"name\":\"petgraph\",\"req\":\"^0.6\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.9.1\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^10.0.1\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}", "prost-build_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.11\"},{\"name\":\"heck\",\"req\":\">=0.4, <=0.5\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.14\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"default_features\":false,\"name\":\"multimap\",\"req\":\">=0.8, <=0.10\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"petgraph\",\"req\":\"^0.8\"},{\"name\":\"prettyplease\",\"optional\":true,\"req\":\"^0.2\"},{\"default_features\":false,\"name\":\"prost\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.14.3\"},{\"default_features\":false,\"name\":\"pulldown-cmark\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"pulldown-cmark-to-cmark\",\"optional\":true,\"req\":\"^22\"},{\"default_features\":false,\"features\":[\"std\",\"unicode-bool\"],\"name\":\"regex\",\"req\":\"^1.8.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"optional\":true,\"req\":\"^2\"},{\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"cleanup-markdown\":[\"dep:pulldown-cmark\",\"dep:pulldown-cmark-to-cmark\"],\"default\":[\"format\"],\"format\":[\"dep:prettyplease\",\"dep:syn\"]}}", "prost-derive_0.12.6": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"use_alloc\"],\"name\":\"itertools\",\"req\":\">=0.10, <=0.12\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", + "prost-derive_0.13.1": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.13\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "prost-derive_0.14.3": "{\"dependencies\":[{\"name\":\"anyhow\",\"req\":\"^1.0.1\"},{\"name\":\"itertools\",\"req\":\">=0.10.1, <=0.14\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"extra-traits\"],\"name\":\"syn\",\"req\":\"^2\"}],\"features\":{}}", "prost-types_0.12.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"prost-derive\"],\"name\":\"prost\",\"req\":\"^0.12.6\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"prost/std\"]}}", + "prost-types_0.13.1": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"prost-derive\"],\"name\":\"prost\",\"req\":\"^0.13.1\"}],\"features\":{\"default\":[\"std\"],\"std\":[\"prost/std\"]}}", "prost-types_0.14.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.4\"},{\"default_features\":false,\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"derive\"],\"name\":\"prost\",\"req\":\"^0.14.3\"}],\"features\":{\"arbitrary\":[\"dep:arbitrary\"],\"default\":[\"std\"],\"std\":[\"prost/std\"]}}", "prost_0.12.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.12.6\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"prost-derive\":[\"derive\"],\"std\":[]}}", + "prost_0.13.1": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.13.1\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"prost-derive\":[\"derive\"],\"std\":[]}}", "prost_0.14.3": "{\"dependencies\":[{\"default_features\":false,\"name\":\"bytes\",\"req\":\"^1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"prost-derive\",\"optional\":true,\"req\":\"^0.14.3\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"}],\"features\":{\"default\":[\"derive\",\"std\"],\"derive\":[\"dep:prost-derive\"],\"no-recursion-limit\":[],\"std\":[]}}", + "protoc-gen-prost_0.4.0": "{\"dependencies\":[{\"name\":\"once_cell\",\"req\":\"^1.10.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-build\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"regex\",\"req\":\"^1.5.5\"}],\"features\":{}}", + "protoc-gen-tonic_0.4.1": "{\"dependencies\":[{\"name\":\"heck\",\"req\":\"^0.5.0\"},{\"name\":\"prettyplease\",\"req\":\"^0.2.9\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"prost\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-build\",\"req\":\"^0.13.1\"},{\"default_features\":false,\"name\":\"prost-types\",\"req\":\"^0.13.1\"},{\"name\":\"protoc-gen-prost\",\"req\":\"^0.4.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"regex\",\"req\":\"^1.5.5\"},{\"features\":[\"parsing\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.22\"},{\"name\":\"tonic-build\",\"req\":\"^0.12.0\"}],\"features\":{}}", "psl-types_2.0.11": "{\"dependencies\":[],\"features\":{}}", "psl_2.1.184": "{\"dependencies\":[{\"name\":\"psl-types\",\"req\":\"^2.0.11\"},{\"kind\":\"dev\",\"name\":\"rspec\",\"req\":\"^1.0.0\"}],\"features\":{\"default\":[\"helpers\"],\"helpers\":[]}}", "pulldown-cmark-escape_0.10.1": "{\"dependencies\":[],\"features\":{\"simd\":[]}}", @@ -1372,6 +1412,7 @@ "smol_str_0.3.5": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.3\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.5\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[\"std\"],\"serde\":[\"dep:serde_core\"],\"std\":[\"serde_core?/std\",\"borsh?/std\"]}}", "socket2_0.5.10": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.171\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", "socket2_0.6.2": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.172\",\"target\":\"cfg(unix)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.60\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", + "socket2_0.6.3": "{\"dependencies\":[{\"name\":\"libc\",\"req\":\"^0.2.172\",\"target\":\"cfg(any(unix, target_os = \\\"wasi\\\"))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_System_IO\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\">=0.60, <0.62\",\"target\":\"cfg(windows)\"}],\"features\":{\"all\":[]}}", "spin_0.9.8": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4\"},{\"name\":\"lock_api_crate\",\"optional\":true,\"package\":\"lock_api\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"portable-atomic\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"barrier\":[\"mutex\"],\"default\":[\"lock_api\",\"mutex\",\"spin_mutex\",\"rwlock\",\"once\",\"lazy\",\"barrier\"],\"fair_mutex\":[\"mutex\"],\"lazy\":[\"once\"],\"lock_api\":[\"lock_api_crate\"],\"mutex\":[],\"once\":[],\"portable_atomic\":[\"portable-atomic\"],\"rwlock\":[],\"spin_mutex\":[\"mutex\"],\"std\":[],\"ticket_mutex\":[\"mutex\"],\"use_ticket_mutex\":[\"mutex\",\"ticket_mutex\"]}}", "spki_0.7.3": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1.2\"},{\"default_features\":false,\"name\":\"base64ct\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7.2\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"alloc\":[\"base64ct?/alloc\",\"der/alloc\"],\"arbitrary\":[\"std\",\"dep:arbitrary\",\"der/arbitrary\"],\"base64\":[\"dep:base64ct\"],\"fingerprint\":[\"sha2\"],\"pem\":[\"alloc\",\"der/pem\"],\"std\":[\"der/std\",\"alloc\"]}}", "sqlx-core_0.8.6": "{\"dependencies\":[{\"name\":\"async-io\",\"optional\":true,\"req\":\"^1.9.0\"},{\"name\":\"async-std\",\"optional\":true,\"req\":\"^1.12\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"bigdecimal\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"bit-vec\",\"optional\":true,\"req\":\"^0.6.3\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"bstr\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"bytes\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"clock\"],\"name\":\"chrono\",\"optional\":true,\"req\":\"^0.4.34\"},{\"name\":\"crc\",\"optional\":true,\"req\":\"^3\"},{\"name\":\"crossbeam-queue\",\"req\":\"^0.3.2\"},{\"name\":\"either\",\"req\":\"^1.6.1\"},{\"name\":\"event-listener\",\"req\":\"^5.2.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"req\":\"^0.3.19\"},{\"name\":\"futures-intrusive\",\"req\":\"^0.5.0\"},{\"name\":\"futures-io\",\"req\":\"^0.3.24\"},{\"default_features\":false,\"features\":[\"alloc\",\"sink\",\"io\"],\"name\":\"futures-util\",\"req\":\"^0.3.19\"},{\"name\":\"hashbrown\",\"req\":\"^0.15.0\"},{\"name\":\"hashlink\",\"req\":\"^0.10.0\"},{\"name\":\"indexmap\",\"req\":\"^2.0\"},{\"name\":\"ipnet\",\"optional\":true,\"req\":\"^2.3.0\"},{\"name\":\"ipnetwork\",\"optional\":true,\"req\":\"^0.20.0\"},{\"default_features\":false,\"name\":\"log\",\"req\":\"^0.4.18\"},{\"name\":\"mac_address\",\"optional\":true,\"req\":\"^1.1.5\"},{\"default_features\":false,\"name\":\"memchr\",\"req\":\"^2.4.1\"},{\"name\":\"native-tls\",\"optional\":true,\"req\":\"^0.2.10\"},{\"name\":\"once_cell\",\"req\":\"^1.9.0\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1.0\"},{\"name\":\"regex\",\"optional\":true,\"req\":\"^1.5.5\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rust_decimal\",\"optional\":true,\"req\":\"^1.26.1\"},{\"default_features\":false,\"features\":[\"std\",\"tls12\"],\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.23.15\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8.0\"},{\"features\":[\"derive\",\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.132\"},{\"features\":[\"raw_value\"],\"name\":\"serde_json\",\"optional\":true,\"req\":\"^1.0.73\"},{\"default_features\":false,\"name\":\"sha2\",\"optional\":true,\"req\":\"^0.10.0\"},{\"name\":\"smallvec\",\"req\":\"^1.7.0\"},{\"default_features\":false,\"features\":[\"postgres\",\"sqlite\",\"mysql\",\"migrate\",\"macros\",\"time\",\"uuid\"],\"kind\":\"dev\",\"name\":\"sqlx\",\"req\":\"=0.8.6\"},{\"name\":\"thiserror\",\"req\":\"^2.0.0\"},{\"features\":[\"formatting\",\"parsing\",\"macros\"],\"name\":\"time\",\"optional\":true,\"req\":\"^0.3.36\"},{\"default_features\":false,\"features\":[\"time\",\"net\",\"sync\",\"fs\",\"io-util\",\"rt\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"fs\"],\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.8\"},{\"features\":[\"log\"],\"name\":\"tracing\",\"req\":\"^0.1.37\"},{\"name\":\"url\",\"req\":\"^2.2.2\"},{\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.1.2\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"}],\"features\":{\"_rt-async-std\":[\"async-std\",\"async-io\"],\"_rt-tokio\":[\"tokio\",\"tokio-stream\"],\"_tls-native-tls\":[\"native-tls\"],\"_tls-none\":[],\"_tls-rustls\":[\"rustls\"],\"_tls-rustls-aws-lc-rs\":[\"_tls-rustls\",\"rustls/aws-lc-rs\",\"webpki-roots\"],\"_tls-rustls-ring-native-roots\":[\"_tls-rustls\",\"rustls/ring\",\"rustls-native-certs\"],\"_tls-rustls-ring-webpki\":[\"_tls-rustls\",\"rustls/ring\",\"webpki-roots\"],\"any\":[],\"default\":[],\"json\":[\"serde\",\"serde_json\"],\"migrate\":[\"sha2\",\"crc\"],\"offline\":[\"serde\",\"either/serde\"]}}", @@ -1448,12 +1489,16 @@ "tinyvec_1.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"smallvec\",\"req\":\"^1\"},{\"name\":\"tinyvec_macros\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"alloc\":[\"tinyvec_macros\"],\"debugger_visualizer\":[],\"default\":[],\"experimental_write_impl\":[],\"grab_spare_slice\":[],\"latest_stable_rust\":[\"rustc_1_61\"],\"nightly_slice_partition_dedup\":[],\"real_blackbox\":[\"criterion/real_blackbox\"],\"rustc_1_40\":[],\"rustc_1_55\":[],\"rustc_1_57\":[],\"rustc_1_61\":[\"rustc_1_57\"],\"std\":[\"alloc\"]}}", "tinyvec_macros_0.1.1": "{\"dependencies\":[],\"features\":{}}", "tokio-graceful_0.2.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\",\"target\":\"cfg(not(loom))\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"server\",\"http1\",\"http2\"],\"kind\":\"dev\",\"name\":\"hyper\",\"req\":\"^1.0.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"server\",\"server-auto\",\"http1\",\"http2\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1.1\",\"target\":\"cfg(not(loom))\"},{\"features\":[\"futures\",\"checkpoint\"],\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"slab\",\"req\":\"^0.4\"},{\"features\":[\"rt\",\"signal\",\"sync\",\"macros\",\"time\"],\"name\":\"tokio\",\"req\":\"^1\"},{\"features\":[\"net\",\"rt-multi-thread\",\"io-util\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{}}", + "tokio-macros_2.4.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"}],\"features\":{}}", "tokio-macros_2.6.0": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"}],\"features\":{}}", "tokio-native-tls_0.3.1": "{\"dependencies\":[{\"name\":\"native-tls\",\"req\":\"^0.2\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"cfg-if\",\"req\":\"^0.1\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.6\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1\"},{\"features\":[\"macros\",\"rt\",\"rt-multi-thread\",\"io-util\",\"net\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.6.0\"},{\"kind\":\"dev\",\"name\":\"openssl\",\"req\":\"^0.10\",\"target\":\"cfg(all(not(target_os = \\\"macos\\\"), not(windows), not(target_os = \\\"ios\\\")))\"},{\"kind\":\"dev\",\"name\":\"security-framework\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_os = \\\"macos\\\", target_os = \\\"ios\\\"))\"},{\"kind\":\"dev\",\"name\":\"schannel\",\"req\":\"^0.1\",\"target\":\"cfg(windows)\"},{\"features\":[\"lmcons\",\"basetsd\",\"minwinbase\",\"minwindef\",\"ntdef\",\"sysinfoapi\",\"timezoneapi\",\"wincrypt\",\"winerror\"],\"kind\":\"dev\",\"name\":\"winapi\",\"req\":\"^0.3\",\"target\":\"cfg(windows)\"}],\"features\":{\"vendored\":[\"native-tls/vendored\"]}}", "tokio-rustls_0.26.4": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"argh\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.1\"},{\"features\":[\"pem\"],\"kind\":\"dev\",\"name\":\"rcgen\",\"req\":\"^0.14\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"rustls\",\"req\":\"^0.23.27\"},{\"name\":\"tokio\",\"req\":\"^1.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"webpki-roots\",\"req\":\"^1\"}],\"features\":{\"aws-lc-rs\":[\"aws_lc_rs\"],\"aws_lc_rs\":[\"rustls/aws_lc_rs\"],\"brotli\":[\"rustls/brotli\"],\"default\":[\"logging\",\"tls12\",\"aws_lc_rs\"],\"early-data\":[],\"fips\":[\"rustls/fips\"],\"logging\":[\"rustls/logging\"],\"ring\":[\"rustls/ring\"],\"tls12\":[\"rustls/tls12\"],\"zlib\":[\"rustls/zlib\"]}}", + "tokio-stream_0.1.15": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"}],\"features\":{\"default\":[\"time\"],\"fs\":[\"tokio/fs\"],\"full\":[\"time\",\"net\",\"io-util\",\"fs\",\"sync\",\"signal\"],\"io-util\":[\"tokio/io-util\"],\"net\":[\"tokio/net\"],\"signal\":[\"tokio/signal\"],\"sync\":[\"tokio/sync\",\"tokio-util\"],\"time\":[\"tokio/time\"]}}", "tokio-stream_0.1.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.15.0\"},{\"features\":[\"full\",\"test-util\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"}],\"features\":{\"default\":[\"time\"],\"fs\":[\"tokio/fs\"],\"full\":[\"time\",\"net\",\"io-util\",\"fs\",\"sync\",\"signal\"],\"io-util\":[\"tokio/io-util\"],\"net\":[\"tokio/net\"],\"signal\":[\"tokio/signal\"],\"sync\":[\"tokio/sync\",\"tokio-util\"],\"time\":[\"tokio/time\"]}}", "tokio-test_0.4.5": "{\"dependencies\":[{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.0\"},{\"features\":[\"rt\",\"sync\",\"time\",\"test-util\"],\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.2.0\"},{\"name\":\"tokio-stream\",\"req\":\"^0.1.1\"}],\"features\":{}}", + "tokio-util_0.7.17": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3.0\"},{\"name\":\"bytes\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"futures-sink\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.5\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.0\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.28.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\"}],\"features\":{\"__docs_rs\":[\"futures-util\"],\"codec\":[],\"compat\":[\"futures-io\"],\"default\":[],\"full\":[\"codec\",\"compat\",\"io-util\",\"time\",\"net\",\"rt\",\"join-map\"],\"io\":[],\"io-util\":[\"io\",\"tokio/rt\",\"tokio/io-util\"],\"join-map\":[\"rt\",\"hashbrown\"],\"net\":[\"tokio/net\"],\"rt\":[\"tokio/rt\",\"tokio/sync\",\"futures-util\"],\"time\":[\"tokio/time\",\"slab\"]}}", "tokio-util_0.7.18": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3.0\"},{\"name\":\"bytes\",\"req\":\"^1.5.0\"},{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"name\":\"futures-core\",\"req\":\"^0.3.0\"},{\"name\":\"futures-io\",\"optional\":true,\"req\":\"^0.3.0\"},{\"name\":\"futures-sink\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.5\"},{\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.0\"},{\"default_features\":false,\"name\":\"hashbrown\",\"optional\":true,\"req\":\"^0.15.0\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.4\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"req\":\"^1.44.0\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\"}],\"features\":{\"__docs_rs\":[\"futures-util\"],\"codec\":[],\"compat\":[\"futures-io\"],\"default\":[],\"full\":[\"codec\",\"compat\",\"io-util\",\"time\",\"net\",\"rt\",\"join-map\"],\"io\":[],\"io-util\":[\"io\",\"tokio/rt\",\"tokio/io-util\"],\"join-map\":[\"rt\",\"hashbrown\"],\"net\":[\"tokio/net\"],\"rt\":[\"tokio/rt\",\"tokio/sync\",\"futures-util\"],\"time\":[\"tokio/time\",\"slab\"]}}", + "tokio_1.39.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"backtrace\",\"req\":\"^0.3.58\",\"target\":\"cfg(tokio_taskdump)\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.0.0\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.149\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.149\",\"target\":\"cfg(unix)\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"mio-aio\",\"req\":\"^0.9.0\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.11.1\"},{\"default_features\":false,\"features\":[\"aio\",\"fs\",\"socket\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\",\"target\":\"cfg(unix)\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.0\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"signal-hook-registry\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(unix)\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.5.5\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.5.5\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"tokio-macros\",\"optional\":true,\"req\":\"~2.4.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.25\",\"target\":\"cfg(tokio_unstable)\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.0\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.52\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Authorization\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.52\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"fs\":[],\"full\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"process\",\"rt\",\"rt-multi-thread\",\"signal\",\"sync\",\"time\"],\"io-std\":[],\"io-util\":[\"bytes\"],\"macros\":[\"tokio-macros\"],\"net\":[\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"socket2\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_Security\",\"windows-sys/Win32_Storage_FileSystem\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_System_SystemServices\"],\"process\":[\"bytes\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Threading\",\"windows-sys/Win32_System_WindowsProgramming\"],\"rt\":[],\"rt-multi-thread\":[\"rt\"],\"signal\":[\"libc\",\"mio/os-poll\",\"mio/net\",\"mio/os-ext\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Console\"],\"sync\":[],\"test-util\":[\"rt\",\"sync\",\"time\"],\"time\":[]}}", "tokio_1.49.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-stream\",\"req\":\"^0.3\"},{\"name\":\"backtrace\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"bytes\",\"optional\":true,\"req\":\"^1.2.1\"},{\"features\":[\"async-await\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"futures-concurrency\",\"req\":\"^7.6.3\"},{\"kind\":\"dev\",\"name\":\"futures-test\",\"req\":\"^0.3.31\"},{\"default_features\":false,\"name\":\"io-uring\",\"optional\":true,\"req\":\"^0.7.6\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2.168\",\"target\":\"cfg(unix)\"},{\"features\":[\"futures\",\"checkpoint\"],\"kind\":\"dev\",\"name\":\"loom\",\"req\":\"^0.7\",\"target\":\"cfg(loom)\"},{\"default_features\":false,\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\"},{\"default_features\":false,\"features\":[\"os-poll\",\"os-ext\"],\"name\":\"mio\",\"optional\":true,\"req\":\"^1.0.1\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"tokio\"],\"kind\":\"dev\",\"name\":\"mio-aio\",\"req\":\"^1\",\"target\":\"cfg(target_os = \\\"freebsd\\\")\"},{\"kind\":\"dev\",\"name\":\"mockall\",\"req\":\"^0.13.0\"},{\"default_features\":false,\"features\":[\"aio\",\"fs\",\"socket\"],\"kind\":\"dev\",\"name\":\"nix\",\"req\":\"^0.29.0\",\"target\":\"cfg(unix)\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.11\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\",\"target\":\"cfg(not(all(target_family = \\\"wasm\\\", target_os = \\\"unknown\\\")))\"},{\"name\":\"signal-hook-registry\",\"optional\":true,\"req\":\"^1.1.1\",\"target\":\"cfg(unix)\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.9\",\"target\":\"cfg(all(tokio_unstable, target_os = \\\"linux\\\"))\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3.1.0\",\"target\":\"cfg(not(target_family = \\\"wasm\\\"))\"},{\"name\":\"tokio-macros\",\"optional\":true,\"req\":\"~2.6.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.0\"},{\"features\":[\"rt\"],\"kind\":\"dev\",\"name\":\"tokio-util\",\"req\":\"^0.7\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.29\",\"target\":\"cfg(tokio_unstable)\"},{\"kind\":\"dev\",\"name\":\"tracing-mock\",\"req\":\"=0.1.0-beta.1\",\"target\":\"cfg(all(tokio_unstable, target_has_atomic = \\\"64\\\"))\"},{\"kind\":\"dev\",\"name\":\"wasm-bindgen-test\",\"req\":\"^0.3.0\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(target_os = \\\"wasi\\\")))\"},{\"name\":\"windows-sys\",\"optional\":true,\"req\":\"^0.61\",\"target\":\"cfg(windows)\"},{\"features\":[\"Win32_Foundation\",\"Win32_Security_Authorization\"],\"kind\":\"dev\",\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{\"default\":[],\"fs\":[],\"full\":[\"fs\",\"io-util\",\"io-std\",\"macros\",\"net\",\"parking_lot\",\"process\",\"rt\",\"rt-multi-thread\",\"signal\",\"sync\",\"time\"],\"io-std\":[],\"io-uring\":[\"dep:io-uring\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"dep:slab\"],\"io-util\":[\"bytes\"],\"macros\":[\"tokio-macros\"],\"net\":[\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"socket2\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_Security\",\"windows-sys/Win32_Storage_FileSystem\",\"windows-sys/Win32_System_Pipes\",\"windows-sys/Win32_System_SystemServices\"],\"process\":[\"bytes\",\"libc\",\"mio/os-poll\",\"mio/os-ext\",\"mio/net\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Threading\",\"windows-sys/Win32_System_WindowsProgramming\"],\"rt\":[],\"rt-multi-thread\":[\"rt\"],\"signal\":[\"libc\",\"mio/os-poll\",\"mio/net\",\"mio/os-ext\",\"signal-hook-registry\",\"windows-sys/Win32_Foundation\",\"windows-sys/Win32_System_Console\"],\"sync\":[],\"taskdump\":[\"dep:backtrace\"],\"test-util\":[\"rt\",\"sync\",\"time\"],\"time\":[]}}", "toml_0.5.11": "{\"dependencies\":[{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"serde\",\"req\":\"^1.0.97\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"}],\"features\":{\"default\":[],\"preserve_order\":[\"indexmap\"]}}", "toml_0.9.11+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"default_features\":false,\"name\":\"foldhash\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.11.4\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde-untagged\",\"req\":\"^0.1.9\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.225\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.145\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"serde_spanned\",\"req\":\"^1.0.4\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml-test-data\",\"req\":\"^2.3.3\"},{\"features\":[\"snapshot\"],\"kind\":\"dev\",\"name\":\"toml-test-harness\",\"req\":\"^1.3.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_datetime\",\"req\":\"^0.7.5\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_parser\",\"optional\":true,\"req\":\"^1.0.6\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"toml_writer\",\"optional\":true,\"req\":\"^1.0.6\"},{\"kind\":\"dev\",\"name\":\"walkdir\",\"req\":\"^2.5.0\"},{\"default_features\":false,\"name\":\"winnow\",\"optional\":true,\"req\":\"^0.7.13\"}],\"features\":{\"debug\":[\"std\",\"toml_parser?/debug\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\",\"serde\",\"parse\",\"display\"],\"display\":[\"dep:toml_writer\"],\"fast_hash\":[\"preserve_order\",\"dep:foldhash\"],\"parse\":[\"dep:toml_parser\",\"dep:winnow\"],\"preserve_order\":[\"dep:indexmap\",\"std\"],\"serde\":[\"dep:serde_core\",\"toml_datetime/serde\",\"serde_spanned/serde\"],\"std\":[\"indexmap?/std\",\"serde_core?/std\",\"toml_parser?/std\",\"toml_writer?/std\",\"toml_datetime/std\",\"serde_spanned/std\"],\"unbounded\":[]}}", @@ -1464,13 +1509,16 @@ "toml_parser_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", "toml_parser_1.0.9+spec-1.1.0": "{\"dependencies\":[{\"name\":\"anstream\",\"optional\":true,\"req\":\"^0.6.20\"},{\"features\":[\"test\"],\"kind\":\"dev\",\"name\":\"anstream\",\"req\":\"^0.6.20\"},{\"name\":\"anstyle\",\"optional\":true,\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"default_features\":false,\"name\":\"winnow\",\"req\":\"^0.7.13\"}],\"features\":{\"alloc\":[],\"debug\":[\"std\",\"dep:anstream\",\"dep:anstyle\"],\"default\":[\"std\"],\"simd\":[\"winnow/simd\"],\"std\":[\"alloc\"],\"unsafe\":[]}}", "toml_writer_1.0.6+spec-1.1.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1.7.0\"},{\"kind\":\"dev\",\"name\":\"snapbox\",\"req\":\"^0.6.21\"},{\"kind\":\"dev\",\"name\":\"toml_old\",\"package\":\"toml\",\"req\":\"^0.5.11\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}", + "tonic-build_0.12.3": "{\"dependencies\":[{\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"prost-build\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"prost-types\",\"optional\":true,\"req\":\"^0.13\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{\"cleanup-markdown\":[\"prost\",\"prost-build/cleanup-markdown\"],\"default\":[\"transport\",\"prost\"],\"prost\":[\"prost-build\",\"dep:prost-types\"],\"transport\":[]}}", "tonic-build_0.14.3": "{\"dependencies\":[{\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"}],\"features\":{\"default\":[\"transport\"],\"transport\":[]}}", "tonic-prost-build_0.14.3": "{\"dependencies\":[{\"name\":\"prettyplease\",\"req\":\"^0.2\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0\"},{\"name\":\"prost-build\",\"req\":\"^0.14\"},{\"name\":\"prost-types\",\"req\":\"^0.14\"},{\"name\":\"quote\",\"req\":\"^1.0\"},{\"name\":\"syn\",\"req\":\"^2.0\"},{\"name\":\"tempfile\",\"req\":\"^3.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"tonic\",\"req\":\"^0.14.0\"},{\"default_features\":false,\"name\":\"tonic-build\",\"req\":\"^0.14.0\"}],\"features\":{\"cleanup-markdown\":[\"prost-build/cleanup-markdown\"],\"default\":[\"transport\",\"cleanup-markdown\"],\"transport\":[\"tonic-build/transport\"]}}", "tonic-prost_0.14.3": "{\"dependencies\":[{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"name\":\"prost\",\"req\":\"^0.14\"},{\"features\":[\"macros\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"tonic\",\"req\":\"^0.14.0\"}],\"features\":{}}", + "tonic_0.12.1": "{\"dependencies\":[{\"name\":\"async-stream\",\"optional\":true,\"req\":\"^0.3\"},{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"name\":\"axum\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"hyper-timeout\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.4\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"prost\",\"optional\":true,\"req\":\"^0.13\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"rustls-pemfile\",\"optional\":true,\"req\":\"^2.0\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\",\"ring\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26\"},{\"default_features\":false,\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"tower\",\"optional\":true,\"req\":\"^0.4.7\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.4.7\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^0.26\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13.0\"}],\"features\":{\"channel\":[\"dep:hyper\",\"hyper?/client\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"dep:tower\",\"tower?/balance\",\"tower?/buffer\",\"tower?/discover\",\"tower?/limit\",\"dep:tokio\",\"tokio?/time\",\"dep:hyper-timeout\"],\"codegen\":[\"dep:async-trait\"],\"default\":[\"transport\",\"codegen\",\"prost\"],\"gzip\":[\"dep:flate2\"],\"prost\":[\"dep:prost\"],\"router\":[\"dep:axum\",\"dep:tower\",\"tower?/util\"],\"server\":[\"router\",\"dep:async-stream\",\"dep:h2\",\"dep:hyper\",\"hyper?/server\",\"dep:hyper-util\",\"hyper-util?/service\",\"hyper-util?/server-auto\",\"dep:socket2\",\"dep:tokio\",\"tokio?/macros\",\"tokio?/net\",\"tokio?/time\",\"tokio-stream/net\",\"dep:tower\",\"tower?/util\",\"tower?/limit\"],\"tls\":[\"dep:rustls-pemfile\",\"dep:tokio-rustls\",\"dep:tokio\",\"tokio?/rt\",\"tokio?/macros\"],\"tls-roots\":[\"tls\",\"channel\",\"dep:rustls-native-certs\"],\"tls-webpki-roots\":[\"tls\",\"channel\",\"dep:webpki-roots\"],\"transport\":[\"server\",\"channel\"],\"zstd\":[\"dep:zstd\"]}}", "tonic_0.14.3": "{\"dependencies\":[{\"name\":\"async-trait\",\"optional\":true,\"req\":\"^0.1.13\"},{\"default_features\":false,\"name\":\"axum\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"base64\",\"req\":\"^0.22\"},{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.5\"},{\"name\":\"bytes\",\"req\":\"^1.0\"},{\"name\":\"flate2\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"h2\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"http\",\"req\":\"^1.1.0\"},{\"name\":\"http-body\",\"req\":\"^1\"},{\"name\":\"http-body-util\",\"req\":\"^0.1\"},{\"features\":[\"http1\",\"http2\"],\"name\":\"hyper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"hyper-timeout\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"tokio\"],\"name\":\"hyper-util\",\"optional\":true,\"req\":\"^0.1.11\"},{\"name\":\"percent-encoding\",\"req\":\"^2.1\"},{\"name\":\"pin-project\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0\"},{\"name\":\"rustls-native-certs\",\"optional\":true,\"req\":\"^0.8\"},{\"features\":[\"all\"],\"name\":\"socket2\",\"optional\":true,\"req\":\"^0.6\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.0\"},{\"name\":\"sync_wrapper\",\"req\":\"^1.0.2\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1\"},{\"features\":[\"rt-multi-thread\",\"macros\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.0\"},{\"default_features\":false,\"features\":[\"logging\",\"tls12\"],\"name\":\"tokio-rustls\",\"optional\":true,\"req\":\"^0.26.1\"},{\"default_features\":false,\"name\":\"tokio-stream\",\"req\":\"^0.1.16\"},{\"default_features\":false,\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"load-shed\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"name\":\"tracing\",\"req\":\"^0.1\"},{\"name\":\"webpki-roots\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"zstd\",\"optional\":true,\"req\":\"^0.13.0\"}],\"features\":{\"_tls-any\":[\"dep:tokio\",\"tokio?/rt\",\"tokio?/macros\",\"tls-connect-info\"],\"channel\":[\"dep:hyper\",\"hyper?/client\",\"dep:hyper-util\",\"hyper-util?/client-legacy\",\"dep:tower\",\"tower?/balance\",\"tower?/buffer\",\"tower?/discover\",\"tower?/limit\",\"tower?/load-shed\",\"tower?/util\",\"dep:tokio\",\"tokio?/time\",\"dep:hyper-timeout\"],\"codegen\":[\"dep:async-trait\"],\"default\":[\"router\",\"transport\",\"codegen\"],\"deflate\":[\"dep:flate2\"],\"gzip\":[\"dep:flate2\"],\"router\":[\"dep:axum\",\"dep:tower\",\"tower?/util\"],\"server\":[\"dep:h2\",\"dep:hyper\",\"hyper?/server\",\"dep:hyper-util\",\"hyper-util?/service\",\"hyper-util?/server-auto\",\"dep:socket2\",\"dep:tokio\",\"tokio?/macros\",\"tokio?/net\",\"tokio?/time\",\"tokio-stream/net\",\"dep:tower\",\"tower?/util\",\"tower?/limit\",\"tower?/load-shed\"],\"tls-aws-lc\":[\"_tls-any\",\"tokio-rustls/aws-lc-rs\"],\"tls-connect-info\":[\"dep:tokio-rustls\"],\"tls-native-roots\":[\"_tls-any\",\"channel\",\"dep:rustls-native-certs\"],\"tls-ring\":[\"_tls-any\",\"tokio-rustls/ring\"],\"tls-webpki-roots\":[\"_tls-any\",\"channel\",\"dep:webpki-roots\"],\"transport\":[\"server\",\"channel\"],\"zstd\":[\"dep:zstd\"]}}", "tower-http_0.6.8": "{\"dependencies\":[{\"features\":[\"tokio\"],\"name\":\"async-compression\",\"optional\":true,\"req\":\"^0.4\"},{\"name\":\"base64\",\"optional\":true,\"req\":\"^0.22\"},{\"name\":\"bitflags\",\"req\":\"^2.0.2\"},{\"kind\":\"dev\",\"name\":\"brotli\",\"req\":\"^8\"},{\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"bytes\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"flate2\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.14\"},{\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.14\"},{\"name\":\"http\",\"req\":\"^1.0\"},{\"name\":\"http-body\",\"optional\":true,\"req\":\"^1.0.0\"},{\"kind\":\"dev\",\"name\":\"http-body\",\"req\":\"^1.0.0\"},{\"name\":\"http-body-util\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"http-body-util\",\"req\":\"^0.1.0\"},{\"name\":\"http-range-header\",\"optional\":true,\"req\":\"^0.4.0\"},{\"name\":\"httpdate\",\"optional\":true,\"req\":\"^1.0\"},{\"features\":[\"client-legacy\",\"http1\",\"tokio\"],\"kind\":\"dev\",\"name\":\"hyper-util\",\"req\":\"^0.1\"},{\"name\":\"iri-string\",\"optional\":true,\"req\":\"^0.7.0\"},{\"default_features\":false,\"name\":\"mime\",\"optional\":true,\"req\":\"^0.3.17\"},{\"default_features\":false,\"name\":\"mime_guess\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"^1\"},{\"name\":\"percent-encoding\",\"optional\":true,\"req\":\"^2.1.0\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"sync_wrapper\",\"req\":\"^1\"},{\"default_features\":false,\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6\"},{\"features\":[\"full\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1\"},{\"default_features\":false,\"features\":[\"io\"],\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7\"},{\"name\":\"tower\",\"optional\":true,\"req\":\"^0.5\"},{\"features\":[\"buffer\",\"util\",\"retry\",\"make\",\"timeout\"],\"kind\":\"dev\",\"name\":\"tower\",\"req\":\"^0.5\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"},{\"features\":[\"v4\"],\"name\":\"uuid\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"zstd\",\"req\":\"^0.13\"}],\"features\":{\"add-extension\":[],\"auth\":[\"base64\",\"validate-request\"],\"catch-panic\":[\"tracing\",\"futures-util/std\",\"dep:http-body\",\"dep:http-body-util\"],\"compression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-full\":[\"compression-br\",\"compression-deflate\",\"compression-gzip\",\"compression-zstd\"],\"compression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"compression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"tokio-util\",\"tokio\"],\"cors\":[],\"decompression-br\":[\"async-compression/brotli\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-deflate\":[\"async-compression/zlib\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-full\":[\"decompression-br\",\"decompression-deflate\",\"decompression-gzip\",\"decompression-zstd\"],\"decompression-gzip\":[\"async-compression/gzip\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"decompression-zstd\":[\"async-compression/zstd\",\"futures-core\",\"dep:http-body\",\"dep:http-body-util\",\"tokio-util\",\"tokio\"],\"default\":[],\"follow-redirect\":[\"futures-util\",\"dep:http-body\",\"iri-string\",\"tower/util\"],\"fs\":[\"futures-core\",\"futures-util\",\"dep:http-body\",\"dep:http-body-util\",\"tokio/fs\",\"tokio-util/io\",\"tokio/io-util\",\"dep:http-range-header\",\"mime_guess\",\"mime\",\"percent-encoding\",\"httpdate\",\"set-status\",\"futures-util/alloc\",\"tracing\"],\"full\":[\"add-extension\",\"auth\",\"catch-panic\",\"compression-full\",\"cors\",\"decompression-full\",\"follow-redirect\",\"fs\",\"limit\",\"map-request-body\",\"map-response-body\",\"metrics\",\"normalize-path\",\"propagate-header\",\"redirect\",\"request-id\",\"sensitive-headers\",\"set-header\",\"set-status\",\"timeout\",\"trace\",\"util\",\"validate-request\"],\"limit\":[\"dep:http-body\",\"dep:http-body-util\"],\"map-request-body\":[],\"map-response-body\":[],\"metrics\":[\"dep:http-body\",\"tokio/time\"],\"normalize-path\":[],\"propagate-header\":[],\"redirect\":[],\"request-id\":[\"uuid\"],\"sensitive-headers\":[],\"set-header\":[],\"set-status\":[],\"timeout\":[\"dep:http-body\",\"tokio/time\"],\"trace\":[\"dep:http-body\",\"tracing\"],\"util\":[\"tower\"],\"validate-request\":[\"mime\"]}}", "tower-layer_0.3.3": "{\"dependencies\":[],\"features\":{}}", "tower-service_0.3.3": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.22\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"features\":[\"macros\",\"time\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"tower-layer\",\"req\":\"^0.3\"}],\"features\":{}}", + "tower_0.4.13": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"hdrhistogram\",\"optional\":true,\"req\":\"^7.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"hdrhistogram\",\"req\":\"^7.0\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^0.2\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1.4.0\"},{\"name\":\"pin-project\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"pin-project-lite\",\"req\":\"^0.2.7\"},{\"features\":[\"small_rng\"],\"name\":\"rand\",\"optional\":true,\"req\":\"^0.8\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4\"},{\"features\":[\"sync\"],\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6\"},{\"features\":[\"macros\",\"sync\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.6.2\"},{\"name\":\"tokio-stream\",\"optional\":true,\"req\":\"^0.1.0\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.1\"},{\"name\":\"tower-service\",\"req\":\"^0.3.1\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"fmt\",\"ansi\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"__common\":[\"futures-core\",\"pin-project-lite\"],\"balance\":[\"discover\",\"load\",\"ready-cache\",\"make\",\"rand\",\"slab\"],\"buffer\":[\"__common\",\"tokio/sync\",\"tokio/rt\",\"tokio-util\",\"tracing\"],\"default\":[\"log\"],\"discover\":[\"__common\"],\"filter\":[\"__common\",\"futures-util\"],\"full\":[\"balance\",\"buffer\",\"discover\",\"filter\",\"hedge\",\"limit\",\"load\",\"load-shed\",\"make\",\"ready-cache\",\"reconnect\",\"retry\",\"spawn-ready\",\"steer\",\"timeout\",\"util\"],\"hedge\":[\"util\",\"filter\",\"futures-util\",\"hdrhistogram\",\"tokio/time\",\"tracing\"],\"limit\":[\"__common\",\"tokio/time\",\"tokio/sync\",\"tokio-util\",\"tracing\"],\"load\":[\"__common\",\"tokio/time\",\"tracing\"],\"load-shed\":[\"__common\"],\"log\":[\"tracing/log\"],\"make\":[\"futures-util\",\"pin-project-lite\",\"tokio/io-std\"],\"ready-cache\":[\"futures-core\",\"futures-util\",\"indexmap\",\"tokio/sync\",\"tracing\",\"pin-project-lite\"],\"reconnect\":[\"make\",\"tokio/io-std\",\"tracing\"],\"retry\":[\"__common\",\"tokio/time\"],\"spawn-ready\":[\"__common\",\"futures-util\",\"tokio/sync\",\"tokio/rt\",\"util\",\"tracing\"],\"steer\":[],\"timeout\":[\"pin-project-lite\",\"tokio/time\"],\"util\":[\"__common\",\"futures-util\",\"pin-project\"]}}", "tower_0.5.3": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"futures\",\"req\":\"^0.3.22\"},{\"name\":\"futures-core\",\"optional\":true,\"req\":\"^0.3.22\"},{\"default_features\":false,\"features\":[\"alloc\"],\"name\":\"futures-util\",\"optional\":true,\"req\":\"^0.3.22\"},{\"default_features\":false,\"features\":[\"async-await-macro\"],\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.22\"},{\"default_features\":false,\"name\":\"hdrhistogram\",\"optional\":true,\"req\":\"^7.0\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"hdrhistogram\",\"req\":\"^7.0\"},{\"kind\":\"dev\",\"name\":\"http\",\"req\":\"^1\"},{\"name\":\"indexmap\",\"optional\":true,\"req\":\"^2.0.2\"},{\"name\":\"pin-project-lite\",\"optional\":true,\"req\":\"^0.2.7\"},{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"name\":\"slab\",\"optional\":true,\"req\":\"^0.4.9\"},{\"name\":\"sync_wrapper\",\"optional\":true,\"req\":\"^1\"},{\"name\":\"tokio\",\"optional\":true,\"req\":\"^1.6.2\"},{\"features\":[\"macros\",\"sync\",\"test-util\",\"rt-multi-thread\"],\"kind\":\"dev\",\"name\":\"tokio\",\"req\":\"^1.6.2\"},{\"kind\":\"dev\",\"name\":\"tokio-stream\",\"req\":\"^0.1.1\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"name\":\"tokio-util\",\"optional\":true,\"req\":\"^0.7.0\"},{\"name\":\"tower-layer\",\"req\":\"^0.3.3\"},{\"name\":\"tower-service\",\"req\":\"^0.3.3\"},{\"kind\":\"dev\",\"name\":\"tower-test\",\"req\":\"^0.4\"},{\"default_features\":false,\"features\":[\"std\"],\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"std\"],\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.2\"},{\"default_features\":false,\"features\":[\"fmt\",\"ansi\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3\"}],\"features\":{\"balance\":[\"discover\",\"load\",\"ready-cache\",\"make\",\"slab\",\"util\"],\"buffer\":[\"tokio/sync\",\"tokio/rt\",\"tokio-util\",\"tracing\",\"pin-project-lite\"],\"discover\":[\"futures-core\",\"pin-project-lite\"],\"filter\":[\"futures-util\",\"pin-project-lite\"],\"full\":[\"balance\",\"buffer\",\"discover\",\"filter\",\"hedge\",\"limit\",\"load\",\"load-shed\",\"make\",\"ready-cache\",\"reconnect\",\"retry\",\"spawn-ready\",\"steer\",\"timeout\",\"util\"],\"hedge\":[\"util\",\"filter\",\"futures-util\",\"hdrhistogram\",\"tokio/time\",\"tracing\"],\"limit\":[\"tokio/time\",\"tokio/sync\",\"tokio-util\",\"tracing\",\"pin-project-lite\"],\"load\":[\"tokio/time\",\"tracing\",\"pin-project-lite\"],\"load-shed\":[\"pin-project-lite\"],\"log\":[\"tracing/log\"],\"make\":[\"pin-project-lite\",\"tokio\"],\"ready-cache\":[\"futures-core\",\"futures-util\",\"indexmap\",\"tokio/sync\",\"tracing\",\"pin-project-lite\"],\"reconnect\":[\"make\",\"tracing\"],\"retry\":[\"tokio/time\",\"util\"],\"spawn-ready\":[\"futures-util\",\"tokio/sync\",\"tokio/rt\",\"util\",\"tracing\"],\"steer\":[],\"timeout\":[\"pin-project-lite\",\"tokio/time\"],\"tokio-stream\":[],\"util\":[\"futures-core\",\"futures-util\",\"pin-project-lite\",\"sync_wrapper\"]}}", "tracing-appender_0.2.4": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.6\"},{\"name\":\"crossbeam-channel\",\"req\":\"^0.5.6\"},{\"name\":\"parking_lot\",\"optional\":true,\"req\":\"^0.12.1\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"},{\"name\":\"thiserror\",\"req\":\"^2\"},{\"default_features\":false,\"features\":[\"formatting\",\"parsing\"],\"name\":\"time\",\"req\":\"^0.3.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"default_features\":false,\"features\":[\"fmt\",\"std\"],\"name\":\"tracing-subscriber\",\"req\":\"^0.3.18\"}],\"features\":{}}", "tracing-attributes_0.1.31": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-trait\",\"req\":\"^0.1.67\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.60\"},{\"name\":\"quote\",\"req\":\"^1.0.20\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.9\"},{\"default_features\":false,\"features\":[\"full\",\"parsing\",\"printing\",\"visit-mut\",\"clone-impls\",\"extra-traits\",\"proc-macro\"],\"name\":\"syn\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"tokio-test\",\"req\":\"^0.4.2\"},{\"kind\":\"dev\",\"name\":\"tracing\",\"req\":\"^0.1.35\"},{\"features\":[\"env-filter\"],\"kind\":\"dev\",\"name\":\"tracing-subscriber\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.64\"}],\"features\":{\"async-await\":[]}}", @@ -1662,7 +1710,9 @@ "zbus_macros_4.4.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"async-io\",\"req\":\"^2.3.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"futures-util\",\"req\":\"^0.3.30\"},{\"name\":\"proc-macro-crate\",\"req\":\"^3.1.0\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.81\"},{\"name\":\"quote\",\"req\":\"^1.0.36\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0.15\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.200\"},{\"features\":[\"extra-traits\",\"fold\",\"full\"],\"name\":\"syn\",\"req\":\"^2.0.64\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.93\"},{\"name\":\"zvariant_utils\",\"req\":\"=2.1.0\"}],\"features\":{}}", "zbus_names_3.0.0": "{\"dependencies\":[{\"features\":[\"derive\"],\"name\":\"serde\",\"req\":\"^1.0\"},{\"name\":\"static_assertions\",\"req\":\"^1.1.0\"},{\"default_features\":false,\"features\":[\"enumflags2\"],\"name\":\"zvariant\",\"req\":\"^4.0.0\"}],\"features\":{}}", "zerocopy-derive_0.8.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"dissimilar\",\"req\":\"^1.0.9\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"=0.2.17\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.1\"},{\"kind\":\"dev\",\"name\":\"proc-macro2\",\"req\":\"=1.0.80\"},{\"name\":\"quote\",\"req\":\"^1.0.40\"},{\"kind\":\"dev\",\"name\":\"quote\",\"req\":\"=1.0.40\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"visit\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"}],\"features\":{}}", + "zerocopy-derive_0.8.42": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"dissimilar\",\"req\":\"^1.0.9\"},{\"kind\":\"dev\",\"name\":\"prettyplease\",\"req\":\"=0.2.17\"},{\"name\":\"proc-macro2\",\"req\":\"^1.0.1\"},{\"name\":\"quote\",\"req\":\"^1.0.40\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"full\"],\"name\":\"syn\",\"req\":\"^2.0.46\"},{\"features\":[\"visit\"],\"kind\":\"dev\",\"name\":\"syn\",\"req\":\"^2.0.46\"}],\"features\":{}}", "zerocopy_0.8.37": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"elain\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"features\":[\"diff\"],\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"=1.0.89\"},{\"name\":\"zerocopy-derive\",\"req\":\"=0.8.37\",\"target\":\"cfg(any())\"},{\"name\":\"zerocopy-derive\",\"optional\":true,\"req\":\"=0.8.37\"},{\"kind\":\"dev\",\"name\":\"zerocopy-derive\",\"req\":\"=0.8.37\"}],\"features\":{\"__internal_use_only_features_that_work_on_stable\":[\"alloc\",\"derive\",\"simd\",\"std\"],\"alloc\":[],\"derive\":[\"zerocopy-derive\"],\"float-nightly\":[],\"simd\":[],\"simd-nightly\":[\"simd\"],\"std\":[\"alloc\"]}}", + "zerocopy_0.8.42": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"elain\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.11\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.5\"},{\"kind\":\"dev\",\"name\":\"rustversion\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"static_assertions\",\"req\":\"^1.1\"},{\"name\":\"zerocopy-derive\",\"req\":\"=0.8.42\",\"target\":\"cfg(any())\"},{\"name\":\"zerocopy-derive\",\"optional\":true,\"req\":\"=0.8.42\"},{\"kind\":\"dev\",\"name\":\"zerocopy-derive\",\"req\":\"=0.8.42\"}],\"features\":{\"__internal_use_only_features_that_work_on_stable\":[\"alloc\",\"derive\",\"simd\",\"std\"],\"alloc\":[],\"derive\":[\"zerocopy-derive\"],\"float-nightly\":[],\"simd\":[],\"simd-nightly\":[\"simd\"],\"std\":[\"alloc\"]}}", "zerofrom-derive_0.1.6": "{\"dependencies\":[{\"name\":\"proc-macro2\",\"req\":\"^1.0.61\"},{\"name\":\"quote\",\"req\":\"^1.0.28\"},{\"features\":[\"fold\"],\"name\":\"syn\",\"req\":\"^2.0.21\"},{\"name\":\"synstructure\",\"req\":\"^0.13.0\"}],\"features\":{}}", "zerofrom_0.1.6": "{\"dependencies\":[{\"default_features\":false,\"name\":\"zerofrom-derive\",\"optional\":true,\"req\":\"^0.1.3\"}],\"features\":{\"alloc\":[],\"default\":[\"alloc\"],\"derive\":[\"dep:zerofrom-derive\"]}}", "zeroize_1.8.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"name\":\"zeroize_derive\",\"optional\":true,\"req\":\"^1.3\"}],\"features\":{\"aarch64\":[],\"alloc\":[],\"default\":[\"alloc\"],\"derive\":[\"zeroize_derive\"],\"simd\":[],\"std\":[\"alloc\"]}}", @@ -1706,6 +1756,15 @@ "clippy-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "b6f1f7264ed6943c59dedfb9531fbadcc3c0fcf273c940a63d58898b14a1060f", "clippy-1.93.0-x86_64-pc-windows-msvc.tar.xz": "25fb103390bf392980b4689ac09b2ec2ab4beefb7022a983215b613ad05eab57", "clippy-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "793108977514b15c0f45ade28ae35c58b05370cb0f22e89bd98fdfa61eabf55d", + "rust-analyzer-1.93.0-aarch64-apple-darwin.tar.xz": "8a09a46d45277678b2d112eef61736e03e78dfa6c506e187df176b904659e5a7", + "rust-analyzer-1.93.0-aarch64-pc-windows-gnullvm.tar.xz": "1fb48fadbaea6da36cfcd6b72122eeaa6ab025c9c82441777cf913108f73c115", + "rust-analyzer-1.93.0-aarch64-pc-windows-msvc.tar.xz": "558100ab62fe22d552be43c1bf166fef057ff2f3eb0d786cf5fe509a08701f37", + "rust-analyzer-1.93.0-aarch64-unknown-linux-gnu.tar.xz": "b2e7890cf5953f572eb05d7328c086fa60d4f16e5d301d35c52b3f58b88a8316", + "rust-analyzer-1.93.0-x86_64-apple-darwin.tar.xz": "3cb7975d4fd17840e2980c9d864755f801ee9e7594de886a5816c73b07594508", + "rust-analyzer-1.93.0-x86_64-pc-windows-gnullvm.tar.xz": "4471db6393b7380624899482aae99612fd1cf509ae36d0c68292077c2104127d", + "rust-analyzer-1.93.0-x86_64-pc-windows-msvc.tar.xz": "f4c1f8f120f48974cb1e0cea01a955ce52cdfa0f1db0355c5fb5a2deeca8188a", + "rust-analyzer-1.93.0-x86_64-unknown-linux-gnu.tar.xz": "33fcd377be3b5ffdd95977c3d0219f63725c18b6c8b53fb5be0418962a84738c", + "rust-src-1.93.0.tar.xz": "0e7b9acd5debfeffef3741dc8f6edf137d70426a0027d6b190cc8cfb0a1ac23c", "rust-std-1.93.0-aarch64-apple-darwin.tar.xz": "8603c63715349636ed85b4fe716c4e827a727918c840e54aff5b243cedadf19b", "rust-std-1.93.0-aarch64-apple-ios-macabi.tar.xz": "24d47e615ce101869ff452a572a6b77ed7cf70f2454d0b50892ac849e8c7ac4d", "rust-std-1.93.0-aarch64-apple-ios-sim.tar.xz": "d1d5e2d1b79206f2cc9fb7f6a2958cfe0f4bbc9147fda8dbc3608aa4be5e6816", diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 4c22d8d732..e0c399a3b2 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2072,6 +2072,7 @@ dependencies = [ "codex-protocol", "codex-skills", "codex-utils-absolute-path", + "codex-utils-output-truncation", "codex-utils-plugins", "dirs", "dunce", @@ -2413,6 +2414,7 @@ dependencies = [ "async-channel", "codex-async-utils", "codex-config", + "codex-exec-server", "codex-login", "codex-otel", "codex-plugin", @@ -2714,6 +2716,7 @@ dependencies = [ "axum", "codex-client", "codex-config", + "codex-exec-server", "codex-keyring-store", "codex-protocol", "codex-utils-cargo-bin", diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index ac9f480a8c..5aae5fd1d9 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -5,6 +5,13 @@ "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", "type": "string" }, + "AddCreditsNudgeCreditType": { + "enum": [ + "credits", + "usage_limit" + ], + "type": "string" + }, "ApprovalsReviewer": { "description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `guardian_subagent` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request.", "enum": [ @@ -464,6 +471,16 @@ }, { "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, "image_url": { "type": "string" }, @@ -1437,19 +1454,27 @@ }, "PluginInstallParams": { "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local install flow.", - "type": "boolean" - }, "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "type": "object" @@ -1465,10 +1490,6 @@ "array", "null" ] - }, - "forceRemoteSync": { - "description": "When true, reconcile the official curated marketplace against the remote plugin state before listing marketplaces.", - "type": "boolean" } }, "type": "object" @@ -1476,24 +1497,32 @@ "PluginReadParams": { "properties": { "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "type": "object" }, "PluginUninstallParams": { "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local uninstall flow.", - "type": "boolean" - }, "pluginId": { "type": "string" } @@ -2536,6 +2565,17 @@ } ] }, + "SendAddCreditsNudgeEmailParams": { + "properties": { + "creditType": { + "$ref": "#/definitions/AddCreditsNudgeCreditType" + } + }, + "required": [ + "creditType" + ], + "type": "object" + }, "ServiceTier": { "enum": [ "fast", @@ -4970,6 +5010,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/sendAddCreditsNudgeEmail" + ], + "title": "Account/sendAddCreditsNudgeEmailRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SendAddCreditsNudgeEmailParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/sendAddCreditsNudgeEmailRequest", + "type": "object" + }, { "properties": { "id": { diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 5695877afa..b0eca67855 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -3990,6 +3990,25 @@ } ] }, + "WarningNotification": { + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "message" + ], + "type": "object" + }, "WebSearchAction": { "oneOf": [ { @@ -4930,6 +4949,26 @@ "title": "Model/reroutedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, { "properties": { "method": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index fb393c1145..bf8dbb166d 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -1440,6 +1440,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/v2/RequestId" + }, + "method": { + "enum": [ + "account/sendAddCreditsNudgeEmail" + ], + "title": "Account/sendAddCreditsNudgeEmailRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/SendAddCreditsNudgeEmailParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/sendAddCreditsNudgeEmailRequest", + "type": "object" + }, { "properties": { "id": { @@ -4303,6 +4327,26 @@ "title": "Model/reroutedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/v2/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, { "properties": { "method": { @@ -5083,6 +5127,20 @@ "title": "AccountUpdatedNotification", "type": "object" }, + "AddCreditsNudgeCreditType": { + "enum": [ + "credits", + "usage_limit" + ], + "type": "string" + }, + "AddCreditsNudgeEmailStatus": { + "enum": [ + "sent", + "cooldown_active" + ], + "type": "string" + }, "AgentMessageDeltaNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -7115,6 +7173,16 @@ }, { "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/v2/ImageDetail" + }, + { + "type": "null" + } + ] + }, "image_url": { "type": "string" }, @@ -10208,19 +10276,27 @@ "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local install flow.", - "type": "boolean" - }, "marketplacePath": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginInstallParams", @@ -10282,6 +10358,14 @@ { "type": "null" } + ], + "description": "Local composer icon path, resolved from the installed plugin package." + }, + "composerIconUrl": { + "description": "Remote composer icon URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "defaultPrompt": { @@ -10314,6 +10398,14 @@ { "type": "null" } + ], + "description": "Local logo path, resolved from the installed plugin package." + }, + "logoUrl": { + "description": "Remote logo URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "longDescription": { @@ -10328,7 +10420,15 @@ "null" ] }, + "screenshotUrls": { + "description": "Remote screenshot URLs from the plugin catalog.", + "items": { + "type": "string" + }, + "type": "array" + }, "screenshots": { + "description": "Local screenshot paths, resolved from the installed plugin package.", "items": { "$ref": "#/definitions/v2/AbsolutePathBuf" }, @@ -10355,6 +10455,7 @@ }, "required": [ "capabilities", + "screenshotUrls", "screenshots" ], "type": "object" @@ -10371,10 +10472,6 @@ "array", "null" ] - }, - "forceRemoteSync": { - "description": "When true, reconcile the official curated marketplace against the remote plugin state before listing marketplaces.", - "type": "boolean" } }, "title": "PluginListParams", @@ -10402,12 +10499,6 @@ "$ref": "#/definitions/v2/PluginMarketplaceEntry" }, "type": "array" - }, - "remoteSyncError": { - "type": [ - "string", - "null" - ] } }, "required": [ @@ -10432,7 +10523,15 @@ "type": "string" }, "path": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." }, "plugins": { "items": { @@ -10443,7 +10542,6 @@ }, "required": [ "name", - "path", "plugins" ], "type": "object" @@ -10452,14 +10550,26 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "marketplacePath": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginReadParams", @@ -10537,6 +10647,23 @@ ], "title": "GitPluginSource", "type": "object" + }, + { + "description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.", + "properties": { + "type": { + "enum": [ + "remote" + ], + "title": "RemotePluginSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RemotePluginSource", + "type": "object" } ] }, @@ -10588,10 +10715,6 @@ "PluginUninstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local uninstall flow.", - "type": "boolean" - }, "pluginId": { "type": "string" } @@ -12192,6 +12315,32 @@ }, "type": "object" }, + "SendAddCreditsNudgeEmailParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "creditType": { + "$ref": "#/definitions/v2/AddCreditsNudgeCreditType" + } + }, + "required": [ + "creditType" + ], + "title": "SendAddCreditsNudgeEmailParams", + "type": "object" + }, + "SendAddCreditsNudgeEmailResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/v2/AddCreditsNudgeEmailStatus" + } + }, + "required": [ + "status" + ], + "title": "SendAddCreditsNudgeEmailResponse", + "type": "object" + }, "ServerRequestResolvedNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -15775,6 +15924,27 @@ ], "type": "string" }, + "WarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "message" + ], + "title": "WarningNotification", + "type": "object" + }, "WebSearchAction": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 458367e205..59b830d4f2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -114,6 +114,20 @@ "title": "AccountUpdatedNotification", "type": "object" }, + "AddCreditsNudgeCreditType": { + "enum": [ + "credits", + "usage_limit" + ], + "type": "string" + }, + "AddCreditsNudgeEmailStatus": { + "enum": [ + "sent", + "cooldown_active" + ], + "type": "string" + }, "AgentMessageDeltaNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { @@ -2022,6 +2036,30 @@ "title": "Account/rateLimits/readRequest", "type": "object" }, + { + "properties": { + "id": { + "$ref": "#/definitions/RequestId" + }, + "method": { + "enum": [ + "account/sendAddCreditsNudgeEmail" + ], + "title": "Account/sendAddCreditsNudgeEmailRequestMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/SendAddCreditsNudgeEmailParams" + } + }, + "required": [ + "id", + "method", + "params" + ], + "title": "Account/sendAddCreditsNudgeEmailRequest", + "type": "object" + }, { "properties": { "id": { @@ -3712,6 +3750,16 @@ }, { "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, "image_url": { "type": "string" }, @@ -6960,19 +7008,27 @@ "PluginInstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local install flow.", - "type": "boolean" - }, "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginInstallParams", @@ -7034,6 +7090,14 @@ { "type": "null" } + ], + "description": "Local composer icon path, resolved from the installed plugin package." + }, + "composerIconUrl": { + "description": "Remote composer icon URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "defaultPrompt": { @@ -7066,6 +7130,14 @@ { "type": "null" } + ], + "description": "Local logo path, resolved from the installed plugin package." + }, + "logoUrl": { + "description": "Remote logo URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "longDescription": { @@ -7080,7 +7152,15 @@ "null" ] }, + "screenshotUrls": { + "description": "Remote screenshot URLs from the plugin catalog.", + "items": { + "type": "string" + }, + "type": "array" + }, "screenshots": { + "description": "Local screenshot paths, resolved from the installed plugin package.", "items": { "$ref": "#/definitions/AbsolutePathBuf" }, @@ -7107,6 +7187,7 @@ }, "required": [ "capabilities", + "screenshotUrls", "screenshots" ], "type": "object" @@ -7123,10 +7204,6 @@ "array", "null" ] - }, - "forceRemoteSync": { - "description": "When true, reconcile the official curated marketplace against the remote plugin state before listing marketplaces.", - "type": "boolean" } }, "title": "PluginListParams", @@ -7154,12 +7231,6 @@ "$ref": "#/definitions/PluginMarketplaceEntry" }, "type": "array" - }, - "remoteSyncError": { - "type": [ - "string", - "null" - ] } }, "required": [ @@ -7184,7 +7255,15 @@ "type": "string" }, "path": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." }, "plugins": { "items": { @@ -7195,7 +7274,6 @@ }, "required": [ "name", - "path", "plugins" ], "type": "object" @@ -7204,14 +7282,26 @@ "$schema": "http://json-schema.org/draft-07/schema#", "properties": { "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginReadParams", @@ -7289,6 +7379,23 @@ ], "title": "GitPluginSource", "type": "object" + }, + { + "description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.", + "properties": { + "type": { + "enum": [ + "remote" + ], + "title": "RemotePluginSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RemotePluginSource", + "type": "object" } ] }, @@ -7340,10 +7447,6 @@ "PluginUninstallParams": { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local uninstall flow.", - "type": "boolean" - }, "pluginId": { "type": "string" } @@ -8944,6 +9047,32 @@ }, "type": "object" }, + "SendAddCreditsNudgeEmailParams": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "creditType": { + "$ref": "#/definitions/AddCreditsNudgeCreditType" + } + }, + "required": [ + "creditType" + ], + "title": "SendAddCreditsNudgeEmailParams", + "type": "object" + }, + "SendAddCreditsNudgeEmailResponse": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "status": { + "$ref": "#/definitions/AddCreditsNudgeEmailStatus" + } + }, + "required": [ + "status" + ], + "title": "SendAddCreditsNudgeEmailResponse", + "type": "object" + }, "ServerNotification": { "$schema": "http://json-schema.org/draft-07/schema#", "description": "Notification sent from the server to the client.", @@ -9732,6 +9861,26 @@ "title": "Model/reroutedNotification", "type": "object" }, + { + "properties": { + "method": { + "enum": [ + "warning" + ], + "title": "WarningNotificationMethod", + "type": "string" + }, + "params": { + "$ref": "#/definitions/WarningNotification" + } + }, + "required": [ + "method", + "params" + ], + "title": "WarningNotification", + "type": "object" + }, { "properties": { "method": { @@ -13619,6 +13768,27 @@ ], "type": "string" }, + "WarningNotification": { + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "message" + ], + "title": "WarningNotification", + "type": "object" + }, "WebSearchAction": { "oneOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json index 6890705311..ad3c0c1079 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginInstallParams.json @@ -7,19 +7,27 @@ } }, "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local install flow.", - "type": "boolean" - }, "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginInstallParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json index 669ff92b9e..27ea8c4df3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListParams.json @@ -16,10 +16,6 @@ "array", "null" ] - }, - "forceRemoteSync": { - "description": "When true, reconcile the official curated marketplace against the remote plugin state before listing marketplaces.", - "type": "boolean" } }, "title": "PluginListParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json index ee039060e6..72c941c45f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginListResponse.json @@ -74,6 +74,14 @@ { "type": "null" } + ], + "description": "Local composer icon path, resolved from the installed plugin package." + }, + "composerIconUrl": { + "description": "Remote composer icon URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "defaultPrompt": { @@ -106,6 +114,14 @@ { "type": "null" } + ], + "description": "Local logo path, resolved from the installed plugin package." + }, + "logoUrl": { + "description": "Remote logo URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "longDescription": { @@ -120,7 +136,15 @@ "null" ] }, + "screenshotUrls": { + "description": "Remote screenshot URLs from the plugin catalog.", + "items": { + "type": "string" + }, + "type": "array" + }, "screenshots": { + "description": "Local screenshot paths, resolved from the installed plugin package.", "items": { "$ref": "#/definitions/AbsolutePathBuf" }, @@ -147,6 +171,7 @@ }, "required": [ "capabilities", + "screenshotUrls", "screenshots" ], "type": "object" @@ -167,7 +192,15 @@ "type": "string" }, "path": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ], + "description": "Local marketplace file path when the marketplace is backed by a local file. Remote-only catalog marketplaces do not have a local path." }, "plugins": { "items": { @@ -178,7 +211,6 @@ }, "required": [ "name", - "path", "plugins" ], "type": "object" @@ -242,6 +274,23 @@ ], "title": "GitPluginSource", "type": "object" + }, + { + "description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.", + "properties": { + "type": { + "enum": [ + "remote" + ], + "title": "RemotePluginSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RemotePluginSource", + "type": "object" } ] }, @@ -311,12 +360,6 @@ "$ref": "#/definitions/PluginMarketplaceEntry" }, "type": "array" - }, - "remoteSyncError": { - "type": [ - "string", - "null" - ] } }, "required": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json index a720ae3b59..5cc3e5cab5 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadParams.json @@ -8,14 +8,26 @@ }, "properties": { "marketplacePath": { - "$ref": "#/definitions/AbsolutePathBuf" + "anyOf": [ + { + "$ref": "#/definitions/AbsolutePathBuf" + }, + { + "type": "null" + } + ] }, "pluginName": { "type": "string" + }, + "remoteMarketplaceName": { + "type": [ + "string", + "null" + ] } }, "required": [ - "marketplacePath", "pluginName" ], "title": "PluginReadParams", diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json index 43628e28f0..5ec07f00f1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginReadResponse.json @@ -126,6 +126,14 @@ { "type": "null" } + ], + "description": "Local composer icon path, resolved from the installed plugin package." + }, + "composerIconUrl": { + "description": "Remote composer icon URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "defaultPrompt": { @@ -158,6 +166,14 @@ { "type": "null" } + ], + "description": "Local logo path, resolved from the installed plugin package." + }, + "logoUrl": { + "description": "Remote logo URL from the plugin catalog.", + "type": [ + "string", + "null" ] }, "longDescription": { @@ -172,7 +188,15 @@ "null" ] }, + "screenshotUrls": { + "description": "Remote screenshot URLs from the plugin catalog.", + "items": { + "type": "string" + }, + "type": "array" + }, "screenshots": { + "description": "Local screenshot paths, resolved from the installed plugin package.", "items": { "$ref": "#/definitions/AbsolutePathBuf" }, @@ -199,6 +223,7 @@ }, "required": [ "capabilities", + "screenshotUrls", "screenshots" ], "type": "object" @@ -262,6 +287,23 @@ ], "title": "GitPluginSource", "type": "object" + }, + { + "description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.", + "properties": { + "type": { + "enum": [ + "remote" + ], + "title": "RemotePluginSourceType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RemotePluginSource", + "type": "object" } ] }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/PluginUninstallParams.json b/codex-rs/app-server-protocol/schema/json/v2/PluginUninstallParams.json index a6d7ec78bb..5b7e0a592f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/PluginUninstallParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/PluginUninstallParams.json @@ -1,10 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "properties": { - "forceRemoteSync": { - "description": "When true, apply the remote plugin change before the local uninstall flow.", - "type": "boolean" - }, "pluginId": { "type": "string" } diff --git a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json index 2b0c66da42..956e3b2507 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/RawResponseItemCompletedNotification.json @@ -25,6 +25,16 @@ }, { "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, "image_url": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailParams.json b/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailParams.json new file mode 100644 index 0000000000..c3c63edef0 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailParams.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AddCreditsNudgeCreditType": { + "enum": [ + "credits", + "usage_limit" + ], + "type": "string" + } + }, + "properties": { + "creditType": { + "$ref": "#/definitions/AddCreditsNudgeCreditType" + } + }, + "required": [ + "creditType" + ], + "title": "SendAddCreditsNudgeEmailParams", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailResponse.json b/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailResponse.json new file mode 100644 index 0000000000..bfeba322b2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/SendAddCreditsNudgeEmailResponse.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "AddCreditsNudgeEmailStatus": { + "enum": [ + "sent", + "cooldown_active" + ], + "type": "string" + } + }, + "properties": { + "status": { + "$ref": "#/definitions/AddCreditsNudgeEmailStatus" + } + }, + "required": [ + "status" + ], + "title": "SendAddCreditsNudgeEmailResponse", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 3c8eb552ae..35a9e5b2a8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -83,6 +83,16 @@ }, { "properties": { + "detail": { + "anyOf": [ + { + "$ref": "#/definitions/ImageDetail" + }, + { + "type": "null" + } + ] + }, "image_url": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json b/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json new file mode 100644 index 0000000000..460486896d --- /dev/null +++ b/codex-rs/app-server-protocol/schema/json/v2/WarningNotification.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "message": { + "description": "Concise warning message for the user.", + "type": "string" + }, + "threadId": { + "description": "Optional thread target when the warning applies to a specific thread.", + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "message" + ], + "title": "WarningNotification", + "type": "object" +} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index d21f951458..b9ef6401b7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -43,6 +43,7 @@ import type { PluginListParams } from "./v2/PluginListParams"; import type { PluginReadParams } from "./v2/PluginReadParams"; import type { PluginUninstallParams } from "./v2/PluginUninstallParams"; import type { ReviewStartParams } from "./v2/ReviewStartParams"; +import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams"; import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams"; import type { SkillsListParams } from "./v2/SkillsListParams"; import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams"; @@ -69,4 +70,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta /** * Request from the client to the server. */ -export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; +export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ContentItem.ts b/codex-rs/app-server-protocol/schema/typescript/ContentItem.ts index c89b9d78a4..21cd8d02f3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ContentItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ContentItem.ts @@ -1,5 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ImageDetail } from "./ImageDetail"; -export type ContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, } | { "type": "output_text", text: string, }; +export type ContentItem = { "type": "input_text", text: string, } | { "type": "input_image", image_url: string, detail?: ImageDetail, } | { "type": "output_text", text: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts index f6df421dcf..a43bb80428 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ServerNotification.ts @@ -54,10 +54,11 @@ import type { TurnCompletedNotification } from "./v2/TurnCompletedNotification"; import type { TurnDiffUpdatedNotification } from "./v2/TurnDiffUpdatedNotification"; import type { TurnPlanUpdatedNotification } from "./v2/TurnPlanUpdatedNotification"; import type { TurnStartedNotification } from "./v2/TurnStartedNotification"; +import type { WarningNotification } from "./v2/WarningNotification"; import type { WindowsSandboxSetupCompletedNotification } from "./v2/WindowsSandboxSetupCompletedNotification"; import type { WindowsWorldWritableWarningNotification } from "./v2/WindowsWorldWritableWarningNotification"; /** * Notification sent from the server to the client. */ -export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; +export type ServerNotification = { "method": "error", "params": ErrorNotification } | { "method": "thread/started", "params": ThreadStartedNotification } | { "method": "thread/status/changed", "params": ThreadStatusChangedNotification } | { "method": "thread/archived", "params": ThreadArchivedNotification } | { "method": "thread/unarchived", "params": ThreadUnarchivedNotification } | { "method": "thread/closed", "params": ThreadClosedNotification } | { "method": "skills/changed", "params": SkillsChangedNotification } | { "method": "thread/name/updated", "params": ThreadNameUpdatedNotification } | { "method": "thread/tokenUsage/updated", "params": ThreadTokenUsageUpdatedNotification } | { "method": "turn/started", "params": TurnStartedNotification } | { "method": "hook/started", "params": HookStartedNotification } | { "method": "turn/completed", "params": TurnCompletedNotification } | { "method": "hook/completed", "params": HookCompletedNotification } | { "method": "turn/diff/updated", "params": TurnDiffUpdatedNotification } | { "method": "turn/plan/updated", "params": TurnPlanUpdatedNotification } | { "method": "item/started", "params": ItemStartedNotification } | { "method": "item/autoApprovalReview/started", "params": ItemGuardianApprovalReviewStartedNotification } | { "method": "item/autoApprovalReview/completed", "params": ItemGuardianApprovalReviewCompletedNotification } | { "method": "item/completed", "params": ItemCompletedNotification } | { "method": "rawResponseItem/completed", "params": RawResponseItemCompletedNotification } | { "method": "item/agentMessage/delta", "params": AgentMessageDeltaNotification } | { "method": "item/plan/delta", "params": PlanDeltaNotification } | { "method": "command/exec/outputDelta", "params": CommandExecOutputDeltaNotification } | { "method": "item/commandExecution/outputDelta", "params": CommandExecutionOutputDeltaNotification } | { "method": "item/commandExecution/terminalInteraction", "params": TerminalInteractionNotification } | { "method": "item/fileChange/outputDelta", "params": FileChangeOutputDeltaNotification } | { "method": "serverRequest/resolved", "params": ServerRequestResolvedNotification } | { "method": "item/mcpToolCall/progress", "params": McpToolCallProgressNotification } | { "method": "mcpServer/oauthLogin/completed", "params": McpServerOauthLoginCompletedNotification } | { "method": "mcpServer/startupStatus/updated", "params": McpServerStatusUpdatedNotification } | { "method": "account/updated", "params": AccountUpdatedNotification } | { "method": "account/rateLimits/updated", "params": AccountRateLimitsUpdatedNotification } | { "method": "app/list/updated", "params": AppListUpdatedNotification } | { "method": "externalAgentConfig/import/completed", "params": ExternalAgentConfigImportCompletedNotification } | { "method": "fs/changed", "params": FsChangedNotification } | { "method": "item/reasoning/summaryTextDelta", "params": ReasoningSummaryTextDeltaNotification } | { "method": "item/reasoning/summaryPartAdded", "params": ReasoningSummaryPartAddedNotification } | { "method": "item/reasoning/textDelta", "params": ReasoningTextDeltaNotification } | { "method": "thread/compacted", "params": ContextCompactedNotification } | { "method": "model/rerouted", "params": ModelReroutedNotification } | { "method": "warning", "params": WarningNotification } | { "method": "deprecationNotice", "params": DeprecationNoticeNotification } | { "method": "configWarning", "params": ConfigWarningNotification } | { "method": "fuzzyFileSearch/sessionUpdated", "params": FuzzyFileSearchSessionUpdatedNotification } | { "method": "fuzzyFileSearch/sessionCompleted", "params": FuzzyFileSearchSessionCompletedNotification } | { "method": "thread/realtime/started", "params": ThreadRealtimeStartedNotification } | { "method": "thread/realtime/itemAdded", "params": ThreadRealtimeItemAddedNotification } | { "method": "thread/realtime/transcript/delta", "params": ThreadRealtimeTranscriptDeltaNotification } | { "method": "thread/realtime/transcript/done", "params": ThreadRealtimeTranscriptDoneNotification } | { "method": "thread/realtime/outputAudio/delta", "params": ThreadRealtimeOutputAudioDeltaNotification } | { "method": "thread/realtime/sdp", "params": ThreadRealtimeSdpNotification } | { "method": "thread/realtime/error", "params": ThreadRealtimeErrorNotification } | { "method": "thread/realtime/closed", "params": ThreadRealtimeClosedNotification } | { "method": "windows/worldWritableWarning", "params": WindowsWorldWritableWarningNotification } | { "method": "windowsSandbox/setupCompleted", "params": WindowsSandboxSetupCompletedNotification } | { "method": "account/login/completed", "params": AccountLoginCompletedNotification }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeCreditType.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeCreditType.ts new file mode 100644 index 0000000000..70498d6a67 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeCreditType.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddCreditsNudgeCreditType = "credits" | "usage_limit"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeEmailStatus.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeEmailStatus.ts new file mode 100644 index 0000000000..2b62da68ea --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AddCreditsNudgeEmailStatus.ts @@ -0,0 +1,5 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type AddCreditsNudgeEmailStatus = "sent" | "cooldown_active"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts index 9ac1c50c1e..257dc47a1e 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInstallParams.ts @@ -3,8 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -export type PluginInstallParams = { marketplacePath: AbsolutePathBuf, pluginName: string, -/** - * When true, apply the remote plugin change before the local install flow. - */ -forceRemoteSync?: boolean, }; +export type PluginInstallParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, pluginName: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInterface.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInterface.ts index 7e0a48aaec..4e97ee66f3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginInterface.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginInterface.ts @@ -8,4 +8,28 @@ export type PluginInterface = { displayName: string | null, shortDescription: st * Starter prompts for the plugin. Capped at 3 entries with a maximum of * 128 characters per entry. */ -defaultPrompt: Array | null, brandColor: string | null, composerIcon: AbsolutePathBuf | null, logo: AbsolutePathBuf | null, screenshots: Array, }; +defaultPrompt: Array | null, brandColor: string | null, +/** + * Local composer icon path, resolved from the installed plugin package. + */ +composerIcon: AbsolutePathBuf | null, +/** + * Remote composer icon URL from the plugin catalog. + */ +composerIconUrl: string | null, +/** + * Local logo path, resolved from the installed plugin package. + */ +logo: AbsolutePathBuf | null, +/** + * Remote logo URL from the plugin catalog. + */ +logoUrl: string | null, +/** + * Local screenshot paths, resolved from the installed plugin package. + */ +screenshots: Array, +/** + * Remote screenshot URLs from the plugin catalog. + */ +screenshotUrls: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts index cd2a0cde1e..dcf23796db 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListParams.ts @@ -8,9 +8,4 @@ export type PluginListParams = { * Optional working directories used to discover repo marketplaces. When omitted, * only home-scoped marketplaces and the official curated marketplace are considered. */ -cwds?: Array | null, -/** - * When true, reconcile the official curated marketplace against the remote plugin state - * before listing marketplaces. - */ -forceRemoteSync?: boolean, }; +cwds?: Array | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListResponse.ts index 7ae5f8e505..d50200c905 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginListResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginListResponse.ts @@ -4,4 +4,4 @@ import type { MarketplaceLoadErrorInfo } from "./MarketplaceLoadErrorInfo"; import type { PluginMarketplaceEntry } from "./PluginMarketplaceEntry"; -export type PluginListResponse = { marketplaces: Array, marketplaceLoadErrors: Array, remoteSyncError: string | null, featuredPluginIds: Array, }; +export type PluginListResponse = { marketplaces: Array, marketplaceLoadErrors: Array, featuredPluginIds: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginMarketplaceEntry.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginMarketplaceEntry.ts index c0ab75b8f9..f9dcee27df 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginMarketplaceEntry.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginMarketplaceEntry.ts @@ -5,4 +5,9 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { MarketplaceInterface } from "./MarketplaceInterface"; import type { PluginSummary } from "./PluginSummary"; -export type PluginMarketplaceEntry = { name: string, path: AbsolutePathBuf, interface: MarketplaceInterface | null, plugins: Array, }; +export type PluginMarketplaceEntry = { name: string, +/** + * Local marketplace file path when the marketplace is backed by a local file. + * Remote-only catalog marketplaces do not have a local path. + */ +path: AbsolutePathBuf | null, interface: MarketplaceInterface | null, plugins: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts index cd6696873d..8c4394f0da 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginReadParams.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -export type PluginReadParams = { marketplacePath: AbsolutePathBuf, pluginName: string, }; +export type PluginReadParams = { marketplacePath?: AbsolutePathBuf | null, remoteMarketplaceName?: string | null, pluginName: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts index 5c8771aa0b..f6e867195d 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginSource.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, }; +export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "remote" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PluginUninstallParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PluginUninstallParams.ts index aa1d1bfef9..e7f52c0eb3 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PluginUninstallParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PluginUninstallParams.ts @@ -2,8 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PluginUninstallParams = { pluginId: string, -/** - * When true, apply the remote plugin change before the local uninstall flow. - */ -forceRemoteSync?: boolean, }; +export type PluginUninstallParams = { pluginId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailParams.ts new file mode 100644 index 0000000000..383ad4aab3 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailParams.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; + +export type SendAddCreditsNudgeEmailParams = { creditType: AddCreditsNudgeCreditType, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailResponse.ts new file mode 100644 index 0000000000..71dcb190a6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SendAddCreditsNudgeEmailResponse.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; + +export type SendAddCreditsNudgeEmailResponse = { status: AddCreditsNudgeEmailStatus, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts new file mode 100644 index 0000000000..bd3433be41 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WarningNotification.ts @@ -0,0 +1,13 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type WarningNotification = { +/** + * Optional thread target when the warning applies to a specific thread. + */ +threadId: string | null, +/** + * Concise warning message for the user. + */ +message: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index f9884efb7c..cbdef12100 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -4,6 +4,8 @@ export type { Account } from "./Account"; export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification"; export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification"; export type { AccountUpdatedNotification } from "./AccountUpdatedNotification"; +export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType"; +export type { AddCreditsNudgeEmailStatus } from "./AddCreditsNudgeEmailStatus"; export type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; export type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; export type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; @@ -259,6 +261,8 @@ export type { ReviewTarget } from "./ReviewTarget"; export type { SandboxMode } from "./SandboxMode"; export type { SandboxPolicy } from "./SandboxPolicy"; export type { SandboxWorkspaceWrite } from "./SandboxWorkspaceWrite"; +export type { SendAddCreditsNudgeEmailParams } from "./SendAddCreditsNudgeEmailParams"; +export type { SendAddCreditsNudgeEmailResponse } from "./SendAddCreditsNudgeEmailResponse"; export type { ServerRequestResolvedNotification } from "./ServerRequestResolvedNotification"; export type { SessionSource } from "./SessionSource"; export type { SkillDependencies } from "./SkillDependencies"; @@ -362,6 +366,7 @@ export type { TurnStatus } from "./TurnStatus"; export type { TurnSteerParams } from "./TurnSteerParams"; export type { TurnSteerResponse } from "./TurnSteerResponse"; export type { UserInput } from "./UserInput"; +export type { WarningNotification } from "./WarningNotification"; export type { WebSearchAction } from "./WebSearchAction"; export type { WindowsSandboxSetupCompletedNotification } from "./WindowsSandboxSetupCompletedNotification"; export type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index c29dbccd50..4e5882f83a 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -524,6 +524,11 @@ client_request_definitions! { response: v2::GetAccountRateLimitsResponse, }, + SendAddCreditsNudgeEmail => "account/sendAddCreditsNudgeEmail" { + params: v2::SendAddCreditsNudgeEmailParams, + response: v2::SendAddCreditsNudgeEmailResponse, + }, + FeedbackUpload => "feedback/upload" { params: v2::FeedbackUploadParams, response: v2::FeedbackUploadResponse, @@ -1029,6 +1034,7 @@ server_notification_definitions! { /// Deprecated: Use `ContextCompaction` item type instead. ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification), ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification), + Warning => "warning" (v2::WarningNotification), DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification), ConfigWarning => "configWarning" (v2::ConfigWarningNotification), FuzzyFileSearchSessionUpdated => "fuzzyFileSearch/sessionUpdated" (FuzzyFileSearchSessionUpdatedNotification), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 6ab79f4763..97dab86588 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1789,6 +1789,36 @@ pub struct GetAccountRateLimitsResponse { pub rate_limits_by_limit_id: Option>, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SendAddCreditsNudgeEmailParams { + pub credit_type: AddCreditsNudgeCreditType, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/", rename_all = "snake_case")] +pub enum AddCreditsNudgeCreditType { + Credits, + UsageLimit, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct SendAddCreditsNudgeEmailResponse { + pub status: AddCreditsNudgeEmailStatus, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export_to = "v2/", rename_all = "snake_case")] +pub enum AddCreditsNudgeEmailStatus { + Sent, + CooldownActive, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -3459,10 +3489,6 @@ pub struct PluginListParams { /// only home-scoped marketplaces and the official curated marketplace are considered. #[ts(optional = nullable)] pub cwds: Option>, - /// When true, reconcile the official curated marketplace against the remote plugin state - /// before listing marketplaces. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub force_remote_sync: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -3472,7 +3498,6 @@ pub struct PluginListResponse { pub marketplaces: Vec, #[serde(default)] pub marketplace_load_errors: Vec, - pub remote_sync_error: Option, #[serde(default)] pub featured_plugin_ids: Vec, } @@ -3489,7 +3514,10 @@ pub struct MarketplaceLoadErrorInfo { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct PluginReadParams { - pub marketplace_path: AbsolutePathBuf, + #[ts(optional = nullable)] + pub marketplace_path: Option, + #[ts(optional = nullable)] + pub remote_marketplace_name: Option, pub plugin_name: String, } @@ -3601,7 +3629,9 @@ pub struct SkillsListEntry { #[ts(export_to = "v2/")] pub struct PluginMarketplaceEntry { pub name: String, - pub path: AbsolutePathBuf, + /// Local marketplace file path when the marketplace is backed by a local file. + /// Remote-only catalog marketplaces do not have a local path. + pub path: Option, pub interface: Option, pub plugins: Vec, } @@ -3694,9 +3724,18 @@ pub struct PluginInterface { /// 128 characters per entry. pub default_prompt: Option>, pub brand_color: Option, + /// Local composer icon path, resolved from the installed plugin package. pub composer_icon: Option, + /// Remote composer icon URL from the plugin catalog. + pub composer_icon_url: Option, + /// Local logo path, resolved from the installed plugin package. pub logo: Option, + /// Remote logo URL from the plugin catalog. + pub logo_url: Option, + /// Local screenshot paths, resolved from the installed plugin package. pub screenshots: Vec, + /// Remote screenshot URLs from the plugin catalog. + pub screenshot_urls: Vec, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -3715,6 +3754,9 @@ pub enum PluginSource { ref_name: Option, sha: Option, }, + /// The plugin is available in the remote catalog. Download metadata is + /// kept server-side and is not exposed through the app-server API. + Remote, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -3741,11 +3783,11 @@ pub struct SkillsConfigWriteResponse { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct PluginInstallParams { - pub marketplace_path: AbsolutePathBuf, + #[ts(optional = nullable)] + pub marketplace_path: Option, + #[ts(optional = nullable)] + pub remote_marketplace_name: Option, pub plugin_name: String, - /// When true, apply the remote plugin change before the local install flow. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub force_remote_sync: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -3761,9 +3803,6 @@ pub struct PluginInstallResponse { #[ts(export_to = "v2/")] pub struct PluginUninstallParams { pub plugin_id: String, - /// When true, apply the remote plugin change before the local uninstall flow. - #[serde(default, skip_serializing_if = "std::ops::Not::not")] - pub force_remote_sync: bool, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -6680,6 +6719,16 @@ pub struct DeprecationNoticeNotification { pub details: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct WarningNotification { + /// Optional thread target when the warning applies to a specific thread. + pub thread_id: Option, + /// Concise warning message for the user. + pub message: String, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] @@ -8581,27 +8630,44 @@ mod tests { } #[test] - fn plugin_list_params_serialization_uses_force_remote_sync() { + fn plugin_source_serializes_local_git_and_remote_variants() { + let local_path = if cfg!(windows) { + r"C:\plugins\linear" + } else { + "/plugins/linear" + }; + let local_path = AbsolutePathBuf::try_from(PathBuf::from(local_path)).unwrap(); + let local_path_json = local_path.as_path().display().to_string(); + assert_eq!( - serde_json::to_value(PluginListParams { - cwds: None, - force_remote_sync: false, - }) - .unwrap(), + serde_json::to_value(PluginSource::Local { path: local_path }).unwrap(), json!({ - "cwds": null, + "type": "local", + "path": local_path_json, }), ); assert_eq!( - serde_json::to_value(PluginListParams { - cwds: None, - force_remote_sync: true, + serde_json::to_value(PluginSource::Git { + url: "https://github.com/openai/example.git".to_string(), + path: Some("plugins/example".to_string()), + ref_name: Some("main".to_string()), + sha: Some("abc123".to_string()), }) .unwrap(), json!({ - "cwds": null, - "forceRemoteSync": true, + "type": "git", + "url": "https://github.com/openai/example.git", + "path": "plugins/example", + "refName": "main", + "sha": "abc123", + }), + ); + + assert_eq!( + serde_json::to_value(PluginSource::Remote).unwrap(), + json!({ + "type": "remote", }), ); } @@ -8638,7 +8704,143 @@ mod tests { } #[test] - fn plugin_install_params_serialization_uses_force_remote_sync() { + fn plugin_marketplace_entry_serializes_remote_only_path_as_null() { + assert_eq!( + serde_json::to_value(PluginMarketplaceEntry { + name: "openai-curated".to_string(), + path: None, + interface: None, + plugins: Vec::new(), + }) + .unwrap(), + json!({ + "name": "openai-curated", + "path": null, + "interface": null, + "plugins": [], + }), + ); + } + + #[test] + fn plugin_interface_serializes_local_paths_and_remote_urls_separately() { + let composer_icon = if cfg!(windows) { + r"C:\plugins\linear\icon.png" + } else { + "/plugins/linear/icon.png" + }; + let composer_icon = AbsolutePathBuf::try_from(PathBuf::from(composer_icon)).unwrap(); + let composer_icon_json = composer_icon.as_path().display().to_string(); + + let interface = PluginInterface { + display_name: Some("Linear".to_string()), + short_description: None, + long_description: None, + developer_name: None, + category: Some("Productivity".to_string()), + capabilities: Vec::new(), + website_url: None, + privacy_policy_url: None, + terms_of_service_url: None, + default_prompt: None, + brand_color: None, + composer_icon: Some(composer_icon), + composer_icon_url: Some("https://example.com/linear/icon.png".to_string()), + logo: None, + logo_url: Some("https://example.com/linear/logo.png".to_string()), + screenshots: Vec::new(), + screenshot_urls: vec!["https://example.com/linear/screenshot.png".to_string()], + }; + + assert_eq!( + serde_json::to_value(interface).unwrap(), + json!({ + "displayName": "Linear", + "shortDescription": null, + "longDescription": null, + "developerName": null, + "category": "Productivity", + "capabilities": [], + "websiteUrl": null, + "privacyPolicyUrl": null, + "termsOfServiceUrl": null, + "defaultPrompt": null, + "brandColor": null, + "composerIcon": composer_icon_json, + "composerIconUrl": "https://example.com/linear/icon.png", + "logo": null, + "logoUrl": "https://example.com/linear/logo.png", + "screenshots": [], + "screenshotUrls": ["https://example.com/linear/screenshot.png"], + }), + ); + } + + #[test] + fn plugin_list_params_ignore_removed_force_remote_sync_field() { + assert_eq!( + serde_json::from_value::(json!({ + "cwds": null, + "forceRemoteSync": true, + })) + .unwrap(), + PluginListParams { cwds: None }, + ); + } + + #[test] + fn plugin_read_params_serialization_uses_install_source_fields() { + let marketplace_path = if cfg!(windows) { + r"C:\plugins\marketplace.json" + } else { + "/plugins/marketplace.json" + }; + let marketplace_path = AbsolutePathBuf::try_from(PathBuf::from(marketplace_path)).unwrap(); + let marketplace_path_json = marketplace_path.as_path().display().to_string(); + assert_eq!( + serde_json::to_value(PluginReadParams { + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }) + .unwrap(), + json!({ + "marketplacePath": marketplace_path_json, + "remoteMarketplaceName": null, + "pluginName": "gmail", + }), + ); + + assert_eq!( + serde_json::from_value::(json!({ + "marketplacePath": marketplace_path_json, + "pluginName": "gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginReadParams { + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }, + ); + + assert_eq!( + serde_json::from_value::(json!({ + "remoteMarketplaceName": "openai-curated", + "pluginName": "gmail", + })) + .unwrap(), + PluginReadParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "gmail".to_string(), + }, + ); + } + + #[test] + fn plugin_install_params_serialization_omits_force_remote_sync() { let marketplace_path = if cfg!(windows) { r"C:\plugins\marketplace.json" } else { @@ -8648,38 +8850,52 @@ mod tests { let marketplace_path_json = marketplace_path.as_path().display().to_string(); assert_eq!( serde_json::to_value(PluginInstallParams { - marketplace_path: marketplace_path.clone(), + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, plugin_name: "gmail".to_string(), - force_remote_sync: false, }) .unwrap(), json!({ "marketplacePath": marketplace_path_json, + "remoteMarketplaceName": null, "pluginName": "gmail", }), ); assert_eq!( - serde_json::to_value(PluginInstallParams { - marketplace_path, - plugin_name: "gmail".to_string(), - force_remote_sync: true, - }) - .unwrap(), - json!({ + serde_json::from_value::(json!({ "marketplacePath": marketplace_path_json, "pluginName": "gmail", "forceRemoteSync": true, - }), + })) + .unwrap(), + PluginInstallParams { + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, + plugin_name: "gmail".to_string(), + }, + ); + + assert_eq!( + serde_json::from_value::(json!({ + "remoteMarketplaceName": "openai-curated", + "pluginName": "gmail", + "forceRemoteSync": true, + })) + .unwrap(), + PluginInstallParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "gmail".to_string(), + }, ); } #[test] - fn plugin_uninstall_params_serialization_uses_force_remote_sync() { + fn plugin_uninstall_params_serialization_omits_force_remote_sync() { assert_eq!( serde_json::to_value(PluginUninstallParams { plugin_id: "gmail@openai-curated".to_string(), - force_remote_sync: false, }) .unwrap(), json!({ @@ -8688,15 +8904,14 @@ mod tests { ); assert_eq!( - serde_json::to_value(PluginUninstallParams { - plugin_id: "gmail@openai-curated".to_string(), - force_remote_sync: true, - }) - .unwrap(), - json!({ + serde_json::from_value::(json!({ "pluginId": "gmail@openai-curated", "forceRemoteSync": true, - }), + })) + .unwrap(), + PluginUninstallParams { + plugin_id: "gmail@openai-curated".to_string(), + }, ); } diff --git a/codex-rs/app-server/BUILD.bazel b/codex-rs/app-server/BUILD.bazel index 872f533be1..d2d3f42a19 100644 --- a/codex-rs/app-server/BUILD.bazel +++ b/codex-rs/app-server/BUILD.bazel @@ -4,5 +4,9 @@ codex_rust_crate( name = "app-server", crate_name = "codex_app_server", integration_test_timeout = "long", + test_shard_counts = { + "app-server-all-test": 8, + "app-server-unit-tests": 8, + }, test_tags = ["no-sandbox"], ) diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 88156f5532..e34e44d8c4 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -185,7 +185,7 @@ Example with notification opt-out: - `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly. - `skills/list` — list skills for one or more `cwd` values (optional `forceReload`). - `marketplace/add` — add a remote plugin marketplace from an HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand, then persist it into the user marketplace config. Returns the installed root path plus whether the marketplace was already present. -- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**). +- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category (**under development; do not call from production clients yet**). - `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**). - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. @@ -958,6 +958,10 @@ Event notifications are the server-initiated event stream for thread lifecycles, Thread realtime uses a separate thread-scoped notification surface. `thread/realtime/*` notifications are ephemeral transport events, not `ThreadItem`s, and are not returned by `thread/read`, `thread/resume`, or `thread/fork`. +Recoverable configuration and initialization warnings use the existing `configWarning` notification: `{ summary, details?, path?, range? }`. App-server may emit it during initialization for config parsing and related setup diagnostics. + +Generic runtime warnings use the `warning` notification: `{ threadId?, message }`. App-server emits this for non-fatal warnings from the core event stream, including cases where not all enabled skills are included in the model-visible skills list for a session. + ### Notification opt-out Clients can suppress specific notifications per connection by sending exact method names in `initialize.params.capabilities.optOutNotificationMethods`. @@ -1444,6 +1448,7 @@ Codex supports these authentication modes. The current mode is surfaced in `acco - `account/updated` (notify) — emitted whenever auth mode changes (`authMode`: `apikey`, `chatgpt`, or `null`) and includes the current ChatGPT `planType` when available. - `account/rateLimits/read` — fetch ChatGPT rate limits; updates arrive via `account/rateLimits/updated` (notify). - `account/rateLimits/updated` (notify) — emitted whenever a user's ChatGPT rate limits change. +- `account/sendAddCreditsNudgeEmail` — ask ChatGPT to email the workspace owner about depleted credits or a reached usage limit. - `mcpServer/oauthLogin/completed` (notify) — emitted after a `mcpServer/oauth/login` flow finishes for a server; payload includes `{ name, success, error? }`. - `mcpServer/startupStatus/updated` (notify) — emitted when a configured MCP server's startup status changes for a loaded thread; payload includes `{ name, status, error }` where `status` is `starting`, `ready`, `failed`, or `cancelled`. @@ -1536,7 +1541,7 @@ Field notes: ```json { "method": "account/rateLimits/read", "id": 7 } -{ "id": 7, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null } } } +{ "id": 7, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null, "rateLimitReachedType": null } } } { "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } } ``` @@ -1545,6 +1550,16 @@ Field notes: - `usedPercent` is current usage within the OpenAI quota window. - `windowDurationMins` is the quota window length. - `resetsAt` is a Unix timestamp (seconds) for the next reset. +- `rateLimitReachedType` identifies the backend-classified limit state when one has been reached. + +### 8) Notify a workspace owner about a limit + +```json +{ "method": "account/sendAddCreditsNudgeEmail", "id": 8, "params": { "creditType": "credits" } } +{ "id": 8, "result": { "status": "sent" } } +``` + +Use `creditType: "credits"` when workspace credits are depleted, or `creditType: "usage_limit"` when the workspace usage limit has been reached. If the owner was already notified recently, the response status is `cooldown_active`. ## Experimental API Opt-in diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 0b243c0304..82105f9a8a 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -101,6 +101,7 @@ use codex_app_server_protocol::TurnPlanStep; use codex_app_server_protocol::TurnPlanUpdatedNotification; use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; +use codex_app_server_protocol::WarningNotification; use codex_app_server_protocol::build_command_execution_end_item; use codex_app_server_protocol::build_file_change_approval_request_item; use codex_app_server_protocol::build_file_change_begin_item; @@ -268,7 +269,21 @@ pub(crate) async fn apply_bespoke_event_handling( .await; } } - EventMsg::Warning(_warning_event) => {} + EventMsg::Warning(warning_event) => { + if let ApiVersion::V2 = api_version { + let notification = WarningNotification { + thread_id: Some(conversation_id.to_string()), + message: warning_event.message, + }; + if let Some(analytics_events_client) = analytics_events_client.as_ref() { + analytics_events_client + .track_notification(ServerNotification::Warning(notification.clone())); + } + outgoing + .send_server_notification(ServerNotification::Warning(notification)) + .await; + } + } EventMsg::GuardianAssessment(assessment) => { if let ApiVersion::V2 = api_version { let pending_command_execution = match build_item_from_guardian_event( diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index e3f6322d40..4cd97e99a6 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -29,6 +29,8 @@ use codex_analytics::TurnSteerRequestError; use codex_app_server_protocol::Account; use codex_app_server_protocol::AccountLoginCompletedNotification; use codex_app_server_protocol::AccountUpdatedNotification; +use codex_app_server_protocol::AddCreditsNudgeCreditType; +use codex_app_server_protocol::AddCreditsNudgeEmailStatus; use codex_app_server_protocol::AppInfo; use codex_app_server_protocol::AppsListParams; use codex_app_server_protocol::AppsListResponse; @@ -117,6 +119,8 @@ use codex_app_server_protocol::ReviewStartParams; use codex_app_server_protocol::ReviewStartResponse; use codex_app_server_protocol::ReviewTarget as ApiReviewTarget; use codex_app_server_protocol::SandboxMode; +use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; +use codex_app_server_protocol::SendAddCreditsNudgeEmailResponse; use codex_app_server_protocol::ServerNotification; use codex_app_server_protocol::ServerRequestResolvedNotification; use codex_app_server_protocol::SkillSummary; @@ -203,6 +207,7 @@ use codex_app_server_protocol::WindowsSandboxSetupStartParams; use codex_app_server_protocol::WindowsSandboxSetupStartResponse; use codex_app_server_protocol::build_turns_from_rollout_items; use codex_arg0::Arg0DispatchPaths; +use codex_backend_client::AddCreditsNudgeCreditType as BackendAddCreditsNudgeCreditType; use codex_backend_client::Client as BackendClient; use codex_chatgpt::connectors; use codex_cloud_requirements::cloud_requirements_loader; @@ -274,6 +279,7 @@ use codex_login::default_client::set_default_client_residency_requirement; use codex_login::login_with_api_key; use codex_login::request_device_code; use codex_login::run_login_server; +use codex_mcp::McpRuntimeEnvironment; use codex_mcp::McpServerStatusSnapshot; use codex_mcp::McpSnapshotDetail; use codex_mcp::collect_mcp_server_status_snapshot_with_detail; @@ -1187,6 +1193,10 @@ impl CodexMessageProcessor { self.get_account_rate_limits(to_connection_request_id(request_id)) .await; } + ClientRequest::SendAddCreditsNudgeEmail { request_id, params } => { + self.send_add_credits_nudge_email(to_connection_request_id(request_id), params) + .await; + } ClientRequest::FeedbackUpload { request_id, params } => { self.upload_feedback(to_connection_request_id(request_id), params) .await; @@ -1901,6 +1911,74 @@ impl CodexMessageProcessor { } } + async fn send_add_credits_nudge_email( + &self, + request_id: ConnectionRequestId, + params: SendAddCreditsNudgeEmailParams, + ) { + match self.send_add_credits_nudge_email_inner(params).await { + Ok(status) => { + self.outgoing + .send_response(request_id, SendAddCreditsNudgeEmailResponse { status }) + .await; + } + Err(error) => { + self.outgoing.send_error(request_id, error).await; + } + } + } + + async fn send_add_credits_nudge_email_inner( + &self, + params: SendAddCreditsNudgeEmailParams, + ) -> Result { + let Some(auth) = self.auth_manager.auth().await else { + return Err(JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "codex account authentication required to notify workspace owner" + .to_string(), + data: None, + }); + }; + + if !auth.is_chatgpt_auth() { + return Err(JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "chatgpt authentication required to notify workspace owner".to_string(), + data: None, + }); + } + + let client = BackendClient::from_auth(self.config.chatgpt_base_url.clone(), &auth) + .map_err(|err| JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to construct backend client: {err}"), + data: None, + })?; + + match client + .send_add_credits_nudge_email(Self::backend_credit_type(params.credit_type)) + .await + { + Ok(()) => Ok(AddCreditsNudgeEmailStatus::Sent), + Err(err) if err.status().is_some_and(|status| status.as_u16() == 429) => { + Ok(AddCreditsNudgeEmailStatus::CooldownActive) + } + Err(err) => Err(JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to notify workspace owner: {err}"), + data: None, + }), + } + } + + fn backend_credit_type(value: AddCreditsNudgeCreditType) -> BackendAddCreditsNudgeCreditType { + match value { + AddCreditsNudgeCreditType::Credits => BackendAddCreditsNudgeCreditType::Credits, + AddCreditsNudgeCreditType::UsageLimit => BackendAddCreditsNudgeCreditType::UsageLimit, + } + } + async fn fetch_account_rate_limits( &self, ) -> Result< @@ -4314,6 +4392,7 @@ impl CodexMessageProcessor { thread_id, thread: codex_thread, session_configured, + .. }) => { let SessionConfiguredEvent { rollout_path, .. } = session_configured; let Some(rollout_path) = rollout_path else { @@ -5646,10 +5725,40 @@ impl CodexMessageProcessor { .to_mcp_config(self.thread_manager.plugins_manager().as_ref()) .await; let auth = self.auth_manager.auth().await; + let runtime_environment = match self.thread_manager.environment_manager().current().await { + Ok(Some(environment)) => { + // Status listing has no turn cwd. This fallback is used only + // by executor-backed stdio MCPs whose config omits `cwd`. + McpRuntimeEnvironment::new(environment, config.cwd.to_path_buf()) + } + Ok(None) => McpRuntimeEnvironment::new( + Arc::new(codex_exec_server::Environment::default()), + config.cwd.to_path_buf(), + ), + Err(err) => { + // TODO(aibrahim): Investigate degrading MCP status listing when + // executor environment creation fails. + let error = JSONRPCErrorError { + code: INTERNAL_ERROR_CODE, + message: format!("failed to create environment: {err}"), + data: None, + }; + self.outgoing.send_error(request, error).await; + return; + } + }; tokio::spawn(async move { - Self::list_mcp_server_status_task(outgoing, request, params, config, mcp_config, auth) - .await; + Self::list_mcp_server_status_task( + outgoing, + request, + params, + config, + mcp_config, + auth, + runtime_environment, + ) + .await; }); } @@ -5660,6 +5769,7 @@ impl CodexMessageProcessor { config: Config, mcp_config: codex_mcp::McpConfig, auth: Option, + runtime_environment: McpRuntimeEnvironment, ) { let detail = match params.detail.unwrap_or(McpServerStatusDetail::Full) { McpServerStatusDetail::Full => McpSnapshotDetail::Full, @@ -5670,6 +5780,7 @@ impl CodexMessageProcessor { &mcp_config, auth.as_ref(), request_id.request_id.to_string(), + runtime_environment, detail, ) .await; @@ -6428,55 +6539,19 @@ impl CodexMessageProcessor { async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) { let plugins_manager = self.thread_manager.plugins_manager(); - let PluginListParams { - cwds, - force_remote_sync, - } = params; + let PluginListParams { cwds } = params; let roots = cwds.unwrap_or_default(); plugins_manager.maybe_start_non_curated_plugin_cache_refresh(&roots); - let mut config = match self.load_latest_config(/*fallback_cwd*/ None).await { + let config = match self.load_latest_config(/*fallback_cwd*/ None).await { Ok(config) => config, Err(err) => { self.outgoing.send_error(request_id, err).await; return; } }; - let mut remote_sync_error = None; let auth = self.auth_manager.auth().await; - if force_remote_sync { - match plugins_manager - .sync_plugins_from_remote(&config, auth.as_ref(), /*additive_only*/ false) - .await - { - Ok(sync_result) => { - info!( - installed_plugin_ids = ?sync_result.installed_plugin_ids, - enabled_plugin_ids = ?sync_result.enabled_plugin_ids, - disabled_plugin_ids = ?sync_result.disabled_plugin_ids, - uninstalled_plugin_ids = ?sync_result.uninstalled_plugin_ids, - "completed plugin/list remote sync" - ); - } - Err(err) => { - warn!( - error = %err, - "plugin/list remote sync failed; returning local marketplace state" - ); - remote_sync_error = Some(err.to_string()); - } - } - - config = match self.load_latest_config(/*fallback_cwd*/ None).await { - Ok(config) => config, - Err(err) => { - self.outgoing.send_error(request_id, err).await; - return; - } - }; - } - let config_for_marketplace_listing = config.clone(); let plugins_manager_for_marketplace_listing = plugins_manager.clone(); let (data, marketplace_load_errors) = match tokio::task::spawn_blocking(move || { @@ -6494,7 +6569,7 @@ impl CodexMessageProcessor { .into_iter() .map(|marketplace| PluginMarketplaceEntry { name: marketplace.name, - path: marketplace.path, + path: Some(marketplace.path), interface: marketplace.interface.map(|interface| MarketplaceInterface { display_name: interface.display_name, }), @@ -6509,7 +6584,7 @@ impl CodexMessageProcessor { source: marketplace_plugin_source_to_info(plugin.source), install_policy: plugin.policy.installation.into(), auth_policy: plugin.policy.authentication.into(), - interface: plugin.interface.map(plugin_interface_to_info), + interface: plugin.interface.map(local_plugin_interface_to_info), }) .collect(), }) @@ -6569,7 +6644,6 @@ impl CodexMessageProcessor { PluginListResponse { marketplaces: data, marketplace_load_errors, - remote_sync_error, featured_plugin_ids, }, ) @@ -6613,8 +6687,40 @@ impl CodexMessageProcessor { let plugins_manager = self.thread_manager.plugins_manager(); let PluginReadParams { marketplace_path, + remote_marketplace_name, plugin_name, } = params; + let marketplace_path = match (marketplace_path, remote_marketplace_name) { + (Some(marketplace_path), None) => marketplace_path, + (None, Some(remote_marketplace_name)) => { + self.outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!( + "remote plugin read is not supported yet for marketplace {remote_marketplace_name}" + ), + data: None, + }, + ) + .await; + return; + } + (Some(_), Some(_)) | (None, None) => { + self.outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "plugin/read requires exactly one of marketplacePath or remoteMarketplaceName".to_string(), + data: None, + }, + ) + .await; + return; + } + }; let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); let config = match self.load_latest_config(config_cwd).await { @@ -6664,7 +6770,7 @@ impl CodexMessageProcessor { enabled: outcome.plugin.enabled, install_policy: outcome.plugin.policy.installation.into(), auth_policy: outcome.plugin.policy.authentication.into(), - interface: outcome.plugin.interface.map(plugin_interface_to_info), + interface: outcome.plugin.interface.map(local_plugin_interface_to_info), }, description: outcome.plugin.description, skills: plugin_skills_to_info(&visible_skills, &outcome.plugin.disabled_skill_paths), @@ -6738,9 +6844,40 @@ impl CodexMessageProcessor { async fn plugin_install(&self, request_id: ConnectionRequestId, params: PluginInstallParams) { let PluginInstallParams { marketplace_path, + remote_marketplace_name, plugin_name, - force_remote_sync, } = params; + let marketplace_path = match (marketplace_path, remote_marketplace_name) { + (Some(marketplace_path), None) => marketplace_path, + (None, Some(remote_marketplace_name)) => { + self.outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!( + "remote plugin install is not supported yet for marketplace {remote_marketplace_name}" + ), + data: None, + }, + ) + .await; + return; + } + (Some(_), Some(_)) | (None, None) => { + self.outgoing + .send_error( + request_id, + JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "plugin/install requires exactly one of marketplacePath or remoteMarketplaceName".to_string(), + data: None, + }, + ) + .await; + return; + } + }; let config_cwd = marketplace_path.as_path().parent().map(Path::to_path_buf); let plugins_manager = self.thread_manager.plugins_manager(); @@ -6749,21 +6886,7 @@ impl CodexMessageProcessor { marketplace_path, }; - let install_result = if force_remote_sync { - let config = match self.load_latest_config(config_cwd.clone()).await { - Ok(config) => config, - Err(err) => { - self.outgoing.send_error(request_id, err).await; - return; - } - }; - let auth = self.auth_manager.auth().await; - plugins_manager - .install_plugin_with_remote_sync(&config, auth.as_ref(), request) - .await - } else { - plugins_manager.install_plugin(request).await - }; + let install_result = plugins_manager.install_plugin(request).await; match install_result { Ok(result) => { @@ -6915,27 +7038,10 @@ impl CodexMessageProcessor { request_id: ConnectionRequestId, params: PluginUninstallParams, ) { - let PluginUninstallParams { - plugin_id, - force_remote_sync, - } = params; + let PluginUninstallParams { plugin_id } = params; let plugins_manager = self.thread_manager.plugins_manager(); - let uninstall_result = if force_remote_sync { - let config = match self.load_latest_config(/*fallback_cwd*/ None).await { - Ok(config) => config, - Err(err) => { - self.outgoing.send_error(request_id, err).await; - return; - } - }; - let auth = self.auth_manager.auth().await; - plugins_manager - .uninstall_plugin_with_remote_sync(&config, auth.as_ref(), plugin_id) - .await - } else { - plugins_manager.uninstall_plugin(plugin_id).await - }; + let uninstall_result = plugins_manager.uninstall_plugin(plugin_id).await; match uninstall_result { Ok(()) => { @@ -9105,7 +9211,7 @@ fn plugin_skills_to_info( .collect() } -fn plugin_interface_to_info(interface: PluginManifestInterface) -> PluginInterface { +fn local_plugin_interface_to_info(interface: PluginManifestInterface) -> PluginInterface { PluginInterface { display_name: interface.display_name, short_description: interface.short_description, @@ -9119,8 +9225,11 @@ fn plugin_interface_to_info(interface: PluginManifestInterface) -> PluginInterfa default_prompt: interface.default_prompt, brand_color: interface.brand_color, composer_icon: interface.composer_icon, + composer_icon_url: None, logo: interface.logo, + logo_url: None, screenshots: interface.screenshots, + screenshot_urls: Vec::new(), } } diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs index ba946c67b6..65374a0d98 100644 --- a/codex-rs/app-server/src/lib.rs +++ b/codex-rs/app-server/src/lib.rs @@ -280,21 +280,16 @@ fn project_config_warning(config: &Config) -> Option ConfigLayerStackOrdering::LowestPrecedenceFirst, /*include_disabled*/ true, ) { - if !matches!(layer.name, ConfigLayerSource::Project { .. }) - || layer.disabled_reason.is_none() - { + let ConfigLayerSource::Project { dot_codex_folder } = &layer.name else { continue; - } - if let ConfigLayerSource::Project { dot_codex_folder } = &layer.name { - disabled_folders.push(( - dot_codex_folder.as_path().display().to_string(), - layer - .disabled_reason - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "config.toml is disabled.".to_string()), - )); - } + }; + let Some(disabled_reason) = &layer.disabled_reason else { + continue; + }; + disabled_folders.push(( + dot_codex_folder.as_path().display().to_string(), + disabled_reason.clone(), + )); } if disabled_folders.is_empty() { @@ -302,8 +297,8 @@ fn project_config_warning(config: &Config) -> Option } let mut message = concat!( - "Project config.toml files are disabled in the following folders. ", - "Settings in those files are ignored, but skills and exec policies still load.\n", + "Project-local config, hooks, and exec policies are disabled in the following folders ", + "until the project is trusted, but skills still load.\n", ) .to_string(); for (index, (folder, reason)) in disabled_folders.iter().enumerate() { diff --git a/codex-rs/app-server/tests/common/mcp_process.rs b/codex-rs/app-server/tests/common/mcp_process.rs index 27bacbcd23..05bb66d57d 100644 --- a/codex-rs/app-server/tests/common/mcp_process.rs +++ b/codex-rs/app-server/tests/common/mcp_process.rs @@ -58,6 +58,7 @@ use codex_app_server_protocol::PluginReadParams; use codex_app_server_protocol::PluginUninstallParams; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ReviewStartParams; +use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::SkillsListParams; use codex_app_server_protocol::ThreadArchiveParams; @@ -305,6 +306,16 @@ impl McpProcess { .await } + /// Send an `account/sendAddCreditsNudgeEmail` JSON-RPC request. + pub async fn send_add_credits_nudge_email_request( + &mut self, + params: SendAddCreditsNudgeEmailParams, + ) -> anyhow::Result { + let params = Some(serde_json::to_value(params)?); + self.send_request("account/sendAddCreditsNudgeEmail", params) + .await + } + /// Send an `account/read` JSON-RPC request. pub async fn send_get_account_request( &mut self, diff --git a/codex-rs/app-server/tests/common/models_cache.rs b/codex-rs/app-server/tests/common/models_cache.rs index f38b6fd992..557fa56204 100644 --- a/codex-rs/app-server/tests/common/models_cache.rs +++ b/codex-rs/app-server/tests/common/models_cache.rs @@ -43,6 +43,7 @@ fn preset_to_info(preset: &ModelPreset, priority: i32) -> ModelInfo { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs index 0a3315a07a..50dd071d84 100644 --- a/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs +++ b/codex-rs/app-server/tests/suite/v2/dynamic_tools.rs @@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStartResponse; use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -477,7 +478,7 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( DynamicToolCallOutputContentItem::InputImage { image_url } => { FunctionCallOutputContentItem::InputImage { image_url, - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), } } }) @@ -535,7 +536,8 @@ async fn dynamic_tool_call_round_trip_sends_content_items_to_model() -> Result<( }, { "type": "input_image", - "image_url": "data:image/png;base64,AAA" + "image_url": "data:image/png;base64,AAA", + "detail": "high" } ]) ); diff --git a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs index 21acf24906..049256b602 100644 --- a/codex-rs/app-server/tests/suite/v2/external_agent_config.rs +++ b/codex-rs/app-server/tests/suite/v2/external_agent_config.rs @@ -98,10 +98,7 @@ async fn external_agent_config_import_sends_completion_notification_for_local_pl assert_eq!(notification.method, "externalAgentConfig/import/completed"); let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: false, - }) + .send_plugin_list_request(PluginListParams { cwds: None }) .await?; let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_install.rs b/codex-rs/app-server/tests/suite/v2/plugin_install.rs index e51fac725f..3555dd745b 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_install.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_install.rs @@ -44,12 +44,6 @@ use tempfile::TempDir; use tokio::net::TcpListener; use tokio::task::JoinHandle; use tokio::time::timeout; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::ResponseTemplate; -use wiremock::matchers::header; -use wiremock::matchers::method; -use wiremock::matchers::path; // Plugin install tests wait on connector discovery after the install response path // starts, which is noticeably slower on Windows CI. @@ -82,6 +76,97 @@ async fn plugin_install_rejects_relative_marketplace_paths() -> Result<()> { Ok(()) } +#[tokio::test] +async fn plugin_install_rejects_missing_install_source() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path: None, + remote_marketplace_name: None, + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("requires exactly one of marketplacePath or remoteMarketplaceName") + ); + Ok(()) +} + +#[tokio::test] +async fn plugin_install_rejects_multiple_install_sources() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path: Some(AbsolutePathBuf::try_from( + codex_home.path().join("marketplace.json"), + )?), + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("requires exactly one of marketplacePath or remoteMarketplaceName") + ); + Ok(()) +} + +#[tokio::test] +async fn plugin_install_rejects_remote_marketplace_until_remote_install_is_supported() -> Result<()> +{ + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("remote plugin install is not supported yet") + ); + assert!(err.error.message.contains("openai-curated")); + Ok(()) +} + #[tokio::test] async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() -> Result<()> { let codex_home = TempDir::new()?; @@ -90,11 +175,11 @@ async fn plugin_install_returns_invalid_request_for_missing_marketplace_file() - let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path: AbsolutePathBuf::try_from( + marketplace_path: Some(AbsolutePathBuf::try_from( codex_home.path().join("missing-marketplace.json"), - )?, + )?), + remote_marketplace_name: None, plugin_name: "missing-plugin".to_string(), - force_remote_sync: false, }) .await?; @@ -131,9 +216,9 @@ async fn plugin_install_returns_invalid_request_for_not_available_plugin() -> Re let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; @@ -181,9 +266,9 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; @@ -198,76 +283,6 @@ async fn plugin_install_returns_invalid_request_for_disallowed_product_plugin() Ok(()) } -#[tokio::test] -async fn plugin_install_force_remote_sync_enables_remote_plugin_before_local_install() -> Result<()> -{ - let server = MockServer::start().await; - let codex_home = TempDir::new()?; - write_plugin_remote_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - - let repo_root = TempDir::new()?; - write_plugin_marketplace( - repo_root.path(), - "debug", - "sample-plugin", - "./sample-plugin", - /*install_policy*/ None, - /*auth_policy*/ None, - )?; - write_plugin_source(repo_root.path(), "sample-plugin", &[])?; - let marketplace_path = - AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; - - Mock::given(method("POST")) - .and(path("/backend-api/plugins/sample-plugin@debug/enable")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"{"id":"sample-plugin@debug","enabled":true}"#), - ) - .expect(1) - .mount(&server) - .await; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_plugin_install_request(PluginInstallParams { - marketplace_path, - plugin_name: "sample-plugin".to_string(), - force_remote_sync: true, - }) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginInstallResponse = to_response(response)?; - assert_eq!(response.apps_needing_auth, Vec::::new()); - - assert!( - codex_home - .path() - .join("plugins/cache/debug/sample-plugin/local/.codex-plugin/plugin.json") - .is_file() - ); - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(config.contains(r#"[plugins."sample-plugin@debug"]"#)); - assert!(config.contains("enabled = true")); - Ok(()) -} - #[tokio::test] async fn plugin_install_tracks_analytics_event() -> Result<()> { let analytics_server = start_analytics_events_server().await?; @@ -300,9 +315,9 @@ async fn plugin_install_tracks_analytics_event() -> Result<()> { let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -415,9 +430,9 @@ async fn plugin_install_returns_apps_needing_auth() -> Result<()> { let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; @@ -499,9 +514,9 @@ async fn plugin_install_filters_disallowed_apps_needing_auth() -> Result<()> { let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; @@ -566,9 +581,9 @@ async fn plugin_install_makes_bundled_mcp_servers_available_to_followup_requests let request_id = mcp .send_plugin_install_request(PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), - force_remote_sync: false, }) .await?; let response: JSONRPCResponse = timeout( @@ -758,23 +773,6 @@ fn write_analytics_config(codex_home: &std::path::Path, base_url: &str) -> std:: ) } -fn write_plugin_remote_sync_config( - codex_home: &std::path::Path, - base_url: &str, -) -> std::io::Result<()> { - std::fs::write( - codex_home.join("config.toml"), - format!( - r#" -chatgpt_base_url = "{base_url}" - -[features] -plugins = true -"# - ), - ) -} - fn write_plugin_marketplace( repo_root: &std::path::Path, marketplace_name: &str, diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 28056e9ec7..bf69df3c47 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -71,7 +71,6 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), - force_remote_sync: false, }) .await?; @@ -86,7 +85,7 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul response .marketplaces .iter() - .all(|marketplace| { marketplace.path != marketplace_path }), + .all(|marketplace| { marketplace.path.as_ref() != Some(&marketplace_path) }), "invalid marketplace should be skipped" ); assert_eq!(response.marketplace_load_errors.len(), 1); @@ -200,7 +199,6 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ AbsolutePathBuf::try_from(valid_repo_root.path())?, AbsolutePathBuf::try_from(invalid_repo_root.path())?, ]), - force_remote_sync: false, }) .await?; @@ -215,7 +213,7 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ response.marketplaces, vec![PluginMarketplaceEntry { name: "valid-marketplace".to_string(), - path: valid_marketplace_path, + path: Some(valid_marketplace_path), interface: None, plugins: vec![PluginSummary { id: "valid-plugin@valid-marketplace".to_string(), @@ -243,7 +241,6 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_ "unexpected error: {:?}", response.marketplace_load_errors ); - assert_eq!(response.remote_sync_error, None); assert!(response.featured_plugin_ids.is_empty()); Ok(()) } @@ -314,7 +311,6 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), - force_remote_sync: false, }) .await?; @@ -329,7 +325,7 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab response.marketplaces, vec![PluginMarketplaceEntry { name: "alternate-marketplace".to_string(), - path: marketplace_path, + path: Some(marketplace_path), interface: None, plugins: vec![ PluginSummary { @@ -355,8 +351,11 @@ async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverab default_prompt: None, brand_color: None, composer_icon: None, + composer_icon_url: None, logo: None, + logo_url: None, screenshots: Vec::new(), + screenshot_urls: Vec::new(), }), }, PluginSummary { @@ -412,10 +411,7 @@ async fn plugin_list_accepts_omitted_cwds() -> Result<()> { timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: false, - }) + .send_plugin_list_request(PluginListParams { cwds: None }) .await?; let response: JSONRPCResponse = timeout( @@ -486,7 +482,6 @@ enabled = false let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), - force_remote_sync: false, }) .await?; @@ -501,11 +496,13 @@ enabled = false .marketplaces .into_iter() .find(|marketplace| { - marketplace.path - == AbsolutePathBuf::try_from( - repo_root.path().join(".agents/plugins/marketplace.json"), + marketplace.path.as_ref() + == Some( + &AbsolutePathBuf::try_from( + repo_root.path().join(".agents/plugins/marketplace.json"), + ) + .expect("absolute marketplace path"), ) - .expect("absolute marketplace path") }) .expect("expected repo marketplace entry"); @@ -641,7 +638,6 @@ enabled = false AbsolutePathBuf::try_from(workspace_enabled.path())?, AbsolutePathBuf::try_from(workspace_default.path())?, ]), - force_remote_sync: false, }) .await?; @@ -725,7 +721,6 @@ async fn plugin_list_returns_plugin_interface_with_absolute_asset_paths() -> Res let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), - force_remote_sync: false, }) .await?; @@ -838,7 +833,6 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> { let request_id = mcp .send_plugin_list_request(PluginListParams { cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]), - force_remote_sync: false, }) .await?; @@ -865,163 +859,6 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> { Ok(()) } -#[tokio::test] -async fn plugin_list_force_remote_sync_returns_remote_sync_error_on_fail_open() -> Result<()> { - let codex_home = TempDir::new()?; - write_plugin_sync_config(codex_home.path(), "https://chatgpt.com/backend-api/")?; - write_openai_curated_marketplace(codex_home.path(), &["linear"])?; - write_installed_plugin(&codex_home, "openai-curated", "linear")?; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: true, - }) - .await?; - - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginListResponse = to_response(response)?; - - assert!( - response - .remote_sync_error - .as_deref() - .is_some_and(|message| message.contains("chatgpt authentication required")) - ); - let curated_marketplace = response - .marketplaces - .into_iter() - .find(|marketplace| marketplace.name == "openai-curated") - .expect("expected openai-curated marketplace entry"); - assert_eq!( - curated_marketplace - .plugins - .into_iter() - .map(|plugin| (plugin.id, plugin.installed, plugin.enabled)) - .collect::>(), - vec![("linear@openai-curated".to_string(), true, false)] - ); - Ok(()) -} - -#[tokio::test] -async fn plugin_list_force_remote_sync_reconciles_curated_plugin_state() -> Result<()> { - let codex_home = TempDir::new()?; - let server = MockServer::start().await; - write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail", "calendar"])?; - write_installed_plugin(&codex_home, "openai-curated", "linear")?; - write_installed_plugin(&codex_home, "openai-curated", "gmail")?; - write_installed_plugin(&codex_home, "openai-curated", "calendar")?; - - Mock::given(method("GET")) - .and(path("/backend-api/plugins/list")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with(ResponseTemplate::new(200).set_body_string( - r#"[ - {"id":"1","name":"linear","marketplace_name":"openai-curated","version":"1.0.0","enabled":true}, - {"id":"2","name":"gmail","marketplace_name":"openai-curated","version":"1.0.0","enabled":false} -]"#, - )) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path("/backend-api/plugins/featured")) - .and(query_param("platform", "codex")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"["linear@openai-curated","calendar@openai-curated"]"#), - ) - .mount(&server) - .await; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: true, - }) - .await?; - - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginListResponse = to_response(response)?; - assert_eq!(response.remote_sync_error, None); - assert_eq!( - response.featured_plugin_ids, - vec![ - "linear@openai-curated".to_string(), - "calendar@openai-curated".to_string(), - ] - ); - - let curated_marketplace = response - .marketplaces - .into_iter() - .find(|marketplace| marketplace.name == "openai-curated") - .expect("expected openai-curated marketplace entry"); - assert_eq!( - curated_marketplace - .plugins - .into_iter() - .map(|plugin| (plugin.id, plugin.installed, plugin.enabled)) - .collect::>(), - vec![ - ("linear@openai-curated".to_string(), true, true), - ("gmail@openai-curated".to_string(), false, false), - ("calendar@openai-curated".to_string(), false, false), - ] - ); - - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(config.contains(r#"[plugins."linear@openai-curated"]"#)); - assert!(!config.contains(r#"[plugins."gmail@openai-curated"]"#)); - assert!(!config.contains(r#"[plugins."calendar@openai-curated"]"#)); - - assert!( - codex_home - .path() - .join("plugins/cache/openai-curated/linear/local") - .is_dir() - ); - assert!( - !codex_home - .path() - .join("plugins/cache/openai-curated/gmail") - .exists() - ); - assert!( - !codex_home - .path() - .join("plugins/cache/openai-curated/calendar") - .exists() - ); - Ok(()) -} - #[tokio::test] async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { let codex_home = TempDir::new()?; @@ -1069,10 +906,7 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> { wait_for_remote_plugin_request_count(&server, "/plugins/list", /*expected_count*/ 1) .await?; let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: false, - }) + .send_plugin_list_request(PluginListParams { cwds: None }) .await?; let response: JSONRPCResponse = timeout( DEFAULT_TIMEOUT, @@ -1128,10 +962,7 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: false, - }) + .send_plugin_list_request(PluginListParams { cwds: None }) .await?; let response: JSONRPCResponse = timeout( @@ -1145,7 +976,6 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul response.featured_plugin_ids, vec!["linear@openai-curated".to_string()] ); - assert_eq!(response.remote_sync_error, None); Ok(()) } @@ -1169,10 +999,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?; let request_id = mcp - .send_plugin_list_request(PluginListParams { - cwds: None, - force_remote_sync: false, - }) + .send_plugin_list_request(PluginListParams { cwds: None }) .await?; let response: JSONRPCResponse = timeout( @@ -1186,7 +1013,6 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() -> response.featured_plugin_ids, vec!["linear@openai-curated".to_string()] ); - assert_eq!(response.remote_sync_error, None); Ok(()) } diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index 20114e79ba..e79a72a920 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -45,6 +45,96 @@ use tokio::time::timeout; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); +#[tokio::test] +async fn plugin_read_rejects_missing_read_source() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: None, + remote_marketplace_name: None, + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("requires exactly one of marketplacePath or remoteMarketplaceName") + ); + Ok(()) +} + +#[tokio::test] +async fn plugin_read_rejects_multiple_read_sources() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: Some(AbsolutePathBuf::try_from( + codex_home.path().join("marketplace.json"), + )?), + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("requires exactly one of marketplacePath or remoteMarketplaceName") + ); + Ok(()) +} + +#[tokio::test] +async fn plugin_read_rejects_remote_marketplace_until_remote_read_is_supported() -> Result<()> { + let codex_home = TempDir::new()?; + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: None, + remote_marketplace_name: Some("openai-curated".to_string()), + plugin_name: "sample-plugin".to_string(), + }) + .await?; + + let err = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(err.error.code, -32600); + assert!( + err.error + .message + .contains("remote plugin read is not supported yet") + ); + assert!(err.error.message.contains("openai-curated")); + Ok(()) +} + #[tokio::test] async fn plugin_read_returns_plugin_details_with_bundle_contents() -> Result<()> { let codex_home = TempDir::new()?; @@ -179,7 +269,8 @@ enabled = true AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; let request_id = mcp .send_plugin_read_request(PluginReadParams { - marketplace_path: marketplace_path.clone(), + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, plugin_name: "demo-plugin".to_string(), }) .await?; @@ -326,7 +417,8 @@ async fn plugin_read_returns_app_needs_auth() -> Result<()> { let request_id = mcp .send_plugin_read_request(PluginReadParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name: "sample-plugin".to_string(), }) .await?; @@ -392,9 +484,10 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> { let request_id = mcp .send_plugin_read_request(PluginReadParams { - marketplace_path: AbsolutePathBuf::try_from( + marketplace_path: Some(AbsolutePathBuf::try_from( repo_root.path().join(".agents/plugins/marketplace.json"), - )?, + )?), + remote_marketplace_name: None, plugin_name: "demo-plugin".to_string(), }) .await?; @@ -418,6 +511,76 @@ async fn plugin_read_accepts_legacy_string_default_prompt() -> Result<()> { Ok(()) } +#[tokio::test] +async fn plugin_read_describes_uninstalled_git_source_without_cloning() -> Result<()> { + let codex_home = TempDir::new()?; + let repo_root = TempDir::new()?; + let missing_remote_repo = repo_root.path().join("missing-remote-plugin-repo"); + let missing_remote_repo_url = url::Url::from_directory_path(&missing_remote_repo) + .unwrap() + .to_string(); + std::fs::create_dir_all(repo_root.path().join(".git"))?; + std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?; + std::fs::write( + repo_root.path().join(".agents/plugins/marketplace.json"), + format!( + r#"{{ + "name": "debug", + "plugins": [ + {{ + "name": "toolkit", + "source": {{ + "source": "git-subdir", + "url": "{missing_remote_repo_url}", + "path": "plugins/toolkit" + }} + }} + ] +}}"# + ), + )?; + write_plugins_enabled_config(&codex_home)?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_plugin_read_request(PluginReadParams { + marketplace_path: Some(AbsolutePathBuf::try_from( + repo_root.path().join(".agents/plugins/marketplace.json"), + )?), + remote_marketplace_name: None, + plugin_name: "toolkit".to_string(), + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let response: PluginReadResponse = to_response(response)?; + + let expected_description = format!( + "This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {missing_remote_repo_url}, path `plugins/toolkit`." + ); + assert_eq!( + response.plugin.description.as_deref(), + Some(expected_description.as_str()) + ); + assert!(!response.plugin.summary.installed); + assert!(response.plugin.skills.is_empty()); + assert!(response.plugin.apps.is_empty()); + assert!(response.plugin.mcp_servers.is_empty()); + assert!( + !codex_home + .path() + .join("plugins/.marketplace-plugin-source-staging") + .exists() + ); + Ok(()) +} + #[tokio::test] async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result<()> { let codex_home = TempDir::new()?; @@ -446,9 +609,10 @@ async fn plugin_read_returns_invalid_request_when_plugin_is_missing() -> Result< let request_id = mcp .send_plugin_read_request(PluginReadParams { - marketplace_path: AbsolutePathBuf::try_from( + marketplace_path: Some(AbsolutePathBuf::try_from( repo_root.path().join(".agents/plugins/marketplace.json"), - )?, + )?), + remote_marketplace_name: None, plugin_name: "missing-plugin".to_string(), }) .await?; @@ -498,9 +662,10 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() - let request_id = mcp .send_plugin_read_request(PluginReadParams { - marketplace_path: AbsolutePathBuf::try_from( + marketplace_path: Some(AbsolutePathBuf::try_from( repo_root.path().join(".agents/plugins/marketplace.json"), - )?, + )?), + remote_marketplace_name: None, plugin_name: "demo-plugin".to_string(), }) .await?; diff --git a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs index 00fabe4832..512cce3994 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_uninstall.rs @@ -16,12 +16,6 @@ use pretty_assertions::assert_eq; use serde_json::json; use tempfile::TempDir; use tokio::time::timeout; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::ResponseTemplate; -use wiremock::matchers::header; -use wiremock::matchers::method; -use wiremock::matchers::path; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10); @@ -44,7 +38,6 @@ enabled = true let params = PluginUninstallParams { plugin_id: "sample-plugin@debug".to_string(), - force_remote_sync: false, }; let request_id = mcp.send_plugin_uninstall_request(params.clone()).await?; @@ -77,74 +70,6 @@ enabled = true Ok(()) } -#[tokio::test] -async fn plugin_uninstall_force_remote_sync_calls_remote_uninstall_first() -> Result<()> { - let server = MockServer::start().await; - let codex_home = TempDir::new()?; - write_installed_plugin(&codex_home, "debug", "sample-plugin")?; - std::fs::write( - codex_home.path().join("config.toml"), - format!( - r#"chatgpt_base_url = "{}/backend-api/" - -[features] -plugins = true - -[plugins."sample-plugin@debug"] -enabled = true -"#, - server.uri() - ), - )?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("chatgpt-token") - .account_id("account-123") - .chatgpt_user_id("user-123") - .chatgpt_account_id("account-123"), - AuthCredentialsStoreMode::File, - )?; - - Mock::given(method("POST")) - .and(path("/backend-api/plugins/sample-plugin@debug/uninstall")) - .and(header("authorization", "Bearer chatgpt-token")) - .and(header("chatgpt-account-id", "account-123")) - .respond_with( - ResponseTemplate::new(200) - .set_body_string(r#"{"id":"sample-plugin@debug","enabled":false}"#), - ) - .expect(1) - .mount(&server) - .await; - - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??; - - let request_id = mcp - .send_plugin_uninstall_request(PluginUninstallParams { - plugin_id: "sample-plugin@debug".to_string(), - force_remote_sync: true, - }) - .await?; - let response: JSONRPCResponse = timeout( - DEFAULT_TIMEOUT, - mcp.read_stream_until_response_message(RequestId::Integer(request_id)), - ) - .await??; - let response: PluginUninstallResponse = to_response(response)?; - assert_eq!(response, PluginUninstallResponse {}); - - assert!( - !codex_home - .path() - .join("plugins/cache/debug/sample-plugin") - .exists() - ); - let config = std::fs::read_to_string(codex_home.path().join("config.toml"))?; - assert!(!config.contains(r#"[plugins."sample-plugin@debug"]"#)); - Ok(()) -} - #[tokio::test] async fn plugin_uninstall_tracks_analytics_event() -> Result<()> { let analytics_server = start_analytics_events_server().await?; @@ -172,7 +97,6 @@ async fn plugin_uninstall_tracks_analytics_event() -> Result<()> { let request_id = mcp .send_plugin_uninstall_request(PluginUninstallParams { plugin_id: "sample-plugin@debug".to_string(), - force_remote_sync: false, }) .await?; let response: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/rate_limits.rs b/codex-rs/app-server/tests/suite/v2/rate_limits.rs index 20b1729f11..b15960c1e9 100644 --- a/codex-rs/app-server/tests/suite/v2/rate_limits.rs +++ b/codex-rs/app-server/tests/suite/v2/rate_limits.rs @@ -3,6 +3,8 @@ use app_test_support::ChatGptAuthFixture; use app_test_support::McpProcess; use app_test_support::to_response; use app_test_support::write_chatgpt_auth; +use codex_app_server_protocol::AddCreditsNudgeCreditType; +use codex_app_server_protocol::AddCreditsNudgeEmailStatus; use codex_app_server_protocol::GetAccountRateLimitsResponse; use codex_app_server_protocol::JSONRPCError; use codex_app_server_protocol::JSONRPCResponse; @@ -11,6 +13,8 @@ use codex_app_server_protocol::RateLimitReachedType; use codex_app_server_protocol::RateLimitSnapshot; use codex_app_server_protocol::RateLimitWindow; use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SendAddCreditsNudgeEmailParams; +use codex_app_server_protocol::SendAddCreditsNudgeEmailResponse; use codex_config::types::AuthCredentialsStoreMode; use codex_protocol::account::PlanType as AccountPlanType; use pretty_assertions::assert_eq; @@ -27,6 +31,7 @@ use wiremock::matchers::path; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); const INVALID_REQUEST_ERROR_CODE: i64 = -32600; +const INTERNAL_ERROR_CODE: i64 = -32603; #[tokio::test] async fn get_account_rate_limits_requires_auth() -> Result<()> { @@ -229,6 +234,209 @@ async fn get_account_rate_limits_returns_snapshot() -> Result<()> { Ok(()) } +#[tokio::test] +async fn send_add_credits_nudge_email_requires_auth() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams { + credit_type: AddCreditsNudgeCreditType::Credits, + }) + .await?; + + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(error.id, RequestId::Integer(request_id)); + assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); + assert_eq!( + error.error.message, + "codex account authentication required to notify workspace owner" + ); + + Ok(()) +} + +#[tokio::test] +async fn send_add_credits_nudge_email_requires_chatgpt_auth() -> Result<()> { + let codex_home = TempDir::new()?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + login_with_api_key(&mut mcp, "sk-test-key").await?; + + let request_id = mcp + .send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams { + credit_type: AddCreditsNudgeCreditType::UsageLimit, + }) + .await?; + + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(error.id, RequestId::Integer(request_id)); + assert_eq!(error.error.code, INVALID_REQUEST_ERROR_CODE); + assert_eq!( + error.error.message, + "chatgpt authentication required to notify workspace owner" + ); + + Ok(()) +} + +#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")] +#[tokio::test] +async fn send_add_credits_nudge_email_posts_expected_body() -> Result<()> { + let codex_home = TempDir::new()?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + + let server = MockServer::start().await; + let server_url = server.uri(); + write_chatgpt_base_url(codex_home.path(), &server_url)?; + + Mock::given(method("POST")) + .and(path("/api/codex/accounts/send_add_credits_nudge_email")) + .and(header("authorization", "Bearer chatgpt-token")) + .and(header("chatgpt-account-id", "account-123")) + .and(wiremock::matchers::body_json(json!({ + "credit_type": "usage_limit", + }))) + .respond_with(ResponseTemplate::new(200)) + .mount(&server) + .await; + + let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams { + credit_type: AddCreditsNudgeCreditType::UsageLimit, + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let received: SendAddCreditsNudgeEmailResponse = to_response(response)?; + + assert_eq!(received.status, AddCreditsNudgeEmailStatus::Sent); + + Ok(()) +} + +#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")] +#[tokio::test] +async fn send_add_credits_nudge_email_maps_cooldown() -> Result<()> { + let codex_home = TempDir::new()?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + + let server = MockServer::start().await; + let server_url = server.uri(); + write_chatgpt_base_url(codex_home.path(), &server_url)?; + + Mock::given(method("POST")) + .and(path("/api/codex/accounts/send_add_credits_nudge_email")) + .respond_with(ResponseTemplate::new(429)) + .mount(&server) + .await; + + let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams { + credit_type: AddCreditsNudgeCreditType::Credits, + }) + .await?; + + let response: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(request_id)), + ) + .await??; + let received: SendAddCreditsNudgeEmailResponse = to_response(response)?; + + assert_eq!(received.status, AddCreditsNudgeEmailStatus::CooldownActive); + + Ok(()) +} + +#[cfg_attr(target_os = "windows", ignore = "covered by Linux and macOS CI")] +#[tokio::test] +async fn send_add_credits_nudge_email_surfaces_backend_failure() -> Result<()> { + let codex_home = TempDir::new()?; + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("chatgpt-token") + .account_id("account-123") + .plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; + + let server = MockServer::start().await; + let server_url = server.uri(); + write_chatgpt_base_url(codex_home.path(), &server_url)?; + + Mock::given(method("POST")) + .and(path("/api/codex/accounts/send_add_credits_nudge_email")) + .respond_with(ResponseTemplate::new(500).set_body_string("boom")) + .mount(&server) + .await; + + let mut mcp = McpProcess::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let request_id = mcp + .send_add_credits_nudge_email_request(SendAddCreditsNudgeEmailParams { + credit_type: AddCreditsNudgeCreditType::Credits, + }) + .await?; + + let error: JSONRPCError = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + + assert_eq!(error.id, RequestId::Integer(request_id)); + assert_eq!(error.error.code, INTERNAL_ERROR_CODE); + assert!( + error + .error + .message + .contains("failed to notify workspace owner"), + "unexpected error message: {}", + error.error.message + ); + assert_eq!(error.error.data, None); + + Ok(()) +} + async fn login_with_api_key(mcp: &mut McpProcess, api_key: &str) -> Result<()> { let request_id = mcp.send_login_account_api_key_request(api_key).await?; let response: JSONRPCResponse = timeout( diff --git a/codex-rs/app-server/tests/suite/v2/thread_start.rs b/codex-rs/app-server/tests/suite/v2/thread_start.rs index 508c77f7ff..9f212de14f 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_start.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_start.rs @@ -21,6 +21,7 @@ use codex_app_server_protocol::ThreadStatus; use codex_app_server_protocol::ThreadStatusChangedNotification; use codex_config::types::AuthCredentialsStoreMode; use codex_core::config::set_project_trust_level; +use codex_core::config_loader::project_trust_key; use codex_exec_server::LOCAL_FS; use codex_git_utils::resolve_root_git_project_for_trust; use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR; @@ -722,7 +723,8 @@ model_reasoning_effort = "high" let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &workspace_abs) .await .unwrap_or(workspace_abs); - assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path()))); + let trusted_root_key = project_trust_key(trusted_root.as_path()); + assert!(config_toml.contains(&trusted_root_key)); assert!(config_toml.contains("trust_level = \"trusted\"")); Ok(()) @@ -761,8 +763,10 @@ async fn thread_start_with_nested_git_cwd_trusts_repo_root() -> Result<()> { let trusted_root = resolve_root_git_project_for_trust(LOCAL_FS.as_ref(), &nested_abs) .await .expect("git root should resolve"); - assert!(config_toml.contains(&persisted_trust_path(trusted_root.as_path()))); - assert!(!config_toml.contains(&persisted_trust_path(&nested))); + let trusted_root_key = project_trust_key(trusted_root.as_path()); + let nested_key = project_trust_key(&nested); + assert!(config_toml.contains(&trusted_root_key)); + assert!(!config_toml.contains(&nested_key)); Ok(()) } @@ -856,21 +860,6 @@ fn create_config_toml_without_approval_policy( ) } -fn persisted_trust_path(project_path: &Path) -> String { - let project_path = - std::fs::canonicalize(project_path).unwrap_or_else(|_| project_path.to_path_buf()); - let project_path = project_path.display().to_string(); - - if let Some(project_path) = project_path.strip_prefix(r"\\?\UNC\") { - return format!(r"\\{project_path}"); - } - - project_path - .strip_prefix(r"\\?\") - .unwrap_or(&project_path) - .to_string() -} - fn create_config_toml_with_optional_approval_policy( codex_home: &Path, server_uri: &str, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index e8682d7325..9f491b3368 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -11,6 +11,7 @@ use app_test_support::create_shell_command_sse_response; use app_test_support::format_with_current_shell_display; use app_test_support::to_response; use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url; +use app_test_support::write_models_cache; use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE; use codex_app_server::INVALID_PARAMS_ERROR_CODE; use codex_app_server_protocol::ByteRange; @@ -45,6 +46,7 @@ use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::TurnStartedNotification; use codex_app_server_protocol::TurnStatus; use codex_app_server_protocol::UserInput as V2UserInput; +use codex_app_server_protocol::WarningNotification; use codex_config::config_toml::ConfigToml; use codex_core::personality_migration::PERSONALITY_MIGRATION_FILENAME; use codex_features::FEATURES; @@ -244,6 +246,111 @@ async fn turn_start_emits_user_message_item_with_text_elements() -> Result<()> { Ok(()) } +#[tokio::test] +async fn turn_start_emits_thread_scoped_warning_notification_for_trimmed_skills() -> Result<()> { + let responses = vec![create_final_assistant_message_sse_response("Done")?]; + let server = create_mock_responses_server_sequence_unchecked(responses).await; + + let codex_home = TempDir::new()?; + create_config_toml( + codex_home.path(), + &server.uri(), + "never", + &BTreeMap::from([(Feature::Personality, true)]), + )?; + write_models_cache(codex_home.path())?; + let cache_path = codex_home.path().join("models_cache.json"); + let mut cache: serde_json::Value = + serde_json::from_str(&std::fs::read_to_string(&cache_path)?)?; + let models = cache["models"] + .as_array_mut() + .expect("models_cache.json models should be an array"); + let entry = models + .first_mut() + .expect("models cache should not be empty"); + let model = entry["slug"] + .as_str() + .expect("model slug should be present") + .to_string(); + entry["context_window"] = serde_json::Value::from(100); + std::fs::write(&cache_path, serde_json::to_string_pretty(&cache)?)?; + let config_path = codex_home.path().join("config.toml"); + let config = std::fs::read_to_string(&config_path)?; + std::fs::write( + &config_path, + config.replace("model = \"mock-model\"", &format!("model = \"{model}\"")), + )?; + write_test_skill(codex_home.path(), "alpha-skill")?; + write_test_skill(codex_home.path(), "beta-skill")?; + + let mut mcp = McpProcess::new(codex_home.path()).await?; + timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; + + let thread_req = mcp + .send_thread_start_request(ThreadStartParams::default()) + .await?; + let thread_resp: JSONRPCResponse = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(thread_req)), + ) + .await??; + let ThreadStartResponse { thread, .. } = to_response::(thread_resp)?; + + let turn_req = mcp + .send_turn_start_request(TurnStartParams { + thread_id: thread.id.clone(), + input: vec![V2UserInput::Text { + text: "Hello".to_string(), + text_elements: Vec::new(), + }], + ..Default::default() + }) + .await?; + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_response_message(RequestId::Integer(turn_req)), + ) + .await??; + + let notification = timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("warning"), + ) + .await??; + let params = notification.params.expect("warning params"); + let warning: WarningNotification = + serde_json::from_value(params).expect("deserialize warning notification"); + assert_eq!(warning.thread_id.as_deref(), Some(thread.id.as_str())); + assert_eq!( + warning.message, + "Some enabled skills were not included in the model-visible skills list for this session. Mention a skill by name or path if you need it." + ); + + timeout( + DEFAULT_READ_TIMEOUT, + mcp.read_stream_until_notification_message("turn/completed"), + ) + .await??; + + let requests = server + .received_requests() + .await + .expect("failed to fetch received requests"); + let request = requests + .last() + .expect("expected at least one model request"); + assert!( + body_contains(request, "## Skills"), + "expected outgoing request to include the skills section" + ); + assert!( + !body_contains(request, "- alpha-skill:") && !body_contains(request, "- beta-skill:"), + "expected trimmed skills to be omitted from the outgoing request body" + ); + + Ok(()) +} + #[tokio::test] async fn thread_start_omits_empty_instruction_overrides_from_model_request() -> Result<()> { let server = responses::start_mock_server().await; @@ -2902,3 +3009,12 @@ stream_max_retries = 0 ), ) } + +fn write_test_skill(codex_home: &Path, name: &str) -> std::io::Result<()> { + let skill_dir = codex_home.join("skills").join(name); + std::fs::create_dir_all(&skill_dir)?; + std::fs::write( + skill_dir.join("SKILL.md"), + format!("---\nname: {name}\ndescription: {name} description\n---\n\n# Body\n"), + ) +} diff --git a/codex-rs/backend-client/src/client.rs b/codex-rs/backend-client/src/client.rs index 84fbffb690..8f84ef28f4 100644 --- a/codex-rs/backend-client/src/client.rs +++ b/codex-rs/backend-client/src/client.rs @@ -20,6 +20,7 @@ use reqwest::header::HeaderMap; use reqwest::header::HeaderName; use reqwest::header::HeaderValue; use reqwest::header::USER_AGENT; +use serde::Serialize; use serde::de::DeserializeOwned; use std::fmt; @@ -81,6 +82,18 @@ impl From for RequestError { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AddCreditsNudgeCreditType { + Credits, + UsageLimit, +} + +#[derive(Serialize)] +struct SendAddCreditsNudgeEmailRequest { + credit_type: AddCreditsNudgeCreditType, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PathStyle { /// /api/codex/… @@ -282,6 +295,21 @@ impl Client { Ok(Self::rate_limit_snapshots_from_payload(payload)) } + pub async fn send_add_credits_nudge_email( + &self, + credit_type: AddCreditsNudgeCreditType, + ) -> std::result::Result<(), RequestError> { + let url = self.send_add_credits_nudge_email_url(); + let req = self + .http + .post(&url) + .headers(self.headers()) + .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) + .json(&SendAddCreditsNudgeEmailRequest { credit_type }); + self.exec_request_detailed(req, "POST", &url).await?; + Ok(()) + } + pub async fn list_tasks( &self, limit: Option, @@ -490,6 +518,21 @@ impl Client { } } + fn send_add_credits_nudge_email_url(&self) -> String { + match self.path_style { + PathStyle::CodexApi => format!( + "{}/api/codex/accounts/send_add_credits_nudge_email", + self.base_url + ), + PathStyle::ChatGptApi => { + format!( + "{}/wham/accounts/send_add_credits_nudge_email", + self.base_url + ) + } + } + } + fn map_rate_limit_window( window: Option>>, ) -> Option { @@ -767,4 +810,50 @@ mod tests { let snapshots = Client::rate_limit_snapshots_from_payload(payload); assert_eq!(snapshots[0].rate_limit_reached_type, None); } + + #[test] + fn add_credits_nudge_email_uses_expected_paths_and_bodies() { + let codex_client = Client { + base_url: "https://example.test".to_string(), + http: reqwest::Client::new(), + bearer_token: None, + user_agent: None, + chatgpt_account_id: None, + chatgpt_account_is_fedramp: false, + path_style: PathStyle::CodexApi, + }; + assert_eq!( + codex_client.send_add_credits_nudge_email_url(), + "https://example.test/api/codex/accounts/send_add_credits_nudge_email" + ); + + let chatgpt_client = Client { + base_url: "https://chatgpt.com/backend-api".to_string(), + http: reqwest::Client::new(), + bearer_token: None, + user_agent: None, + chatgpt_account_id: None, + chatgpt_account_is_fedramp: false, + path_style: PathStyle::ChatGptApi, + }; + assert_eq!( + chatgpt_client.send_add_credits_nudge_email_url(), + "https://chatgpt.com/backend-api/wham/accounts/send_add_credits_nudge_email" + ); + + assert_eq!( + serde_json::to_value(SendAddCreditsNudgeEmailRequest { + credit_type: AddCreditsNudgeCreditType::Credits, + }) + .unwrap(), + serde_json::json!({ "credit_type": "credits" }) + ); + assert_eq!( + serde_json::to_value(SendAddCreditsNudgeEmailRequest { + credit_type: AddCreditsNudgeCreditType::UsageLimit, + }) + .unwrap(), + serde_json::json!({ "credit_type": "usage_limit" }) + ); + } } diff --git a/codex-rs/backend-client/src/lib.rs b/codex-rs/backend-client/src/lib.rs index 397c2a2cd4..300da81568 100644 --- a/codex-rs/backend-client/src/lib.rs +++ b/codex-rs/backend-client/src/lib.rs @@ -1,6 +1,7 @@ mod client; pub(crate) mod types; +pub use client::AddCreditsNudgeCreditType; pub use client::Client; pub use client::RequestError; pub use types::CodeTaskDetailsResponse; diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 32fb25ad3d..7ed8262fdb 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -1729,6 +1729,15 @@ mod tests { assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_)))); } + #[test] + fn plugin_marketplace_remove_parses_under_plugin() { + let cli = + MultitoolCli::try_parse_from(["codex", "plugin", "marketplace", "remove", "debug"]) + .expect("parse"); + + assert!(matches!(cli.subcommand, Some(Subcommand::Plugin(_)))); + } + #[test] fn marketplace_no_longer_parses_at_top_level() { let add_result = @@ -1738,6 +1747,10 @@ mod tests { let upgrade_result = MultitoolCli::try_parse_from(["codex", "marketplace", "upgrade", "debug"]); assert!(upgrade_result.is_err()); + + let remove_result = + MultitoolCli::try_parse_from(["codex", "marketplace", "remove", "debug"]); + assert!(remove_result.is_err()); } fn sample_exit_info(conversation_id: Option<&str>, thread_name: Option<&str>) -> AppExitInfo { diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index ce1f99390a..a29f9ba76f 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -5,9 +5,11 @@ use clap::Parser; use codex_core::config::Config; use codex_core::config::find_codex_home; use codex_core::plugins::MarketplaceAddRequest; +use codex_core::plugins::MarketplaceRemoveRequest; use codex_core::plugins::PluginMarketplaceUpgradeOutcome; use codex_core::plugins::PluginsManager; use codex_core::plugins::add_marketplace; +use codex_core::plugins::remove_marketplace; use codex_utils_cli::CliConfigOverrides; #[derive(Debug, Parser)] @@ -23,6 +25,7 @@ pub struct MarketplaceCli { enum MarketplaceSubcommand { Add(AddMarketplaceArgs), Upgrade(UpgradeMarketplaceArgs), + Remove(RemoveMarketplaceArgs), } #[derive(Debug, Parser)] @@ -47,6 +50,12 @@ struct UpgradeMarketplaceArgs { marketplace_name: Option, } +#[derive(Debug, Parser)] +struct RemoveMarketplaceArgs { + /// Configured marketplace name to remove. + marketplace_name: String, +} + impl MarketplaceCli { pub async fn run(self) -> Result<()> { let MarketplaceCli { @@ -61,6 +70,7 @@ impl MarketplaceCli { match subcommand { MarketplaceSubcommand::Add(args) => run_add(args).await?, MarketplaceSubcommand::Upgrade(args) => run_upgrade(overrides, args).await?, + MarketplaceSubcommand::Remove(args) => run_remove(args).await?, } Ok(()) @@ -120,6 +130,26 @@ async fn run_upgrade( print_upgrade_outcome(&outcome, marketplace_name.as_deref()) } +async fn run_remove(args: RemoveMarketplaceArgs) -> Result<()> { + let RemoveMarketplaceArgs { marketplace_name } = args; + let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?; + let outcome = remove_marketplace( + codex_home.to_path_buf(), + MarketplaceRemoveRequest { marketplace_name }, + ) + .await?; + + println!("Removed marketplace `{}`.", outcome.marketplace_name); + if let Some(installed_root) = outcome.removed_installed_root { + println!( + "Removed installed marketplace root: {}", + installed_root.as_path().display() + ); + } + + Ok(()) +} + fn print_upgrade_outcome( outcome: &PluginMarketplaceUpgradeOutcome, marketplace_name: Option<&str>, @@ -201,4 +231,10 @@ mod tests { let upgrade_one = UpgradeMarketplaceArgs::try_parse_from(["upgrade", "debug"]).unwrap(); assert_eq!(upgrade_one.marketplace_name.as_deref(), Some("debug")); } + + #[test] + fn remove_subcommand_parses_marketplace_name() { + let remove = RemoveMarketplaceArgs::try_parse_from(["remove", "debug"]).unwrap(); + assert_eq!(remove.marketplace_name, "debug"); + } } diff --git a/codex-rs/cli/tests/marketplace_remove.rs b/codex-rs/cli/tests/marketplace_remove.rs new file mode 100644 index 0000000000..06e213bae6 --- /dev/null +++ b/codex-rs/cli/tests/marketplace_remove.rs @@ -0,0 +1,70 @@ +use anyhow::Result; +use codex_config::MarketplaceConfigUpdate; +use codex_config::record_user_marketplace; +use codex_core::plugins::marketplace_install_root; +use predicates::str::contains; +use std::path::Path; +use tempfile::TempDir; + +fn codex_command(codex_home: &Path) -> Result { + let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?); + cmd.env("CODEX_HOME", codex_home); + Ok(cmd) +} + +fn configured_marketplace_update() -> MarketplaceConfigUpdate<'static> { + MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + } +} + +fn write_installed_marketplace(codex_home: &Path, marketplace_name: &str) -> Result<()> { + let root = marketplace_install_root(codex_home).join(marketplace_name); + std::fs::create_dir_all(root.join(".agents/plugins"))?; + std::fs::write(root.join(".agents/plugins/marketplace.json"), "{}")?; + std::fs::write(root.join("marker.txt"), "installed")?; + Ok(()) +} + +#[tokio::test] +async fn marketplace_remove_deletes_config_and_installed_root() -> Result<()> { + let codex_home = TempDir::new()?; + record_user_marketplace(codex_home.path(), "debug", &configured_marketplace_update())?; + write_installed_marketplace(codex_home.path(), "debug")?; + + codex_command(codex_home.path())? + .args(["plugin", "marketplace", "remove", "debug"]) + .assert() + .success() + .stdout(contains("Removed marketplace `debug`.")); + + let config_path = codex_home.path().join("config.toml"); + let config = std::fs::read_to_string(config_path)?; + assert!(!config.contains("[marketplaces.debug]")); + assert!( + !marketplace_install_root(codex_home.path()) + .join("debug") + .exists() + ); + Ok(()) +} + +#[tokio::test] +async fn marketplace_remove_rejects_unknown_marketplace() -> Result<()> { + let codex_home = TempDir::new()?; + + codex_command(codex_home.path())? + .args(["plugin", "marketplace", "remove", "debug"]) + .assert() + .failure() + .stderr(contains( + "marketplace `debug` is not configured or installed", + )); + + Ok(()) +} diff --git a/codex-rs/cli/tests/mcp_list.rs b/codex-rs/cli/tests/mcp_list.rs index d41a3cc62a..bed3505985 100644 --- a/codex-rs/cli/tests/mcp_list.rs +++ b/codex-rs/cli/tests/mcp_list.rs @@ -55,7 +55,7 @@ async fn list_and_get_render_expected_output() -> Result<()> { .expect("docs server should exist after add"); match &mut docs_entry.transport { McpServerTransportConfig::Stdio { env_vars, .. } => { - *env_vars = vec!["APP_TOKEN".to_string(), "WORKSPACE_ID".to_string()]; + *env_vars = vec!["APP_TOKEN".into(), "WORKSPACE_ID".into()]; } other => panic!("unexpected transport: {other:?}"), } diff --git a/codex-rs/code-mode/src/description.rs b/codex-rs/code-mode/src/description.rs index 7f5e47fa25..4c5eb6fbdc 100644 --- a/codex-rs/code-mode/src/description.rs +++ b/codex-rs/code-mode/src/description.rs @@ -26,7 +26,7 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co - Global helpers: - `exit()`: Immediately ends the current script successfully (like an early return from the top level). - `text(value: string | number | boolean | undefined | null)`: Appends a text item. Non-string values are stringified with `JSON.stringify(...)` when possible. -- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request original detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. +- `image(imageUrlOrItem: string | { image_url: string; detail?: "auto" | "low" | "high" | "original" | null } | ImageContent, detail?: "auto" | "low" | "high" | "original" | null)`: Appends an image item. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. To forward an MCP tool image, pass an individual `ImageContent` block from `result.content`, for example `image(result.content[0])`. MCP image blocks may request detail with `_meta: { "codex/imageDetail": "original" }`. When provided, the second `detail` argument overrides any detail embedded in the first argument. - `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. - `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. - `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`. diff --git a/codex-rs/code-mode/src/lib.rs b/codex-rs/code-mode/src/lib.rs index 880e84ef4a..bb27d99960 100644 --- a/codex-rs/code-mode/src/lib.rs +++ b/codex-rs/code-mode/src/lib.rs @@ -15,6 +15,7 @@ pub use description::normalize_code_mode_identifier; pub use description::parse_exec_source; pub use description::render_code_mode_sample; pub use description::render_json_schema_to_typescript; +pub use response::DEFAULT_IMAGE_DETAIL; pub use response::FunctionCallOutputContentItem; pub use response::ImageDetail; pub use runtime::DEFAULT_EXEC_YIELD_TIME_MS; diff --git a/codex-rs/code-mode/src/response.rs b/codex-rs/code-mode/src/response.rs index 43579fac85..0ac3a03770 100644 --- a/codex-rs/code-mode/src/response.rs +++ b/codex-rs/code-mode/src/response.rs @@ -10,6 +10,8 @@ pub enum ImageDetail { Original, } +pub const DEFAULT_IMAGE_DETAIL: ImageDetail = ImageDetail::High; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum FunctionCallOutputContentItem { diff --git a/codex-rs/code-mode/src/runtime/value.rs b/codex-rs/code-mode/src/runtime/value.rs index 5c63434f4f..8d76a832d3 100644 --- a/codex-rs/code-mode/src/runtime/value.rs +++ b/codex-rs/code-mode/src/runtime/value.rs @@ -1,5 +1,6 @@ use serde_json::Value as JsonValue; +use crate::response::DEFAULT_IMAGE_DETAIL; use crate::response::FunctionCallOutputContentItem; use crate::response::ImageDetail; @@ -81,7 +82,7 @@ pub(super) fn normalize_output_image( } }) } - None => None, + None => Some(DEFAULT_IMAGE_DETAIL), }; Ok(FunctionCallOutputContentItem::InputImage { image_url, detail }) @@ -159,7 +160,7 @@ fn parse_mcp_output_image( .and_then(JsonValue::as_object) .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(JsonValue::as_str) - .filter(|detail| *detail == "original") + .filter(|detail| matches!(*detail, "auto" | "low" | "high" | "original")) .map(str::to_string); Ok((image_url, detail)) } diff --git a/codex-rs/code-mode/src/service.rs b/codex-rs/code-mode/src/service.rs index 79ca010c1a..4a46d36b41 100644 --- a/codex-rs/code-mode/src/service.rs +++ b/codex-rs/code-mode/src/service.rs @@ -669,7 +669,7 @@ text(JSON.stringify(returnsUndefined)); }, FunctionCallOutputContentItem::InputImage { image_url: "https://example.com/image.jpg".to_string(), - detail: None, + detail: Some(crate::DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "[true,true,true]".to_string(), diff --git a/codex-rs/codex-api/tests/models_integration.rs b/codex-rs/codex-api/tests/models_integration.rs index 96de0b2cec..9f95c9441f 100644 --- a/codex-rs/codex-api/tests/models_integration.rs +++ b/codex-rs/codex-api/tests/models_integration.rs @@ -89,6 +89,7 @@ async fn models_client_hits_models_endpoint() { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/codex-mcp/Cargo.toml b/codex-rs/codex-mcp/Cargo.toml index adc38d4093..0aec1f3aaf 100644 --- a/codex-rs/codex-mcp/Cargo.toml +++ b/codex-rs/codex-mcp/Cargo.toml @@ -16,6 +16,7 @@ anyhow = { workspace = true } async-channel = { workspace = true } codex-async-utils = { workspace = true } codex-config = { workspace = true } +codex-exec-server = { workspace = true } codex-login = { workspace = true } codex-otel = { workspace = true } codex-plugin = { workspace = true } diff --git a/codex-rs/codex-mcp/src/lib.rs b/codex-rs/codex-mcp/src/lib.rs index ed0d9b4122..766465f052 100644 --- a/codex-rs/codex-mcp/src/lib.rs +++ b/codex-rs/codex-mcp/src/lib.rs @@ -38,6 +38,7 @@ pub use mcp_connection_manager::CodexAppsToolsCacheKey; pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT; pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY; pub use mcp_connection_manager::McpConnectionManager; +pub use mcp_connection_manager::McpRuntimeEnvironment; pub use mcp_connection_manager::SandboxState; pub use mcp_connection_manager::ToolInfo; pub use mcp_connection_manager::codex_apps_tools_cache_key; diff --git a/codex-rs/codex-mcp/src/mcp/mod.rs b/codex-rs/codex-mcp/src/mcp/mod.rs index 1dc9db0789..448259d5b7 100644 --- a/codex-rs/codex-mcp/src/mcp/mod.rs +++ b/codex-rs/codex-mcp/src/mcp/mod.rs @@ -35,6 +35,7 @@ use codex_protocol::protocol::SandboxPolicy; use serde_json::Value; use crate::mcp_connection_manager::McpConnectionManager; +use crate::mcp_connection_manager::McpRuntimeEnvironment; use crate::mcp_connection_manager::codex_apps_tools_cache_key; pub type McpManager = McpConnectionManager; @@ -321,14 +322,23 @@ pub async fn collect_mcp_snapshot( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, + runtime_environment: McpRuntimeEnvironment, ) -> McpListToolsResponseEvent { - collect_mcp_snapshot_with_detail(config, auth, submit_id, McpSnapshotDetail::Full).await + collect_mcp_snapshot_with_detail( + config, + auth, + submit_id, + runtime_environment, + McpSnapshotDetail::Full, + ) + .await } pub async fn collect_mcp_snapshot_with_detail( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, + runtime_environment: McpRuntimeEnvironment, detail: McpSnapshotDetail, ) -> McpListToolsResponseEvent { let mcp_servers = effective_mcp_servers(config, auth); @@ -356,6 +366,7 @@ pub async fn collect_mcp_snapshot_with_detail( submit_id, tx_event, SandboxPolicy::new_read_only_policy(), + runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, @@ -386,15 +397,23 @@ pub async fn collect_mcp_server_status_snapshot( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, + runtime_environment: McpRuntimeEnvironment, ) -> McpServerStatusSnapshot { - collect_mcp_server_status_snapshot_with_detail(config, auth, submit_id, McpSnapshotDetail::Full) - .await + collect_mcp_server_status_snapshot_with_detail( + config, + auth, + submit_id, + runtime_environment, + McpSnapshotDetail::Full, + ) + .await } pub async fn collect_mcp_server_status_snapshot_with_detail( config: &McpConfig, auth: Option<&CodexAuth>, submit_id: String, + runtime_environment: McpRuntimeEnvironment, detail: McpSnapshotDetail, ) -> McpServerStatusSnapshot { let mcp_servers = effective_mcp_servers(config, auth); @@ -422,6 +441,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail( submit_id, tx_event, SandboxPolicy::new_read_only_policy(), + runtime_environment, config.codex_home.clone(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, diff --git a/codex-rs/codex-mcp/src/mcp_connection_manager.rs b/codex-rs/codex-mcp/src/mcp_connection_manager.rs index 7a290973f1..10c48e040e 100644 --- a/codex-rs/codex-mcp/src/mcp_connection_manager.rs +++ b/codex-rs/codex-mcp/src/mcp_connection_manager.rs @@ -36,6 +36,7 @@ use codex_async_utils::CancelErr; use codex_async_utils::OrCancelExt; use codex_config::Constrained; use codex_config::types::OAuthCredentialsStoreMode; +use codex_exec_server::Environment; use codex_protocol::ToolName; use codex_protocol::approvals::ElicitationRequest; use codex_protocol::approvals::ElicitationRequestEvent; @@ -50,6 +51,7 @@ use codex_protocol::protocol::McpStartupStatus; use codex_protocol::protocol::McpStartupUpdateEvent; use codex_protocol::protocol::SandboxPolicy; use codex_rmcp_client::ElicitationResponse; +use codex_rmcp_client::ExecutorStdioServerLauncher; use codex_rmcp_client::LocalStdioServerLauncher; use codex_rmcp_client::RmcpClient; use codex_rmcp_client::SendElicitation; @@ -493,6 +495,7 @@ impl AsyncManagedClient { elicitation_requests: ElicitationRequestManager, codex_apps_tools_cache_context: Option, tool_plugin_provenance: Arc, + runtime_environment: McpRuntimeEnvironment, ) -> Self { let tool_filter = ToolFilter::from_config(&config); let startup_snapshot = load_startup_cached_codex_apps_tools_snapshot( @@ -509,8 +512,15 @@ impl AsyncManagedClient { return Err(error.into()); } - let client = - Arc::new(make_rmcp_client(&server_name, config.transport, store_mode).await?); + let client = Arc::new( + make_rmcp_client( + &server_name, + config.clone(), + store_mode, + runtime_environment, + ) + .await?, + ); match start_server_task( server_name, client, @@ -650,6 +660,37 @@ pub struct McpConnectionManager { elicitation_requests: ElicitationRequestManager, } +/// Runtime placement information used when starting MCP server transports. +/// +/// `McpConfig` describes what servers exist. This value describes where those +/// servers should run for the current caller. Keep it explicit at manager +/// construction time so status/snapshot paths and real sessions make the same +/// local-vs-remote decision. `fallback_cwd` is not a per-server override; it is +/// used only when an executor-backed stdio server omits `cwd` and the executor +/// API still needs a concrete process working directory. +#[derive(Clone)] +pub struct McpRuntimeEnvironment { + environment: Arc, + fallback_cwd: PathBuf, +} + +impl McpRuntimeEnvironment { + pub fn new(environment: Arc, fallback_cwd: PathBuf) -> Self { + Self { + environment, + fallback_cwd, + } + } + + fn environment(&self) -> Arc { + Arc::clone(&self.environment) + } + + fn fallback_cwd(&self) -> PathBuf { + self.fallback_cwd.clone() + } +} + impl McpConnectionManager { pub fn configured_servers(&self, config: &McpConfig) -> HashMap { configured_mcp_servers(config) @@ -710,6 +751,7 @@ impl McpConnectionManager { submit_id: String, tx_event: Sender, initial_sandbox_policy: SandboxPolicy, + runtime_environment: McpRuntimeEnvironment, codex_home: PathBuf, codex_apps_tools_cache_key: CodexAppsToolsCacheKey, tool_plugin_provenance: ToolPluginProvenance, @@ -754,6 +796,7 @@ impl McpConnectionManager { elicitation_requests.clone(), codex_apps_tools_cache_context, Arc::clone(&tool_plugin_provenance), + runtime_environment.clone(), ); clients.insert(server_name.clone(), async_managed_client.clone()); let tx_event = tx_event.clone(); @@ -1484,9 +1527,25 @@ struct StartServerTaskParams { async fn make_rmcp_client( server_name: &str, - transport: McpServerTransportConfig, + config: McpServerConfig, store_mode: OAuthCredentialsStoreMode, + runtime_environment: McpRuntimeEnvironment, ) -> Result { + let McpServerConfig { + transport, + experimental_environment, + .. + } = config; + let remote_environment = match experimental_environment.as_deref() { + None | Some("local") => false, + Some("remote") => true, + Some(environment) => { + return Err(StartupOutcomeError::from(anyhow!( + "unsupported experimental_environment `{environment}` for MCP server `{server_name}`" + ))); + } + }; + match transport { McpServerTransportConfig::Stdio { command, @@ -1502,7 +1561,24 @@ async fn make_rmcp_client( .map(|(key, value)| (key.into(), value.into())) .collect::>() }); - let launcher = Arc::new(LocalStdioServerLauncher) as Arc; + let launcher = if remote_environment { + let exec_environment = runtime_environment.environment(); + if !exec_environment.is_remote() { + return Err(StartupOutcomeError::from(anyhow!( + "remote MCP server `{server_name}` requires a remote executor environment" + ))); + } + Arc::new(ExecutorStdioServerLauncher::new( + exec_environment.get_exec_backend(), + runtime_environment.fallback_cwd(), + )) + } else { + Arc::new(LocalStdioServerLauncher) as Arc + }; + + // `RmcpClient` always sees a launched MCP stdio server. The + // launcher hides whether that means a local child process or an + // executor process whose stdin/stdout bytes cross the process API. RmcpClient::new_stdio_client(command_os, args_os, env_os, &env_vars, cwd, launcher) .await .map_err(|err| StartupOutcomeError::from(anyhow!(err))) @@ -1513,6 +1589,23 @@ async fn make_rmcp_client( env_http_headers, bearer_token_env_var, } => { + if remote_environment { + if !runtime_environment.environment().is_remote() { + return Err(StartupOutcomeError::from(anyhow!( + "remote MCP server `{server_name}` requires a remote executor environment" + ))); + } + return Err(StartupOutcomeError::from(anyhow!( + // Remote HTTP needs the future low-level executor + // `network/request` API so reqwest runs on the executor side. + // Do not fall back to local HTTP here; the config explicitly + // asked for remote placement. + "remote streamable HTTP MCP server `{server_name}` is not implemented yet" + ))); + } + + // Local streamable HTTP remains the existing reqwest path from + // the orchestrator process. let resolved_bearer_token = match resolve_bearer_token(server_name, bearer_token_env_var.as_deref()) { Ok(token) => token, diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index 83fe6c88e7..6556ca73e3 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -684,25 +684,21 @@ impl ConfigToml { resolved_cwd: &Path, repo_root: Option<&Path>, ) -> Option { - let projects = self.projects.clone().unwrap_or_default(); + let projects = self.projects.as_ref()?; - let resolved_cwd_key = project_trust_key(resolved_cwd); - let resolved_cwd_raw_key = resolved_cwd.to_string_lossy().to_string(); - if let Some(project_config) = projects - .get(&resolved_cwd_key) - .or_else(|| projects.get(&resolved_cwd_raw_key)) - { - return Some(project_config.clone()); + for normalized_cwd in normalized_project_lookup_keys(resolved_cwd) { + if let Some(project_config) = project_config_for_lookup_key(projects, &normalized_cwd) { + return Some(project_config); + } } if let Some(repo_root) = repo_root { - let repo_root_key = project_trust_key(repo_root); - let repo_root_raw_key = repo_root.to_string_lossy().to_string(); - if let Some(project_config_for_root) = projects - .get(&repo_root_key) - .or_else(|| projects.get(&repo_root_raw_key)) - { - return Some(project_config_for_root.clone()); + for normalized_repo_root in normalized_project_lookup_keys(repo_root) { + if let Some(project_config_for_root) = + project_config_for_lookup_key(projects, &normalized_repo_root) + { + return Some(project_config_for_root); + } } } @@ -734,11 +730,45 @@ impl ConfigToml { /// Canonicalize the path and convert it to a string to be used as a key in the /// projects trust map. On Windows, strips UNC, when possible, to try to ensure /// that different paths that point to the same location have the same key. -fn project_trust_key(project_path: &Path) -> String { - normalize_for_path_comparison(project_path) - .unwrap_or_else(|_| project_path.to_path_buf()) - .to_string_lossy() - .to_string() +fn normalized_project_lookup_keys(path: &Path) -> Vec { + let normalized_path = normalize_project_lookup_key(path.to_string_lossy().to_string()); + let normalized_canonical_path = normalize_project_lookup_key( + normalize_for_path_comparison(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .to_string(), + ); + if normalized_path == normalized_canonical_path { + vec![normalized_canonical_path] + } else { + vec![normalized_canonical_path, normalized_path] + } +} + +fn normalize_project_lookup_key(key: String) -> String { + if cfg!(windows) { + key.to_ascii_lowercase() + } else { + key + } +} + +fn project_config_for_lookup_key( + projects: &HashMap, + lookup_key: &str, +) -> Option { + if let Some(project_config) = projects.get(lookup_key) { + return Some(project_config.clone()); + } + + let mut normalized_matches: Vec<_> = projects + .iter() + .filter(|(key, _)| normalize_project_lookup_key((*key).clone()) == lookup_key) + .collect(); + normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches + .first() + .map(|(_, project_config)| (**project_config).clone()) } pub fn validate_reserved_model_provider_ids( diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 18779f9809..a26d5afe0a 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -63,12 +63,16 @@ pub use diagnostics::format_config_error_with_source; pub use diagnostics::io_error_from_config_error; pub use fingerprint::version_for_toml; pub use marketplace_edit::MarketplaceConfigUpdate; +pub use marketplace_edit::RemoveMarketplaceConfigOutcome; pub use marketplace_edit::record_user_marketplace; +pub use marketplace_edit::remove_user_marketplace; +pub use marketplace_edit::remove_user_marketplace_config; pub use mcp_edit::ConfigEditsBuilder; pub use mcp_edit::load_global_mcp_servers; pub use mcp_types::AppToolApproval; pub use mcp_types::McpServerConfig; pub use mcp_types::McpServerDisabledReason; +pub use mcp_types::McpServerEnvVar; pub use mcp_types::McpServerToolConfig; pub use mcp_types::McpServerTransportConfig; pub use mcp_types::RawMcpServerConfig; diff --git a/codex-rs/config/src/marketplace_edit.rs b/codex-rs/config/src/marketplace_edit.rs index e20a75a02e..2aad2486eb 100644 --- a/codex-rs/config/src/marketplace_edit.rs +++ b/codex-rs/config/src/marketplace_edit.rs @@ -19,6 +19,13 @@ pub struct MarketplaceConfigUpdate<'a> { pub sparse_paths: &'a [String], } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RemoveMarketplaceConfigOutcome { + Removed, + NotFound, + NameCaseMismatch { configured_name: String }, +} + pub fn record_user_marketplace( codex_home: &Path, marketplace_name: &str, @@ -31,6 +38,36 @@ pub fn record_user_marketplace( fs::write(config_path, doc.to_string()) } +pub fn remove_user_marketplace(codex_home: &Path, marketplace_name: &str) -> std::io::Result { + let outcome = remove_user_marketplace_config(codex_home, marketplace_name)?; + Ok(outcome == RemoveMarketplaceConfigOutcome::Removed) +} + +pub fn remove_user_marketplace_config( + codex_home: &Path, + marketplace_name: &str, +) -> std::io::Result { + let config_path = codex_home.join(CONFIG_TOML_FILE); + let mut doc = match fs::read_to_string(&config_path) { + Ok(raw) => raw + .parse::() + .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?, + Err(err) if err.kind() == ErrorKind::NotFound => { + return Ok(RemoveMarketplaceConfigOutcome::NotFound); + } + Err(err) => return Err(err), + }; + + let outcome = remove_marketplace(&mut doc, marketplace_name); + if outcome != RemoveMarketplaceConfigOutcome::Removed { + return Ok(outcome); + } + + fs::create_dir_all(codex_home)?; + fs::write(config_path, doc.to_string())?; + Ok(RemoveMarketplaceConfigOutcome::Removed) +} + fn read_or_create_document(config_path: &Path) -> std::io::Result { match fs::read_to_string(config_path) { Ok(raw) => raw @@ -80,8 +117,160 @@ fn upsert_marketplace( marketplaces.insert(marketplace_name, TomlItem::Table(entry)); } +fn remove_marketplace( + doc: &mut DocumentMut, + marketplace_name: &str, +) -> RemoveMarketplaceConfigOutcome { + let root = doc.as_table_mut(); + let Some(marketplaces_item) = root.get_mut("marketplaces") else { + return RemoveMarketplaceConfigOutcome::NotFound; + }; + + let mut remove_marketplaces = false; + let outcome = match marketplaces_item { + TomlItem::Table(marketplaces) => { + let outcome = if marketplaces.remove(marketplace_name).is_some() { + RemoveMarketplaceConfigOutcome::Removed + } else if let Some(configured_name) = + case_mismatched_key(marketplaces.iter().map(|(key, _)| key), marketplace_name) + { + RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name } + } else { + RemoveMarketplaceConfigOutcome::NotFound + }; + remove_marketplaces = marketplaces.is_empty(); + outcome + } + TomlItem::Value(value) => { + let Some(marketplaces) = value.as_inline_table_mut() else { + return RemoveMarketplaceConfigOutcome::NotFound; + }; + let outcome = if marketplaces.remove(marketplace_name).is_some() { + RemoveMarketplaceConfigOutcome::Removed + } else if let Some(configured_name) = + case_mismatched_key(marketplaces.iter().map(|(key, _)| key), marketplace_name) + { + RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name } + } else { + RemoveMarketplaceConfigOutcome::NotFound + }; + remove_marketplaces = marketplaces.is_empty(); + outcome + } + _ => RemoveMarketplaceConfigOutcome::NotFound, + }; + + if outcome == RemoveMarketplaceConfigOutcome::Removed && remove_marketplaces { + root.remove("marketplaces"); + } + outcome +} + +fn case_mismatched_key<'a>( + mut keys: impl Iterator, + requested_name: &str, +) -> Option { + keys.find(|key| *key != requested_name && key.eq_ignore_ascii_case(requested_name)) + .map(str::to_string) +} + fn new_implicit_table() -> TomlTable { let mut table = TomlTable::new(); table.set_implicit(true); table } + +#[cfg(test)] +mod tests { + use super::*; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + #[test] + fn remove_user_marketplace_removes_requested_entry() { + let codex_home = TempDir::new().unwrap(); + let update = MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + }; + record_user_marketplace(codex_home.path(), "debug", &update).unwrap(); + record_user_marketplace(codex_home.path(), "other", &update).unwrap(); + + let removed = remove_user_marketplace(codex_home.path(), "debug").unwrap(); + + assert!(removed); + let config: toml::Value = + toml::from_str(&fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).unwrap()) + .unwrap(); + let marketplaces = config + .get("marketplaces") + .and_then(toml::Value::as_table) + .unwrap(); + assert_eq!(marketplaces.len(), 1); + assert!(marketplaces.contains_key("other")); + } + + #[test] + fn remove_user_marketplace_returns_false_when_missing() { + let codex_home = TempDir::new().unwrap(); + + let removed = remove_user_marketplace(codex_home.path(), "debug").unwrap(); + + assert!(!removed); + } + + #[test] + fn remove_user_marketplace_config_reports_case_mismatch() { + let codex_home = TempDir::new().unwrap(); + let update = MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + }; + record_user_marketplace(codex_home.path(), "debug", &update).unwrap(); + + let outcome = remove_user_marketplace_config(codex_home.path(), "Debug").unwrap(); + + assert_eq!( + outcome, + RemoveMarketplaceConfigOutcome::NameCaseMismatch { + configured_name: "debug".to_string() + } + ); + } + + #[test] + fn remove_user_marketplace_config_removes_inline_table_entry() { + let codex_home = TempDir::new().unwrap(); + fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#" +marketplaces = { + debug = { source_type = "git", source = "https://github.com/owner/repo.git" }, + other = { source_type = "local", source = "/tmp/marketplace" }, +} +"#, + ) + .unwrap(); + + let outcome = remove_user_marketplace_config(codex_home.path(), "debug").unwrap(); + + assert_eq!(outcome, RemoveMarketplaceConfigOutcome::Removed); + let config: toml::Value = + toml::from_str(&fs::read_to_string(codex_home.path().join(CONFIG_TOML_FILE)).unwrap()) + .unwrap(); + let marketplaces = config + .get("marketplaces") + .and_then(toml::Value::as_table) + .unwrap(); + assert_eq!(marketplaces.len(), 1); + assert!(marketplaces.contains_key("other")); + } +} diff --git a/codex-rs/config/src/mcp_edit.rs b/codex-rs/config/src/mcp_edit.rs index c4bb38c543..f5881a1257 100644 --- a/codex-rs/config/src/mcp_edit.rs +++ b/codex-rs/config/src/mcp_edit.rs @@ -14,6 +14,7 @@ use toml_edit::value; use crate::AppToolApproval; use crate::CONFIG_TOML_FILE; use crate::McpServerConfig; +use crate::McpServerEnvVar; use crate::McpServerTransportConfig; pub async fn load_global_mcp_servers( @@ -142,7 +143,7 @@ fn serialize_mcp_server(config: &McpServerConfig) -> TomlItem { entry["env"] = table_from_pairs(env.iter()); } if !env_vars.is_empty() { - entry["env_vars"] = array_from_strings(env_vars); + entry["env_vars"] = array_from_env_vars(env_vars); } if let Some(cwd) = cwd { entry["cwd"] = value(cwd.to_string_lossy().to_string()); @@ -247,6 +248,24 @@ fn array_from_strings(values: &[String]) -> TomlItem { TomlItem::Value(array.into()) } +fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem { + let mut array = toml_edit::Array::new(); + for env_var in env_vars { + match env_var { + McpServerEnvVar::Name(name) => array.push(name.clone()), + McpServerEnvVar::Config { name, source } => { + let mut table = toml_edit::InlineTable::new(); + table.insert("name", name.clone().into()); + if let Some(source) = source { + table.insert("source", source.clone().into()); + } + array.push(table); + } + } + } + TomlItem::Value(array.into()) +} + fn table_from_pairs<'a, I>(pairs: I) -> TomlItem where I: IntoIterator, diff --git a/codex-rs/config/src/mcp_types.rs b/codex-rs/config/src/mcp_types.rs index 75b68c3f94..a276cd7070 100644 --- a/codex-rs/config/src/mcp_types.rs +++ b/codex-rs/config/src/mcp_types.rs @@ -56,6 +56,64 @@ pub struct McpServerToolConfig { pub approval_mode: Option, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)] +#[serde(untagged, deny_unknown_fields)] +pub enum McpServerEnvVar { + Name(String), + Config { + name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + source: Option, + }, +} + +impl McpServerEnvVar { + pub fn name(&self) -> &str { + match self { + McpServerEnvVar::Name(name) => name, + McpServerEnvVar::Config { name, .. } => name, + } + } + + pub fn source(&self) -> Option<&str> { + match self { + McpServerEnvVar::Name(_) => None, + McpServerEnvVar::Config { source, .. } => source.as_deref(), + } + } + + pub fn is_remote_source(&self) -> bool { + self.source() == Some("remote") + } + + pub fn validate_source(&self) -> Result<(), String> { + match self.source() { + None | Some("local") | Some("remote") => Ok(()), + Some(source) => Err(format!( + "unsupported env_vars source `{source}`; expected `local` or `remote`" + )), + } + } +} + +impl From for McpServerEnvVar { + fn from(value: String) -> Self { + Self::Name(value) + } +} + +impl From<&str> for McpServerEnvVar { + fn from(value: &str) -> Self { + Self::Name(value.to_string()) + } +} + +impl AsRef for McpServerEnvVar { + fn as_ref(&self) -> &str { + self.name() + } +} + #[derive(Serialize, Debug, Clone, PartialEq)] pub struct McpServerConfig { #[serde(flatten)] @@ -133,7 +191,7 @@ pub struct RawMcpServerConfig { #[serde(default)] pub env: Option>, #[serde(default)] - pub env_vars: Option>, + pub env_vars: Option>, #[serde(default)] pub cwd: Option, pub http_headers: Option>, @@ -235,11 +293,15 @@ impl TryFrom for McpServerConfig { throw_if_set("stdio", "http_headers", http_headers.as_ref())?; throw_if_set("stdio", "env_http_headers", env_http_headers.as_ref())?; throw_if_set("stdio", "oauth_resource", oauth_resource.as_ref())?; + let env_vars = env_vars.unwrap_or_default(); + for env_var in &env_vars { + env_var.validate_source()?; + } McpServerTransportConfig::Stdio { command, args: args.unwrap_or_default(), env, - env_vars: env_vars.unwrap_or_default(), + env_vars, cwd, } } else if let Some(url) = url { @@ -303,7 +365,7 @@ pub enum McpServerTransportConfig { #[serde(default, skip_serializing_if = "Option::is_none")] env: Option>, #[serde(default, skip_serializing_if = "Vec::is_empty")] - env_vars: Vec, + env_vars: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] cwd: Option, }, diff --git a/codex-rs/config/src/mcp_types_tests.rs b/codex-rs/config/src/mcp_types_tests.rs index dff4a6bbf5..50f705d0f8 100644 --- a/codex-rs/config/src/mcp_types_tests.rs +++ b/codex-rs/config/src/mcp_types_tests.rs @@ -91,12 +91,65 @@ fn deserialize_stdio_command_server_config_with_env_vars() { command: "echo".to_string(), args: vec![], env: None, - env_vars: vec!["FOO".to_string(), "BAR".to_string()], + env_vars: vec!["FOO".into(), "BAR".into()], cwd: None, } ); } +#[test] +fn deserialize_stdio_command_server_config_with_env_var_sources() { + let cfg: McpServerConfig = toml::from_str( + r#" + command = "echo" + env_vars = [ + "LEGACY_TOKEN", + { name = "LOCAL_TOKEN", source = "local" }, + { name = "REMOTE_TOKEN", source = "remote" }, + ] + "#, + ) + .expect("should deserialize command config with sourced env_vars"); + + assert_eq!( + cfg.transport, + McpServerTransportConfig::Stdio { + command: "echo".to_string(), + args: vec![], + env: None, + env_vars: vec![ + McpServerEnvVar::Name("LEGACY_TOKEN".to_string()), + McpServerEnvVar::Config { + name: "LOCAL_TOKEN".to_string(), + source: Some("local".to_string()), + }, + McpServerEnvVar::Config { + name: "REMOTE_TOKEN".to_string(), + source: Some("remote".to_string()), + }, + ], + cwd: None, + } + ); +} + +#[test] +fn deserialize_stdio_command_server_config_rejects_unknown_env_var_source() { + let err = toml::from_str::( + r#" + command = "echo" + env_vars = [{ name = "TOKEN", source = "elsewhere" }] + "#, + ) + .expect_err("unsupported env var source should be rejected"); + + assert!( + err.to_string() + .contains("unsupported env_vars source `elsewhere`"), + "unexpected error: {err}" + ); +} + #[test] fn deserialize_stdio_command_server_config_with_cwd() { let cfg: McpServerConfig = toml::from_str( diff --git a/codex-rs/config/src/types.rs b/codex-rs/config/src/types.rs index 243bc0d6d6..50a24db534 100644 --- a/codex-rs/config/src/types.rs +++ b/codex-rs/config/src/types.rs @@ -6,6 +6,7 @@ pub use crate::mcp_types::AppToolApproval; pub use crate::mcp_types::McpServerConfig; pub use crate::mcp_types::McpServerDisabledReason; +pub use crate::mcp_types::McpServerEnvVar; pub use crate::mcp_types::McpServerToolConfig; pub use crate::mcp_types::McpServerTransportConfig; pub use crate::mcp_types::RawMcpServerConfig; @@ -592,7 +593,23 @@ const fn default_true() -> bool { /// Settings for notices we display to users via the tui and app-server clients /// (primarily the Codex IDE extension). NOTE: these are different from /// notifications - notices are warnings, NUX screens, acknowledgements, etc. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] +pub struct ExternalConfigMigrationPrompts { + /// Tracks whether home-level external config migration prompts are hidden. + pub home: Option, + /// Tracks the last time the home-level external config migration prompt was shown. + pub home_last_prompted_at: Option, + /// Tracks which project paths have opted out of external config migration prompts. + #[serde(default)] + pub projects: BTreeMap, + /// Tracks the last time a project-level external config migration prompt was shown. + #[serde(default)] + pub project_last_prompted_at: BTreeMap, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema)] +#[schemars(deny_unknown_fields)] pub struct Notice { /// Tracks whether the user has acknowledged the full access warning prompt. pub hide_full_access_warning: Option, @@ -608,6 +625,9 @@ pub struct Notice { /// Tracks acknowledged model migrations as old->new model slug mappings. #[serde(default)] pub model_migrations: BTreeMap, + /// Tracks scopes where external config migration prompts should be suppressed. + #[serde(default)] + pub external_config_migration_prompts: ExternalConfigMigrationPrompts, } pub use crate::skills_config::BundledSkillsConfig; diff --git a/codex-rs/core-plugins/src/loader.rs b/codex-rs/core-plugins/src/loader.rs index 32b4b5d034..f3e5f8d4f1 100644 --- a/codex-rs/core-plugins/src/loader.rs +++ b/codex-rs/core-plugins/src/loader.rs @@ -129,7 +129,7 @@ pub fn refresh_curated_plugin_cache( plugin_version: &str, configured_curated_plugin_ids: &[PluginId], ) -> Result { - let store = PluginStore::new(codex_home.to_path_buf()); + let store = PluginStore::try_new(codex_home.to_path_buf()).map_err(|err| err.to_string())?; let curated_marketplace_path = AbsolutePathBuf::try_from( codex_home .join(".tmp/plugins") @@ -234,7 +234,7 @@ fn refresh_non_curated_plugin_cache_with_mode( .map(PluginId::as_key) .collect::>(); - let store = PluginStore::new(codex_home.to_path_buf()); + let store = PluginStore::try_new(codex_home.to_path_buf()).map_err(|err| err.to_string())?; let marketplace_outcome = list_marketplaces(additional_roots) .map_err(|err| format!("failed to discover marketplaces for cache refresh: {err}"))?; let mut plugin_sources = HashMap::::new(); @@ -742,7 +742,13 @@ pub async fn installed_plugin_telemetry_metadata( codex_home: &Path, plugin_id: &PluginId, ) -> PluginTelemetryMetadata { - let store = PluginStore::new(codex_home.to_path_buf()); + let store = match PluginStore::try_new(codex_home.to_path_buf()) { + Ok(store) => store, + Err(err) => { + warn!("failed to resolve plugin cache root: {err}"); + return PluginTelemetryMetadata::from_plugin_id(plugin_id); + } + }; let Some(plugin_root) = store.active_plugin_root(plugin_id) else { return PluginTelemetryMetadata::from_plugin_id(plugin_id); }; diff --git a/codex-rs/core-plugins/src/store.rs b/codex-rs/core-plugins/src/store.rs index 4ada6a3d53..757aec8bc5 100644 --- a/codex-rs/core-plugins/src/store.rs +++ b/codex-rs/core-plugins/src/store.rs @@ -28,10 +28,15 @@ pub struct PluginStore { impl PluginStore { pub fn new(codex_home: PathBuf) -> Self { - Self { - root: AbsolutePathBuf::try_from(codex_home.join(PLUGINS_CACHE_DIR)) - .unwrap_or_else(|err| panic!("plugin cache root should be absolute: {err}")), - } + Self::try_new(codex_home) + .unwrap_or_else(|err| panic!("plugin cache root should be absolute: {err}")) + } + + pub fn try_new(codex_home: PathBuf) -> Result { + let root = AbsolutePathBuf::from_absolute_path_checked(codex_home.join(PLUGINS_CACHE_DIR)) + .map_err(|err| PluginStoreError::io("failed to resolve plugin cache root", err))?; + + Ok(Self { root }) } pub fn root(&self) -> &AbsolutePathBuf { @@ -39,22 +44,13 @@ impl PluginStore { } pub fn plugin_base_root(&self, plugin_id: &PluginId) -> AbsolutePathBuf { - AbsolutePathBuf::try_from( - self.root - .as_path() - .join(&plugin_id.marketplace_name) - .join(&plugin_id.plugin_name), - ) - .unwrap_or_else(|err| panic!("plugin cache path should resolve to an absolute path: {err}")) + self.root + .join(&plugin_id.marketplace_name) + .join(&plugin_id.plugin_name) } pub fn plugin_root(&self, plugin_id: &PluginId, plugin_version: &str) -> AbsolutePathBuf { - AbsolutePathBuf::try_from( - self.plugin_base_root(plugin_id) - .as_path() - .join(plugin_version), - ) - .unwrap_or_else(|err| panic!("plugin cache path should resolve to an absolute path: {err}")) + self.plugin_base_root(plugin_id).join(plugin_version) } pub fn active_plugin_version(&self, plugin_id: &PluginId) -> Option { diff --git a/codex-rs/core-plugins/src/store_tests.rs b/codex-rs/core-plugins/src/store_tests.rs index c1d5dd1ab6..45feff61bd 100644 --- a/codex-rs/core-plugins/src/store_tests.rs +++ b/codex-rs/core-plugins/src/store_tests.rs @@ -33,6 +33,18 @@ fn write_plugin(root: &Path, dir_name: &str, manifest_name: &str) { ); } +#[test] +fn try_new_rejects_relative_codex_home() { + let err = PluginStore::try_new(PathBuf::from("relative")) + .expect_err("relative codex home should fail"); + let err = err.to_string().replace('\\', "/"); + + assert_eq!( + err, + "failed to resolve plugin cache root: path is not absolute: relative/plugins/cache" + ); +} + #[test] fn install_copies_plugin_into_default_marketplace() { let tmp = tempdir().unwrap(); diff --git a/codex-rs/core-skills/Cargo.toml b/codex-rs/core-skills/Cargo.toml index bc73909f92..09278c8d0e 100644 --- a/codex-rs/core-skills/Cargo.toml +++ b/codex-rs/core-skills/Cargo.toml @@ -24,6 +24,7 @@ codex-otel = { workspace = true } codex-protocol = { workspace = true } codex-skills = { workspace = true } codex-utils-absolute-path = { workspace = true } +codex-utils-output-truncation = { workspace = true } codex-utils-plugins = { workspace = true } dirs = { workspace = true } dunce = { workspace = true } diff --git a/codex-rs/core-skills/src/lib.rs b/codex-rs/core-skills/src/lib.rs index b967e4dddc..6d3ac7a4fe 100644 --- a/codex-rs/core-skills/src/lib.rs +++ b/codex-rs/core-skills/src/lib.rs @@ -22,4 +22,8 @@ pub use model::SkillLoadOutcome; pub use model::SkillMetadata; pub use model::SkillPolicy; pub use model::filter_skill_load_outcome_for_product; +pub use render::RenderedSkillsSection; +pub use render::SkillMetadataBudget; +pub use render::SkillRenderReport; +pub use render::default_skill_metadata_budget; pub use render::render_skills_section; diff --git a/codex-rs/core-skills/src/render.rs b/codex-rs/core-skills/src/render.rs index 9089a05436..ad674fbffa 100644 --- a/codex-rs/core-skills/src/render.rs +++ b/codex-rs/core-skills/src/render.rs @@ -1,22 +1,104 @@ use crate::model::SkillMetadata; +use codex_otel::SessionTelemetry; +use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC; +use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; +use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; +use codex_protocol::protocol::SkillScope; +use codex_utils_output_truncation::approx_token_count; -pub fn render_skills_section(skills: &[SkillMetadata]) -> Option { +const DEFAULT_SKILL_METADATA_CHAR_BUDGET: usize = 8_000; +const SKILL_METADATA_CONTEXT_WINDOW_PERCENT: usize = 2; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SkillMetadataBudget { + Tokens(usize), + Characters(usize), +} + +impl SkillMetadataBudget { + fn limit(self) -> usize { + match self { + Self::Tokens(limit) | Self::Characters(limit) => limit, + } + } + + fn cost(self, text: &str) -> usize { + match self { + Self::Tokens(_) => approx_token_count(text), + Self::Characters(_) => text.chars().count(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SkillRenderReport { + pub total_count: usize, + pub included_count: usize, + pub omitted_count: usize, +} + +#[derive(Clone, Copy)] +pub enum SkillRenderSideEffects<'a> { + None, + ThreadStart { + session_telemetry: &'a SessionTelemetry, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RenderedSkillsSection { + pub text: String, + pub report: SkillRenderReport, + pub emit_warning: bool, +} + +pub fn default_skill_metadata_budget(context_window: Option) -> SkillMetadataBudget { + context_window + .and_then(|window| usize::try_from(window).ok()) + .filter(|window| *window > 0) + .map(|window| { + SkillMetadataBudget::Tokens( + window + .saturating_mul(SKILL_METADATA_CONTEXT_WINDOW_PERCENT) + .saturating_div(100) + .max(1), + ) + }) + .unwrap_or(SkillMetadataBudget::Characters( + DEFAULT_SKILL_METADATA_CHAR_BUDGET, + )) +} + +pub fn render_skills_section( + skills: &[SkillMetadata], + budget: SkillMetadataBudget, + side_effects: SkillRenderSideEffects<'_>, +) -> Option { if skills.is_empty() { + let _ = record_skill_render_side_effects( + side_effects, + /*total_count*/ 0, + /*included_count*/ 0, + /*truncated*/ false, + ); return None; } + let (skill_lines, report) = render_skill_lines(skills, budget); + let emit_warning = record_skill_render_side_effects( + side_effects, + report.total_count, + report.included_count, + report.omitted_count > 0, + ); let mut lines: Vec = Vec::new(); lines.push("## Skills".to_string()); lines.push("A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.".to_string()); lines.push("### Available skills".to_string()); - - for skill in skills { - let path_str = skill.path_to_skills_md.to_string_lossy().replace('\\', "/"); - let name = skill.name.as_str(); - let description = skill.description.as_str(); - lines.push(format!("- {name}: {description} (file: {path_str})")); + if !skill_lines.is_empty() { + lines.extend(skill_lines); } lines.push("### How to use skills".to_string()); @@ -42,7 +124,189 @@ pub fn render_skills_section(skills: &[SkillMetadata]) -> Option { ); let body = lines.join("\n"); - Some(format!( - "{SKILLS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}" - )) + Some(RenderedSkillsSection { + text: format!("{SKILLS_INSTRUCTIONS_OPEN_TAG}\n{body}\n{SKILLS_INSTRUCTIONS_CLOSE_TAG}"), + report, + emit_warning, + }) +} + +fn record_skill_render_side_effects( + side_effects: SkillRenderSideEffects<'_>, + total_count: usize, + included_count: usize, + truncated: bool, +) -> bool { + match side_effects { + SkillRenderSideEffects::None => false, + SkillRenderSideEffects::ThreadStart { session_telemetry } => { + session_telemetry.histogram( + THREAD_SKILLS_ENABLED_TOTAL_METRIC, + i64::try_from(total_count).unwrap_or(i64::MAX), + &[], + ); + session_telemetry.histogram( + THREAD_SKILLS_KEPT_TOTAL_METRIC, + i64::try_from(included_count).unwrap_or(i64::MAX), + &[], + ); + session_telemetry.histogram( + THREAD_SKILLS_TRUNCATED_METRIC, + if truncated { 1 } else { 0 }, + &[], + ); + truncated + } + } +} + +fn render_skill_lines( + skills: &[SkillMetadata], + budget: SkillMetadataBudget, +) -> (Vec, SkillRenderReport) { + let ordered_skills = ordered_skills_for_budget(skills); + + let mut included = Vec::new(); + let mut used = 0usize; + let mut omitted_count = 0usize; + + for skill in ordered_skills { + let line = render_skill_line(skill); + let line_cost = budget.cost(&format!("{line}\n")); + if used.saturating_add(line_cost) <= budget.limit() { + used = used.saturating_add(line_cost); + included.push(line); + continue; + } + + omitted_count = omitted_count.saturating_add(1); + } + + let report = SkillRenderReport { + total_count: skills.len(), + included_count: included.len(), + omitted_count, + }; + + (included, report) +} + +fn ordered_skills_for_budget(skills: &[SkillMetadata]) -> Vec<&SkillMetadata> { + let mut ordered = skills.iter().collect::>(); + ordered.sort_by(|a, b| { + prompt_scope_rank(a.scope) + .cmp(&prompt_scope_rank(b.scope)) + .then_with(|| a.name.cmp(&b.name)) + .then_with(|| a.path_to_skills_md.cmp(&b.path_to_skills_md)) + }); + ordered +} + +fn prompt_scope_rank(scope: SkillScope) -> u8 { + match scope { + SkillScope::System => 0, + SkillScope::Admin => 1, + SkillScope::Repo => 2, + SkillScope::User => 3, + } +} + +fn render_skill_line(skill: &SkillMetadata) -> String { + let path_str = skill.path_to_skills_md.to_string_lossy().replace('\\', "/"); + let name = skill.name.as_str(); + let description = skill.description.as_str(); + format!("- {name}: {description} (file: {path_str})") +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; + use pretty_assertions::assert_eq; + + fn make_skill(name: &str, scope: SkillScope) -> SkillMetadata { + SkillMetadata { + name: name.to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf(&format!("/tmp/{name}/SKILL.md")).abs(), + scope, + } + } + + #[test] + fn default_budget_uses_two_percent_of_full_context_window() { + assert_eq!( + default_skill_metadata_budget(Some(200_000)), + SkillMetadataBudget::Tokens(4_000) + ); + assert_eq!( + default_skill_metadata_budget(Some(99)), + SkillMetadataBudget::Tokens(1) + ); + } + + #[test] + fn default_budget_falls_back_to_characters_without_context_window() { + assert_eq!( + default_skill_metadata_budget(/*context_window*/ None), + SkillMetadataBudget::Characters(DEFAULT_SKILL_METADATA_CHAR_BUDGET) + ); + assert_eq!( + default_skill_metadata_budget(Some(-1)), + SkillMetadataBudget::Characters(DEFAULT_SKILL_METADATA_CHAR_BUDGET) + ); + } + + #[test] + fn budgeted_rendering_preserves_prompt_priority() { + let system = make_skill("system-skill", SkillScope::System); + let user = make_skill("user-skill", SkillScope::User); + let repo = make_skill("repo-skill", SkillScope::Repo); + let admin = make_skill("admin-skill", SkillScope::Admin); + let system_cost = SkillMetadataBudget::Characters(usize::MAX) + .cost(&format!("{}\n", render_skill_line(&system))); + let admin_cost = SkillMetadataBudget::Characters(usize::MAX) + .cost(&format!("{}\n", render_skill_line(&admin))); + let budget = SkillMetadataBudget::Characters(system_cost + admin_cost); + + let rendered = render_skills_section( + &[system, user, repo, admin], + budget, + SkillRenderSideEffects::None, + ) + .expect("skills should render"); + + assert_eq!(rendered.report.included_count, 2); + assert_eq!(rendered.report.omitted_count, 2); + assert!(!rendered.emit_warning); + assert!(rendered.text.contains("- system-skill:")); + assert!(rendered.text.contains("- admin-skill:")); + assert!(!rendered.text.contains("- repo-skill:")); + assert!(!rendered.text.contains("- user-skill:")); + } + + #[test] + fn budgeted_rendering_keeps_scanning_after_oversized_entry() { + let mut oversized = make_skill("oversized-system-skill", SkillScope::System); + oversized.description = "desc ".repeat(100); + let repo = make_skill("repo-skill", SkillScope::Repo); + let repo_cost = SkillMetadataBudget::Characters(usize::MAX) + .cost(&format!("{}\n", render_skill_line(&repo))); + let budget = SkillMetadataBudget::Characters(repo_cost); + + let rendered = + render_skills_section(&[oversized, repo], budget, SkillRenderSideEffects::None) + .expect("skills render"); + + assert_eq!(rendered.report.included_count, 1); + assert_eq!(rendered.report.omitted_count, 1); + assert!(!rendered.emit_warning); + assert!(!rendered.text.contains("- oversized-system-skill:")); + assert!(rendered.text.contains("- repo-skill:")); + } } diff --git a/codex-rs/core/BUILD.bazel b/codex-rs/core/BUILD.bazel index 434dc1f6a4..dd52bce43d 100644 --- a/codex-rs/core/BUILD.bazel +++ b/codex-rs/core/BUILD.bazel @@ -47,6 +47,10 @@ codex_rust_crate( # succeeds without this workaround. "//:AGENTS.md", ], + test_shard_counts = { + "core-all-test": 8, + "core-unit-tests": 8, + }, test_tags = ["no-sandbox"], unit_test_timeout = "long", extra_binaries = [ diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index e90df599f3..2cfb017d8e 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -392,6 +392,9 @@ "experimental_windows_sandbox": { "type": "boolean" }, + "external_migration": { + "type": "boolean" + }, "fast_mode": { "type": "boolean" }, @@ -639,6 +642,39 @@ }, "type": "object" }, + "ExternalConfigMigrationPrompts": { + "additionalProperties": false, + "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", + "properties": { + "home": { + "description": "Tracks whether home-level external config migration prompts are hidden.", + "type": "boolean" + }, + "home_last_prompted_at": { + "description": "Tracks the last time the home-level external config migration prompt was shown.", + "format": "int64", + "type": "integer" + }, + "project_last_prompted_at": { + "additionalProperties": { + "format": "int64", + "type": "integer" + }, + "default": {}, + "description": "Tracks the last time a project-level external config migration prompt was shown.", + "type": "object" + }, + "projects": { + "additionalProperties": { + "type": "boolean" + }, + "default": {}, + "description": "Tracks which project paths have opted out of external config migration prompts.", + "type": "object" + } + }, + "type": "object" + }, "FeatureToml_for_MultiAgentV2ConfigToml": { "anyOf": [ { @@ -843,6 +879,28 @@ ], "type": "string" }, + "McpServerEnvVar": { + "anyOf": [ + { + "type": "string" + }, + { + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + ] + }, "McpServerToolConfig": { "additionalProperties": false, "description": "Per-tool approval settings for a single MCP server tool.", @@ -1156,8 +1214,22 @@ "type": "object" }, "Notice": { - "description": "Settings for notices we display to users via the tui and app-server clients (primarily the Codex IDE extension). NOTE: these are different from notifications - notices are warnings, NUX screens, acknowledgements, etc.", + "additionalProperties": false, "properties": { + "external_config_migration_prompts": { + "allOf": [ + { + "$ref": "#/definitions/ExternalConfigMigrationPrompts" + } + ], + "default": { + "home": null, + "home_last_prompted_at": null, + "project_last_prompted_at": {}, + "projects": {} + }, + "description": "Tracks scopes where external config migration prompts should be suppressed." + }, "hide_full_access_warning": { "description": "Tracks whether the user has acknowledged the full access warning prompt.", "type": "boolean" @@ -1523,7 +1595,7 @@ "env_vars": { "default": null, "items": { - "type": "string" + "$ref": "#/definitions/McpServerEnvVar" }, "type": "array" }, @@ -2291,6 +2363,9 @@ "experimental_windows_sandbox": { "type": "boolean" }, + "external_migration": { + "type": "boolean" + }, "fast_mode": { "type": "boolean" }, @@ -2872,4 +2947,4 @@ }, "title": "ConfigToml", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/core/src/compact_tests.rs b/codex-rs/core/src/compact_tests.rs index e5d49db035..36181289b2 100644 --- a/codex-rs/core/src/compact_tests.rs +++ b/codex-rs/core/src/compact_tests.rs @@ -2,6 +2,7 @@ use super::*; use codex_model_provider_info::ModelProviderInfo; use codex_model_provider_info::WireApi; use codex_model_provider_info::create_oss_provider_with_base_url; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use pretty_assertions::assert_eq; async fn process_compacted_history_with_test_session( @@ -61,6 +62,7 @@ fn content_items_to_text_joins_non_empty_segments() { fn content_items_to_text_ignores_image_only_content() { let items = vec![ContentItem::InputImage { image_url: "file://image.png".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }]; let joined = content_items_to_text(&items); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 24d0e58ba8..133a29a48e 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4,6 +4,7 @@ use crate::config::edit::ConfigEdit; use crate::config::edit::ConfigEditsBuilder; use crate::config::edit::apply_blocking; use crate::config_loader::RequirementSource; +use crate::config_loader::project_trust_key; use crate::plugins::PluginsManager; use assert_matches::assert_matches; use codex_config::CONFIG_TOML_FILE; @@ -31,6 +32,7 @@ use codex_config::types::ApprovalsReviewer; use codex_config::types::BundledSkillsConfig; use codex_config::types::FeedbackConfigToml; use codex_config::types::HistoryPersistence; +use codex_config::types::McpServerEnvVar; use codex_config::types::McpServerToolConfig; use codex_config::types::McpServerTransportConfig; use codex_config::types::MemoriesConfig; @@ -2489,7 +2491,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> { command: "docs-server".to_string(), args: Vec::new(), env: None, - env_vars: vec!["ALPHA".to_string(), "BETA".to_string()], + env_vars: vec!["ALPHA".into(), "BETA".into()], cwd: None, }, experimental_environment: None, @@ -2525,7 +2527,7 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> { let docs = loaded.get("docs").expect("docs entry"); match &docs.transport { McpServerTransportConfig::Stdio { env_vars, .. } => { - assert_eq!(env_vars, &vec!["ALPHA".to_string(), "BETA".to_string()]); + assert_eq!(env_vars, &vec!["ALPHA".into(), "BETA".into()]); } other => panic!("unexpected transport {other:?}"), } @@ -2533,6 +2535,62 @@ async fn replace_mcp_servers_serializes_env_vars() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn replace_mcp_servers_serializes_sourced_env_vars() -> anyhow::Result<()> { + let codex_home = TempDir::new()?; + + let servers = BTreeMap::from([( + "docs".to_string(), + McpServerConfig { + transport: McpServerTransportConfig::Stdio { + command: "docs-server".to_string(), + args: Vec::new(), + env: None, + env_vars: vec![ + "LEGACY".into(), + McpServerEnvVar::Config { + name: "REMOTE_TOKEN".to_string(), + source: Some("remote".to_string()), + }, + ], + cwd: None, + }, + experimental_environment: None, + enabled: true, + required: false, + supports_parallel_tool_calls: false, + disabled_reason: None, + startup_timeout_sec: None, + tool_timeout_sec: None, + default_tools_approval_mode: None, + enabled_tools: None, + disabled_tools: None, + scopes: None, + oauth_resource: None, + tools: HashMap::new(), + }, + )]); + + apply_blocking( + codex_home.path(), + /*profile*/ None, + &[ConfigEdit::ReplaceMcpServers(servers.clone())], + )?; + + let config_path = codex_home.path().join(CONFIG_TOML_FILE); + let serialized = std::fs::read_to_string(&config_path)?; + assert!( + serialized + .contains(r#"env_vars = ["LEGACY", { name = "REMOTE_TOKEN", source = "remote" }]"#), + "serialized config missing sourced env_vars field:\n{serialized}" + ); + + let loaded = load_global_mcp_servers(codex_home.path()).await?; + assert_eq!(loaded, servers); + + Ok(()) +} + #[tokio::test] async fn replace_mcp_servers_serializes_cwd() -> anyhow::Result<()> { let codex_home = TempDir::new()?; @@ -5449,7 +5507,9 @@ model = "foo""#; // Since we created the [projects] table as part of migration, it is kept implicit. // Expect explicit per-project tables, preserving prior entries and appending the new one. - let expected = r#"toplevel = "baz" + let new_project_key = project_trust_key(new_project); + let expected = format!( + r#"toplevel = "baz" model = "foo" [projects."/Users/mbolin/code/codex4"] @@ -5459,14 +5519,42 @@ foo = "bar" [projects."/Users/mbolin/code/codex3"] trust_level = "trusted" -[projects."/Users/mbolin/code/codex2"] +[projects."{new_project_key}"] trust_level = "trusted" -"#; +"# + ); assert_eq!(contents, expected); Ok(()) } +#[cfg(unix)] +#[tokio::test] +async fn active_project_does_not_match_configured_alias_for_canonical_cwd() -> anyhow::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let alias_root = tmp.path().join("project_alias"); + std::fs::create_dir_all(&project_root)?; + std::os::unix::fs::symlink(&project_root, &alias_root)?; + + let config = ConfigToml { + projects: Some(HashMap::from([( + alias_root.to_string_lossy().to_string(), + ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }, + )])), + ..Default::default() + }; + + assert_eq!( + config.get_active_project(&project_root, /*repo_root*/ None), + None + ); + + Ok(()) +} + #[test] fn test_set_default_oss_provider() -> std::io::Result<()> { let temp_dir = TempDir::new()?; diff --git a/codex-rs/core/src/config/edit.rs b/codex-rs/core/src/config/edit.rs index 5adc0f998c..e7cf3651c1 100644 --- a/codex-rs/core/src/config/edit.rs +++ b/codex-rs/core/src/config/edit.rs @@ -43,6 +43,14 @@ pub enum ConfigEdit { SetWindowsWslSetupAcknowledged(bool), /// Toggle the model migration prompt acknowledgement flag. SetNoticeHideModelMigrationPrompt(String, bool), + /// Toggle the home external config migration prompt acknowledgement flag. + SetNoticeHideExternalConfigMigrationPromptHome(bool), + /// Record when the home external config migration prompt was last shown. + SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(i64), + /// Toggle the project external config migration prompt acknowledgement flag. + SetNoticeHideExternalConfigMigrationPromptProject(String, bool), + /// Record when the project external config migration prompt was last shown. + SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt(String, i64), /// Record that a migration prompt was shown for an old->new model mapping. RecordModelMigrationSeen { from: String, to: String }, /// Replace the entire `[mcp_servers]` table. @@ -128,6 +136,7 @@ pub fn model_availability_nux_count_edits(shown_count: &HashMap) -> mod document_helpers { use codex_config::types::AppToolApproval; use codex_config::types::McpServerConfig; + use codex_config::types::McpServerEnvVar; use codex_config::types::McpServerToolConfig; use codex_config::types::McpServerTransportConfig; use toml_edit::Array as TomlArray; @@ -190,7 +199,7 @@ mod document_helpers { entry["env"] = table_from_pairs(env.iter()); } if !env_vars.is_empty() { - entry["env_vars"] = array_from_iter(env_vars.iter().cloned()); + entry["env_vars"] = array_from_env_vars(env_vars); } if let Some(cwd) = cwd { entry["cwd"] = value(cwd.to_string_lossy().to_string()); @@ -340,6 +349,24 @@ mod document_helpers { TomlItem::Value(array.into()) } + fn array_from_env_vars(env_vars: &[McpServerEnvVar]) -> TomlItem { + let mut array = TomlArray::new(); + for env_var in env_vars { + match env_var { + McpServerEnvVar::Name(name) => array.push(name.clone()), + McpServerEnvVar::Config { name, source } => { + let mut table = InlineTable::new(); + table.insert("name", name.clone().into()); + if let Some(source) = source { + table.insert("source", source.clone().into()); + } + array.push(table); + } + } + } + TomlItem::Value(array.into()) + } + fn table_from_pairs<'a, I>(pairs: I) -> TomlItem where I: IntoIterator, @@ -421,6 +448,53 @@ impl ConfigDocument { value(*acknowledged), )) } + ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome(acknowledged) => Ok(self + .write_value( + Scope::Global, + &[ + NOTICE_TABLE_KEY, + "external_config_migration_prompts", + "home", + ], + value(*acknowledged), + )), + ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(timestamp) => { + Ok(self.write_value( + Scope::Global, + &[ + NOTICE_TABLE_KEY, + "external_config_migration_prompts", + "home_last_prompted_at", + ], + value(*timestamp), + )) + } + ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( + project, + acknowledged, + ) => Ok(self.write_value( + Scope::Global, + &[ + NOTICE_TABLE_KEY, + "external_config_migration_prompts", + "projects", + project.as_str(), + ], + value(*acknowledged), + )), + ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( + project, + timestamp, + ) => Ok(self.write_value( + Scope::Global, + &[ + NOTICE_TABLE_KEY, + "external_config_migration_prompts", + "project_last_prompted_at", + project.as_str(), + ], + value(*timestamp), + )), ConfigEdit::RecordModelMigrationSeen { from, to } => Ok(self.write_value( Scope::Global, &[NOTICE_TABLE_KEY, "model_migrations", from.as_str()], @@ -919,6 +993,28 @@ impl ConfigEditsBuilder { self } + pub fn set_hide_external_config_migration_prompt_home(mut self, acknowledged: bool) -> Self { + self.edits + .push(ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( + acknowledged, + )); + self + } + + pub fn set_hide_external_config_migration_prompt_project( + mut self, + project: &str, + acknowledged: bool, + ) -> Self { + self.edits.push( + ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( + project.to_string(), + acknowledged, + ), + ); + self + } + pub fn record_model_migration_seen(mut self, from: &str, to: &str) -> Self { self.edits.push(ConfigEdit::RecordModelMigrationSeen { from: from.to_string(), diff --git a/codex-rs/core/src/config/edit_tests.rs b/codex-rs/core/src/config/edit_tests.rs index 4f340d89b3..2a966d9b41 100644 --- a/codex-rs/core/src/config/edit_tests.rs +++ b/codex-rs/core/src/config/edit_tests.rs @@ -552,6 +552,130 @@ gpt-5 = "gpt-5.1" assert_eq!(contents, expected); } +#[test] +fn blocking_set_hide_external_config_migration_prompt_home_preserves_table() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[notice] +existing = "value" +"#, + ) + .expect("seed"); + apply_blocking( + codex_home, + /*profile*/ None, + &[ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( + true, + )], + ) + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"[notice] +existing = "value" + +[notice.external_config_migration_prompts] +home = true +"#; + assert_eq!(contents, expected); +} + +#[test] +fn blocking_set_hide_external_config_migration_prompt_project_preserves_table() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[notice] +existing = "value" +"#, + ) + .expect("seed"); + apply_blocking( + codex_home, + /*profile*/ None, + &[ + ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( + "/Users/alexsong/code/skills".to_string(), + true, + ), + ], + ) + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"[notice] +existing = "value" + +[notice.external_config_migration_prompts.projects] +"/Users/alexsong/code/skills" = true +"#; + assert_eq!(contents, expected); +} + +#[test] +fn blocking_set_external_config_migration_prompt_home_last_prompted_at_preserves_table() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[notice] +existing = "value" +"#, + ) + .expect("seed"); + apply_blocking( + codex_home, + /*profile*/ None, + &[ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(1_760_000_000)], + ) + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"[notice] +existing = "value" + +[notice.external_config_migration_prompts] +home_last_prompted_at = 1760000000 +"#; + assert_eq!(contents, expected); +} + +#[test] +fn blocking_set_external_config_migration_prompt_project_last_prompted_at_preserves_table() { + let tmp = tempdir().expect("tmpdir"); + let codex_home = tmp.path(); + std::fs::write( + codex_home.join(CONFIG_TOML_FILE), + r#"[notice] +existing = "value" +"#, + ) + .expect("seed"); + apply_blocking( + codex_home, + /*profile*/ None, + &[ + ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( + "/Users/alexsong/code/skills".to_string(), + 1_760_000_000, + ), + ], + ) + .expect("persist"); + + let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config"); + let expected = r#"[notice] +existing = "value" + +[notice.external_config_migration_prompts.project_last_prompted_at] +"/Users/alexsong/code/skills" = 1760000000 +"#; + assert_eq!(contents, expected); +} + #[test] fn blocking_replace_mcp_servers_round_trips() { let tmp = tempdir().expect("tmpdir"); @@ -572,7 +696,7 @@ fn blocking_replace_mcp_servers_round_trips() { .into_iter() .collect(), ), - env_vars: vec!["FOO".to_string()], + env_vars: vec!["FOO".into()], cwd: None, }, experimental_environment: None, diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 9f9d83866d..9eaeb7149c 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -561,7 +561,9 @@ async fn load_requirements_from_legacy_scheme( struct ProjectTrustContext { project_root: AbsolutePathBuf, project_root_key: String, + project_root_lookup_keys: Vec, repo_root_key: Option, + repo_root_lookup_keys: Option>, projects_trust: std::collections::HashMap, user_config_file: AbsolutePathBuf, } @@ -584,28 +586,39 @@ impl ProjectTrustDecision { impl ProjectTrustContext { fn decision_for_dir(&self, dir: &AbsolutePathBuf) -> ProjectTrustDecision { - let dir_key = project_trust_key(dir.as_path()); - if let Some(trust_level) = self.projects_trust.get(&dir_key).copied() { - return ProjectTrustDecision { - trust_level: Some(trust_level), - trust_key: dir_key, - }; + for dir_key in normalized_project_trust_keys(dir.as_path()) { + if let Some((trust_key, trust_level)) = + project_trust_for_lookup_key(&self.projects_trust, &dir_key) + { + return ProjectTrustDecision { + trust_level: Some(trust_level), + trust_key, + }; + } } - if let Some(trust_level) = self.projects_trust.get(&self.project_root_key).copied() { - return ProjectTrustDecision { - trust_level: Some(trust_level), - trust_key: self.project_root_key.clone(), - }; + for project_root_key in &self.project_root_lookup_keys { + if let Some((trust_key, trust_level)) = + project_trust_for_lookup_key(&self.projects_trust, project_root_key) + { + return ProjectTrustDecision { + trust_level: Some(trust_level), + trust_key, + }; + } } - if let Some(repo_root_key) = self.repo_root_key.as_ref() - && let Some(trust_level) = self.projects_trust.get(repo_root_key).copied() - { - return ProjectTrustDecision { - trust_level: Some(trust_level), - trust_key: repo_root_key.clone(), - }; + if let Some(repo_root_lookup_keys) = self.repo_root_lookup_keys.as_ref() { + for repo_root_key in repo_root_lookup_keys { + if let Some((trust_key, trust_level)) = + project_trust_for_lookup_key(&self.projects_trust, repo_root_key) + { + return ProjectTrustDecision { + trust_level: Some(trust_level), + trust_key, + }; + } + } } ProjectTrustDecision { @@ -617,37 +630,35 @@ impl ProjectTrustContext { } } - fn disabled_reason_for_dir(&self, dir: &AbsolutePathBuf) -> Option { - let decision = self.decision_for_dir(dir); + fn disabled_reason_for_decision(&self, decision: &ProjectTrustDecision) -> Option { if decision.is_trusted() { return None; } + let gated_features = "project-local config, hooks, and exec policies"; let trust_key = decision.trust_key.as_str(); let user_config_file = self.user_config_file.as_path().display(); match decision.trust_level { Some(TrustLevel::Untrusted) => Some(format!( - "{trust_key} is marked as untrusted in {user_config_file}. To load config.toml, mark it trusted." + "{trust_key} is marked as untrusted in {user_config_file}. To load {gated_features}, mark it trusted." )), _ => Some(format!( - "To load config.toml, add {trust_key} as a trusted project in {user_config_file}." + "To load {gated_features}, add {trust_key} as a trusted project in {user_config_file}." )), } } } fn project_layer_entry( - trust_context: &ProjectTrustContext, dot_codex_folder: &AbsolutePathBuf, - layer_dir: &AbsolutePathBuf, config: TomlValue, - config_toml_exists: bool, + disabled_reason: Option, ) -> ConfigLayerEntry { let source = ConfigLayerSource::Project { dot_codex_folder: dot_codex_folder.clone(), }; - if config_toml_exists && let Some(reason) = trust_context.disabled_reason_for_dir(layer_dir) { + if let Some(reason) = disabled_reason { ConfigLayerEntry::new_disabled(source, config, reason) } else { ConfigLayerEntry::new(source, config) @@ -673,25 +684,30 @@ async fn project_trust_context( let project_root = find_project_root(fs, cwd, project_root_markers).await?; let projects = project_trust_config.projects.unwrap_or_default(); - let project_root_key = project_trust_key(project_root.as_path()); + let project_root_lookup_keys = normalized_project_trust_keys(project_root.as_path()); + let project_root_key = project_root_lookup_keys + .first() + .cloned() + .unwrap_or_else(|| project_trust_key(project_root.as_path())); let repo_root = resolve_root_git_project_for_trust(fs, cwd).await; - let repo_root_key = repo_root + let repo_root_lookup_keys = repo_root .as_ref() - .map(|root| project_trust_key(root.as_path())); + .map(|root| normalized_project_trust_keys(root.as_path())); + let repo_root_key = repo_root_lookup_keys + .as_ref() + .and_then(|keys| keys.first().cloned()); let projects_trust = projects .into_iter() - .filter_map(|(key, project)| { - project - .trust_level - .map(|trust_level| (project_trust_key(Path::new(&key)), trust_level)) - }) + .filter_map(|(key, project)| project.trust_level.map(|trust_level| (key, trust_level))) .collect(); Ok(ProjectTrustContext { project_root, project_root_key, + project_root_lookup_keys, repo_root_key, + repo_root_lookup_keys, projects_trust, user_config_file: user_config_file.clone(), }) @@ -700,13 +716,52 @@ async fn project_trust_context( /// Canonicalize the path and convert it to a string to be used as a key in the /// projects trust map. On Windows, strips UNC, when possible, to try to ensure /// that different paths that point to the same location have the same key. -pub fn project_trust_key(project_path: &Path) -> String { - normalize_path(project_path) - .unwrap_or_else(|_| project_path.to_path_buf()) - .to_string_lossy() - .to_string() +pub fn project_trust_key(path: &Path) -> String { + normalized_project_trust_keys(path) + .into_iter() + .next() + .unwrap_or_else(|| normalize_project_trust_lookup_key(path.to_string_lossy().to_string())) } +fn normalized_project_trust_keys(path: &Path) -> Vec { + let normalized_path = normalize_project_trust_lookup_key(path.to_string_lossy().to_string()); + let normalized_canonical_path = normalize_project_trust_lookup_key( + normalize_path(path) + .unwrap_or_else(|_| path.to_path_buf()) + .to_string_lossy() + .to_string(), + ); + if normalized_path == normalized_canonical_path { + vec![normalized_canonical_path] + } else { + vec![normalized_canonical_path, normalized_path] + } +} + +fn normalize_project_trust_lookup_key(key: String) -> String { + if cfg!(windows) { + key.to_ascii_lowercase() + } else { + key + } +} +fn project_trust_for_lookup_key( + projects_trust: &std::collections::HashMap, + lookup_key: &str, +) -> Option<(String, TrustLevel)> { + if let Some(trust_level) = projects_trust.get(lookup_key).copied() { + return Some((lookup_key.to_string(), trust_level)); + } + + let mut normalized_matches: Vec<_> = projects_trust + .iter() + .filter(|(key, _)| normalize_project_trust_lookup_key((*key).clone()) == lookup_key) + .collect(); + normalized_matches.sort_by(|(left, _), (right, _)| left.cmp(right)); + normalized_matches + .first() + .map(|(key, trust_level)| ((**key).clone(), **trust_level)) +} /// Takes a `toml::Value` parsed from a config.toml file and walks through it, /// resolving any `AbsolutePathBuf` fields against `base_dir`, returning a new /// `toml::Value` with the same shape but with paths resolved. @@ -834,6 +889,7 @@ async fn load_project_layers( } let decision = trust_context.decision_for_dir(&dir); + let disabled_reason = trust_context.disabled_reason_for_decision(&decision); let dot_codex_normalized = normalize_path(dot_codex_abs.as_path()).unwrap_or_else(|_| dot_codex_abs.to_path_buf()); if dot_codex_abs == codex_home_abs || dot_codex_normalized == codex_home_normalized { @@ -855,24 +911,16 @@ async fn load_project_layers( )); } layers.push(project_layer_entry( - trust_context, &dot_codex_abs, - &dir, TomlValue::Table(toml::map::Map::new()), - /*config_toml_exists*/ true, + disabled_reason.clone(), )); continue; } }; let config = resolve_relative_paths_in_config_toml(config, dot_codex_abs.as_path())?; - let entry = project_layer_entry( - trust_context, - &dot_codex_abs, - &dir, - config, - /*config_toml_exists*/ true, - ); + let entry = project_layer_entry(&dot_codex_abs, config, disabled_reason.clone()); layers.push(entry); } Err(err) => { @@ -881,11 +929,9 @@ async fn load_project_layers( // for this project layer, as this may still have subfolders // that are significant in the overall ConfigLayerStack. layers.push(project_layer_entry( - trust_context, &dot_codex_abs, - &dir, TomlValue::Table(toml::map::Map::new()), - /*config_toml_exists*/ false, + disabled_reason, )); } else { let config_file_display = config_file.as_path().display(); @@ -900,7 +946,6 @@ async fn load_project_layers( Ok(layers) } - /// The legacy mechanism for specifying admin-enforced configuration is to read /// from a file like `/etc/codex/managed_config.toml` that has the same /// structure as `config.toml` where fields like `approval_policy` can specify diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index eb533c4ca9..a8f4eda8ec 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -1343,6 +1343,66 @@ async fn project_layers_disabled_when_untrusted_or_unknown() -> std::io::Result< Ok(()) } +#[cfg(unix)] +#[tokio::test] +async fn project_trust_does_not_match_configured_alias_for_canonical_cwd() -> std::io::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let alias_root = tmp.path().join("project_alias"); + tokio::fs::create_dir_all(project_root.join(".codex")).await?; + tokio::fs::write(project_root.join(".git"), "gitdir: here").await?; + tokio::fs::write( + project_root.join(".codex").join(CONFIG_TOML_FILE), + "foo = \"project\"\n", + ) + .await?; + std::os::unix::fs::symlink(&project_root, &alias_root)?; + + let codex_home = tmp.path().join("home"); + tokio::fs::create_dir_all(&codex_home).await?; + tokio::fs::write( + codex_home.join(CONFIG_TOML_FILE), + toml::to_string(&ConfigToml { + projects: Some(HashMap::from([( + alias_root.to_string_lossy().to_string(), + ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }, + )])), + ..Default::default() + }) + .expect("serialize config"), + ) + .await?; + + let layers = load_config_layers_state( + LOCAL_FS.as_ref(), + &codex_home, + Some(AbsolutePathBuf::from_absolute_path(&project_root)?), + &[] as &[(String, TomlValue)], + LoaderOverrides::default(), + CloudRequirementsLoader::default(), + ) + .await?; + + let project_layers: Vec<_> = layers + .get_layers( + super::ConfigLayerStackOrdering::HighestPrecedenceFirst, + /*include_disabled*/ true, + ) + .into_iter() + .filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. })) + .collect(); + assert_eq!(project_layers.len(), 1); + assert!( + project_layers[0].disabled_reason.is_some(), + "configured aliases must not collapse into the canonical project key" + ); + assert_eq!(layers.effective_config().get("foo"), None); + + Ok(()) +} + #[tokio::test] async fn cli_override_can_update_project_local_mcp_server_when_project_is_trusted() -> std::io::Result<()> { @@ -1507,6 +1567,71 @@ async fn invalid_project_config_ignored_when_untrusted_or_unknown() -> std::io:: Ok(()) } +#[tokio::test] +async fn project_layer_without_config_toml_is_disabled_when_untrusted_or_unknown() +-> std::io::Result<()> { + let tmp = tempdir()?; + let project_root = tmp.path().join("project"); + let nested = project_root.join("child"); + tokio::fs::create_dir_all(nested.join(".codex")).await?; + tokio::fs::write(project_root.join(".git"), "gitdir: here").await?; + + let cwd = AbsolutePathBuf::from_absolute_path(&nested)?; + let cases = [ + ("untrusted", Some(TrustLevel::Untrusted), true), + ("unknown", None, true), + ("trusted", Some(TrustLevel::Trusted), false), + ]; + + for (name, trust_level, expect_disabled) in cases { + let codex_home = tmp.path().join(format!("home_no_config_{name}")); + tokio::fs::create_dir_all(&codex_home).await?; + if let Some(trust_level) = trust_level { + make_config_for_test( + &codex_home, + &project_root, + trust_level, + /*project_root_markers*/ None, + ) + .await?; + } + + let layers = load_config_layers_state( + LOCAL_FS.as_ref(), + &codex_home, + Some(cwd.clone()), + &[] as &[(String, TomlValue)], + LoaderOverrides::default(), + CloudRequirementsLoader::default(), + ) + .await?; + let project_layers: Vec<_> = layers + .get_layers( + super::ConfigLayerStackOrdering::HighestPrecedenceFirst, + /*include_disabled*/ true, + ) + .into_iter() + .filter(|layer| matches!(layer.name, super::ConfigLayerSource::Project { .. })) + .collect(); + assert_eq!( + project_layers.len(), + 1, + "expected one project layer for {name}" + ); + assert_eq!( + project_layers[0].disabled_reason.is_some(), + expect_disabled, + "unexpected disabled state for {name}", + ); + assert_eq!( + project_layers[0].config, + TomlValue::Table(toml::map::Map::new()) + ); + } + + Ok(()) +} + #[tokio::test] async fn cli_overrides_with_relative_paths_do_not_break_trust_check() -> std::io::Result<()> { let tmp = tempdir()?; diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 480c163147..933ce0ac76 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -13,6 +13,7 @@ pub use codex_app_server_protocol::AppInfo; pub use codex_app_server_protocol::AppMetadata; use codex_connectors::AllConnectorsCacheKey; use codex_connectors::DirectoryListResponse; +use codex_exec_server::Environment; use codex_login::token_data::TokenData; use codex_protocol::protocol::SandboxPolicy; use codex_tools::DiscoverableTool; @@ -37,6 +38,7 @@ use codex_login::default_client::create_client; use codex_login::default_client::originator; use codex_mcp::CODEX_APPS_MCP_SERVER_NAME; use codex_mcp::McpConnectionManager; +use codex_mcp::McpRuntimeEnvironment; use codex_mcp::ToolInfo; use codex_mcp::ToolPluginProvenance; use codex_mcp::codex_apps_tools_cache_key; @@ -241,6 +243,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status( INITIAL_SUBMIT_ID.to_owned(), tx_event, SandboxPolicy::new_read_only_policy(), + McpRuntimeEnvironment::new(Arc::new(Environment::default()), config.cwd.to_path_buf()), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), ToolPluginProvenance::default(), diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index db2c6b58b1..c4bdc916ff 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -649,8 +649,8 @@ fn image_data_url_estimate_adjustment(item: &ResponseItem) -> (i64, i64) { match item { ResponseItem::Message { content, .. } => { for content_item in content { - if let ContentItem::InputImage { image_url } = content_item { - accumulate(image_url, None); + if let ContentItem::InputImage { image_url, detail } = content_item { + accumulate(image_url, *detail); } } } diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index ff71b797be..1df14ca8bc 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -6,6 +6,7 @@ use codex_protocol::AgentPath; use codex_protocol::config_types::ReasoningSummary; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -382,6 +383,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ContentItem::InputImage { image_url: "https://example.com/img.png".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ContentItem::InputText { text: "caption".to_string(), @@ -405,7 +407,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, FunctionCallOutputContentItem::InputImage { image_url: "https://example.com/result.png".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }, @@ -425,7 +427,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, FunctionCallOutputContentItem::InputImage { image_url: "https://example.com/js-repl-result.png".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }, @@ -506,6 +508,7 @@ fn for_prompt_strips_images_when_model_does_not_support_images() { }, ContentItem::InputImage { image_url: "https://example.com/img.png".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ], end_turn: None, @@ -715,7 +718,7 @@ fn replace_last_turn_images_replaces_tool_output_images() { body: FunctionCallOutputBody::ContentItems(vec![ FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), success: Some(true), @@ -752,6 +755,7 @@ fn replace_last_turn_images_does_not_touch_user_images() { role: "user".to_string(), content: vec![ContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }], end_turn: None, phase: None, @@ -1680,7 +1684,10 @@ fn image_data_url_payload_does_not_dominate_message_estimate() { ContentItem::InputText { text: "Here is the screenshot".to_string(), }, - ContentItem::InputImage { image_url }, + ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }, ], end_turn: None, phase: None, @@ -1717,7 +1724,7 @@ fn image_data_url_payload_does_not_dominate_function_call_output_estimate() { }, FunctionCallOutputContentItem::InputImage { image_url, - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -1743,7 +1750,7 @@ fn image_data_url_payload_does_not_dominate_custom_tool_call_output_estimate() { }, FunctionCallOutputContentItem::InputImage { image_url, - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -1763,6 +1770,7 @@ fn non_base64_image_urls_are_unchanged() { role: "user".to_string(), content: vec![ContentItem::InputImage { image_url: "https://example.com/foo.png".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }], end_turn: None, phase: None, @@ -1772,7 +1780,7 @@ fn non_base64_image_urls_are_unchanged() { output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url: "file:///tmp/foo.png".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -1794,6 +1802,7 @@ fn data_url_without_base64_marker_is_unchanged() { role: "user".to_string(), content: vec![ContentItem::InputImage { image_url: "data:image/svg+xml,".to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }], end_turn: None, phase: None, @@ -1814,7 +1823,7 @@ fn non_image_base64_data_url_is_unchanged() { output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url, - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -1832,7 +1841,10 @@ fn mixed_case_data_url_markers_are_adjusted() { let item = ResponseItem::Message { id: None, role: "user".to_string(), - content: vec![ContentItem::InputImage { image_url }], + content: vec![ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }], end_turn: None, phase: None, }; @@ -1859,9 +1871,11 @@ fn multiple_inline_images_apply_multiple_fixed_costs() { }, ContentItem::InputImage { image_url: image_url_one, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ContentItem::InputImage { image_url: image_url_two, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ], end_turn: None, diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index 5e174944f1..21e13f6c15 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -90,7 +90,7 @@ fn parse_user_message(message: &[ContentItem]) -> Option { text_elements: Vec::new(), }); } - ContentItem::InputImage { image_url } => { + ContentItem::InputImage { image_url, .. } => { content.push(UserInput::Image { image_url: image_url.clone(), }); diff --git a/codex-rs/core/src/event_mapping_tests.rs b/codex-rs/core/src/event_mapping_tests.rs index a06111fd9a..0cadc5fbda 100644 --- a/codex-rs/core/src/event_mapping_tests.rs +++ b/codex-rs/core/src/event_mapping_tests.rs @@ -5,6 +5,7 @@ use codex_protocol::items::TurnItem; use codex_protocol::items::WebSearchItem; use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::ContentItem; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::ReasoningItemContent; use codex_protocol::models::ReasoningItemReasoningSummary; use codex_protocol::models::ResponseItem; @@ -26,9 +27,11 @@ fn parses_user_message_with_text_and_two_images() { }, ContentItem::InputImage { image_url: img1.clone(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ContentItem::InputImage { image_url: img2.clone(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ], end_turn: None, @@ -66,6 +69,7 @@ fn skips_local_image_label_text() { ContentItem::InputText { text: label }, ContentItem::InputImage { image_url: image_url.clone(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ContentItem::InputText { text: "".to_string(), @@ -145,6 +149,7 @@ fn skips_unnamed_image_label_text() { ContentItem::InputText { text: label }, ContentItem::InputImage { image_url: image_url.clone(), + detail: Some(DEFAULT_IMAGE_DETAIL), }, ContentItem::InputText { text: codex_protocol::models::image_close_tag_text(), diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 798bacfbf3..8f0f076f08 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -495,6 +495,8 @@ async fn load_exec_policy_with_warning( } pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result { + // Disabled project layers already represent the trust decision, so hooks + // and exec-policy loading can reuse the normal trusted-layer view. // Iterate the layers in increasing order of precedence, adding the *.rules // from each layer, so that higher-precedence layers can override // rules defined in lower-precedence ones. diff --git a/codex-rs/core/src/exec_policy_tests.rs b/codex-rs/core/src/exec_policy_tests.rs index f17869205d..64deca6bc6 100644 --- a/codex-rs/core/src/exec_policy_tests.rs +++ b/codex-rs/core/src/exec_policy_tests.rs @@ -10,6 +10,9 @@ use crate::config_loader::RequirementSource; use crate::config_loader::Sourced; use codex_app_server_protocol::ConfigLayerSource; use codex_config::RequirementsExecPolicy; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; +use codex_protocol::config_types::TrustLevel; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -67,6 +70,33 @@ fn starlark_string(value: &str) -> String { value.replace('\\', "\\\\").replace('"', "\\\"") } +async fn write_project_trust_config( + codex_home: &Path, + trusted_projects: &[(&Path, TrustLevel)], +) -> std::io::Result<()> { + tokio::fs::write( + codex_home.join(codex_config::CONFIG_TOML_FILE), + toml::to_string(&ConfigToml { + projects: Some( + trusted_projects + .iter() + .map(|(project, trust_level)| { + ( + project.to_string_lossy().to_string(), + ProjectConfig { + trust_level: Some(*trust_level), + }, + ) + }) + .collect::>(), + ), + ..Default::default() + }) + .expect("serialize config"), + ) + .await +} + fn read_only_file_system_sandbox_policy() -> FileSystemSandboxPolicy { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -1756,3 +1786,159 @@ async fn assert_exec_approval_requirement_for_command( assert_eq!(requirement, expected_requirement); } + +#[tokio::test] +async fn exec_policies_only_load_from_trusted_project_layers() -> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let codex_home = temp.path().join("home_execpolicy_nested"); + let project_root = temp.path().join("project_execpolicy_nested"); + let nested = project_root.join("nested"); + let root_rules = project_root.join(".codex").join(RULES_DIR_NAME); + let nested_rules = nested.join(".codex").join(RULES_DIR_NAME); + + fs::create_dir_all(&codex_home)?; + fs::create_dir_all(&nested_rules)?; + fs::write(project_root.join(".git"), "gitdir: here")?; + fs::create_dir_all(&root_rules)?; + fs::write( + root_rules.join("deny-rm.rules"), + r#"prefix_rule(pattern=["rm"], decision="forbidden")"#, + )?; + fs::write( + nested_rules.join("deny-mv.rules"), + r#"prefix_rule(pattern=["mv"], decision="forbidden")"#, + )?; + write_project_trust_config(&codex_home, &[(&nested, TrustLevel::Trusted)]).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(nested)) + .build() + .await?; + + let policy = load_exec_policy(&config.config_layer_stack) + .await + .map_err(std::io::Error::other)?; + assert_eq!( + policy + .check_multiple([vec!["rm".to_string()]].iter(), &|_| Decision::Allow) + .decision, + Decision::Allow, + ); + assert_eq!( + policy + .check_multiple([vec!["mv".to_string()]].iter(), &|_| Decision::Allow) + .decision, + Decision::Forbidden, + ); + + Ok(()) +} + +#[tokio::test] +async fn exec_policies_require_project_trust_without_config_toml() -> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let project_root = temp.path().join("project_execpolicy"); + let nested = project_root.join("nested"); + let rules_dir = project_root.join(".codex").join(RULES_DIR_NAME); + fs::create_dir_all(&nested)?; + fs::write(project_root.join(".git"), "gitdir: here")?; + fs::create_dir_all(&rules_dir)?; + fs::write( + rules_dir.join("deny-rm.rules"), + r#"prefix_rule(pattern=["rm"], decision="forbidden")"#, + )?; + + let cases = [ + ( + "unknown", + Vec::<(&Path, TrustLevel)>::new(), + Decision::Allow, + ), + ( + "untrusted", + vec![(&project_root as &Path, TrustLevel::Untrusted)], + Decision::Allow, + ), + ( + "trusted", + vec![(&project_root as &Path, TrustLevel::Trusted)], + Decision::Forbidden, + ), + ]; + + for (name, trust_entries, expected_decision) in cases { + let codex_home = temp.path().join(format!("home_execpolicy_{name}")); + fs::create_dir_all(&codex_home)?; + write_project_trust_config(&codex_home, &trust_entries).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(nested.clone())) + .build() + .await?; + + let policy = load_exec_policy(&config.config_layer_stack) + .await + .map_err(std::io::Error::other)?; + assert_eq!( + policy + .check_multiple([vec!["rm".to_string()]].iter(), &|_| Decision::Allow) + .decision, + expected_decision, + "unexpected execpolicy decision for {name}", + ); + } + + Ok(()) +} + +#[tokio::test] +async fn exec_policy_warnings_ignore_untrusted_project_rules_without_config_toml() +-> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let project_root = temp.path().join("project_execpolicy_warning"); + let nested = project_root.join("nested"); + let rules_dir = project_root.join(".codex").join(RULES_DIR_NAME); + fs::create_dir_all(&nested)?; + fs::write(project_root.join(".git"), "gitdir: here")?; + fs::create_dir_all(&rules_dir)?; + fs::write(rules_dir.join("broken.rules"), "prefix_rule(")?; + + let cases = [ + ("unknown", Vec::<(&Path, TrustLevel)>::new(), false), + ( + "untrusted", + vec![(&project_root as &Path, TrustLevel::Untrusted)], + false, + ), + ( + "trusted", + vec![(&project_root as &Path, TrustLevel::Trusted)], + true, + ), + ]; + + for (name, trust_entries, expect_warning) in cases { + let codex_home = temp.path().join(format!("home_execpolicy_warning_{name}")); + fs::create_dir_all(&codex_home)?; + write_project_trust_config(&codex_home, &trust_entries).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(nested.clone())) + .build() + .await?; + + let warning = check_execpolicy_for_warnings(&config.config_layer_stack) + .await + .map_err(std::io::Error::other)?; + assert_eq!( + matches!(warning, Some(ExecPolicyError::ParsePolicy { .. })), + expect_warning, + "unexpected execpolicy warning state for {name}", + ); + } + + Ok(()) +} diff --git a/codex-rs/core/src/external_agent_config.rs b/codex-rs/core/src/external_agent_config.rs index 73aba5af13..db5894e952 100644 --- a/codex-rs/core/src/external_agent_config.rs +++ b/codex-rs/core/src/external_agent_config.rs @@ -273,7 +273,7 @@ impl ExternalAgentConfigService { items.push(ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", source_agents_md.display(), target_agents_md.display() ), @@ -357,7 +357,7 @@ impl ExternalAgentConfigService { Some(ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, - description: format!("Import enabled plugins from {}", source_settings.display()), + description: format!("Migrate enabled plugins from {}", source_settings.display()), cwd, details: Some(plugin_details), }) diff --git a/codex-rs/core/src/external_agent_config_tests.rs b/codex-rs/core/src/external_agent_config_tests.rs index 4fd81351a9..3b427f6bf0 100644 --- a/codex-rs/core/src/external_agent_config_tests.rs +++ b/codex-rs/core/src/external_agent_config_tests.rs @@ -74,7 +74,7 @@ async fn detect_home_lists_config_skills_and_agents_md() { ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", external_agent_home.join("CLAUDE.md").display(), codex_home.join("AGENTS.md").display() ), @@ -107,7 +107,7 @@ async fn detect_repo_lists_agents_md_for_each_cwd() { ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", repo_root.join("CLAUDE.md").display(), repo_root.join("AGENTS.md").display(), ), @@ -117,7 +117,7 @@ async fn detect_repo_lists_agents_md_for_each_cwd() { ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", repo_root.join("CLAUDE.md").display(), repo_root.join("AGENTS.md").display(), ), @@ -194,7 +194,7 @@ async fn detect_repo_still_reports_non_plugin_items_when_home_config_is_invalid( ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", repo_root.join(".claude").join("CLAUDE.md").display(), repo_root.join("AGENTS.md").display(), ), @@ -566,7 +566,7 @@ async fn detect_repo_prefers_non_empty_external_agent_agents_source() { vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::AgentsMd, description: format!( - "Import {} to {}", + "Migrate {} to {}", repo_root.join(".claude").join("CLAUDE.md").display(), repo_root.join("AGENTS.md").display(), ), @@ -650,7 +650,7 @@ async fn detect_home_lists_enabled_plugins_from_settings() { vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", external_agent_home.join("settings.json").display() ), cwd: None, @@ -710,7 +710,7 @@ enabled = true vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", repo_root.join(".claude").join("settings.json").display() ), cwd: Some(repo_root), @@ -868,7 +868,7 @@ enabled = true vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", repo_root.join(".claude").join("settings.json").display() ), cwd: Some(repo_root), @@ -1048,7 +1048,7 @@ source = "owner/debug-marketplace" vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", repo_root.join(".claude").join("settings.json").display() ), cwd: Some(repo_root), @@ -1275,7 +1275,7 @@ async fn detect_home_supports_relative_external_agent_plugin_marketplace_path() vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", external_agent_home.join("settings.json").display() ), cwd: None, @@ -1426,7 +1426,7 @@ async fn detect_repo_supports_project_relative_external_agent_plugin_marketplace vec![ExternalAgentConfigMigrationItem { item_type: ExternalAgentConfigMigrationItemType::Plugins, description: format!( - "Import enabled plugins from {}", + "Migrate enabled plugins from {}", repo_root.join(".claude").join("settings.json").display() ), cwd: Some(repo_root), diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 18d1a30292..1fef33c3f3 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -99,6 +99,7 @@ pub(crate) use skills::build_skill_injections; pub(crate) use skills::build_skill_name_counts; pub(crate) use skills::collect_env_var_dependencies; pub(crate) use skills::collect_explicit_skill_mentions; +pub(crate) use skills::default_skill_metadata_budget; pub(crate) use skills::injection; pub(crate) use skills::manager; pub(crate) use skills::maybe_emit_implicit_skill_invocation; diff --git a/codex-rs/core/src/memories/prompts.rs b/codex-rs/core/src/memories/prompts.rs index fc85917c9b..9425a53804 100644 --- a/codex-rs/core/src/memories/prompts.rs +++ b/codex-rs/core/src/memories/prompts.rs @@ -239,7 +239,7 @@ pub(super) fn build_stage_one_input_message( rollout_contents: &str, ) -> anyhow::Result { let rollout_token_limit = model_info - .context_window + .resolved_context_window() .and_then(|limit| (limit > 0).then_some(limit)) .map(|limit| limit.saturating_mul(model_info.effective_context_window_percent) / 100) .map(|limit| (limit.saturating_mul(phase_one::CONTEXT_WINDOW_PERCENT) / 100).max(1)) diff --git a/codex-rs/core/src/memories/prompts_tests.rs b/codex-rs/core/src/memories/prompts_tests.rs index 959bb6911d..d0ce447c4a 100644 --- a/codex-rs/core/src/memories/prompts_tests.rs +++ b/codex-rs/core/src/memories/prompts_tests.rs @@ -41,6 +41,7 @@ fn build_stage_one_input_message_uses_default_limit_when_model_context_window_mi let input = format!("{}{}{}", "a".repeat(700_000), "middle", "z".repeat(700_000)); let mut model_info = model_info_from_slug("gpt-5.2-codex"); model_info.context_window = None; + model_info.max_context_window = None; let expected_truncated = truncate_text( &input, TruncationPolicy::Tokens(phase_one::DEFAULT_STAGE_ONE_ROLLOUT_TOKEN_LIMIT), diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index d3afbf8978..794d1f2434 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -565,9 +565,7 @@ impl PluginsManager { self.restriction_product, )?; let plugin_id = resolved.plugin_id.as_key(); - // This only forwards the backend mutation before the local install flow. We rely on - // `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra - // reconcile pass here. + // This only forwards the backend mutation before the local install flow. codex_core_plugins::remote::enable_remote_plugin( &remote_plugin_service_config(config), auth, @@ -655,11 +653,11 @@ impl PluginsManager { auth: Option<&CodexAuth>, plugin_id: String, ) -> Result<(), PluginUninstallError> { + // TODO: Remove this legacy remote-sync path once remote plugins have + // their own manager and installed-state API. let plugin_id = PluginId::parse(&plugin_id)?; let plugin_key = plugin_id.as_key(); - // This only forwards the backend mutation before the local uninstall flow. We rely on - // `plugin/list(forceRemoteSync=true)` to sync local state rather than doing an extra - // reconcile pass here. + // This only forwards the backend mutation before the local uninstall flow. codex_core_plugins::remote::uninstall_remote_plugin( &remote_plugin_service_config(config), auth, @@ -1060,10 +1058,11 @@ impl PluginsManager { })?; let plugin_key = plugin_id.as_key(); if matches!(plugin.source, MarketplacePluginSource::Git { .. }) && !plugin.installed { + let description = remote_plugin_install_required_description(&plugin.source); return Ok(PluginDetail { id: plugin_key, name: plugin.name, - description: None, + description: Some(description), source: plugin.source, policy: plugin.policy, interface: plugin.interface, @@ -1497,6 +1496,34 @@ impl PluginsManager { } } +fn remote_plugin_install_required_description(source: &MarketplacePluginSource) -> String { + let source_description = match source { + MarketplacePluginSource::Git { + url, + path, + ref_name, + sha, + } => { + let mut parts = vec![url.clone()]; + if let Some(path) = path { + parts.push(format!("path `{path}`")); + } + if let Some(ref_name) = ref_name { + parts.push(format!("ref `{ref_name}`")); + } + if let Some(sha) = sha { + parts.push(format!("sha `{sha}`")); + } + parts.join(", ") + } + MarketplacePluginSource::Local { path } => path.as_path().display().to_string(), + }; + + format!( + "This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {source_description}." + ) +} + #[derive(Debug, thiserror::Error)] pub enum PluginInstallError { #[error("{0}")] diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index 4f327637b0..e43490803d 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -1623,7 +1623,13 @@ plugins = true Some(PluginDetailsUnavailableReason::InstallRequiredForRemoteSource) ); assert!(!outcome.plugin.installed); - assert!(outcome.plugin.description.is_none()); + let expected_description = format!( + "This is a cross-repo plugin. Install it to view more detailed information. The source of the plugin is {missing_remote_repo_url}, path `plugins/toolkit`." + ); + assert_eq!( + outcome.plugin.description.as_deref(), + Some(expected_description.as_str()) + ); assert!(outcome.plugin.skills.is_empty()); assert!(outcome.plugin.apps.is_empty()); assert!(outcome.plugin.mcp_server_names.is_empty()); diff --git a/codex-rs/core/src/plugins/marketplace_remove.rs b/codex-rs/core/src/plugins/marketplace_remove.rs new file mode 100644 index 0000000000..490b16f95d --- /dev/null +++ b/codex-rs/core/src/plugins/marketplace_remove.rs @@ -0,0 +1,313 @@ +use crate::plugins::marketplace_install_root; +use crate::plugins::validate_plugin_segment; +use codex_config::RemoveMarketplaceConfigOutcome; +use codex_config::remove_user_marketplace_config; +use codex_utils_absolute_path::AbsolutePathBuf; +use std::fs; +use std::path::Path; +use std::path::PathBuf; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MarketplaceRemoveRequest { + pub marketplace_name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MarketplaceRemoveOutcome { + pub marketplace_name: String, + pub removed_installed_root: Option, +} + +#[derive(Debug, thiserror::Error)] +pub enum MarketplaceRemoveError { + #[error("{0}")] + InvalidRequest(String), + #[error("{0}")] + Internal(String), +} + +pub async fn remove_marketplace( + codex_home: PathBuf, + request: MarketplaceRemoveRequest, +) -> Result { + tokio::task::spawn_blocking(move || remove_marketplace_sync(codex_home.as_path(), request)) + .await + .map_err(|err| { + MarketplaceRemoveError::Internal(format!("failed to remove marketplace: {err}")) + })? +} + +fn remove_marketplace_sync( + codex_home: &Path, + request: MarketplaceRemoveRequest, +) -> Result { + let marketplace_name = request.marketplace_name; + validate_plugin_segment(&marketplace_name, "marketplace name") + .map_err(MarketplaceRemoveError::InvalidRequest)?; + + let destination = marketplace_install_root(codex_home).join(&marketplace_name); + let config_outcome = + remove_user_marketplace_config(codex_home, &marketplace_name).map_err(|err| { + MarketplaceRemoveError::Internal(format!( + "failed to remove marketplace '{marketplace_name}' from user config.toml: {err}" + )) + })?; + if let RemoveMarketplaceConfigOutcome::NameCaseMismatch { configured_name } = &config_outcome { + return Err(MarketplaceRemoveError::InvalidRequest(format!( + "marketplace `{marketplace_name}` does not match configured marketplace `{configured_name}` exactly" + ))); + } + + let removed_config = config_outcome == RemoveMarketplaceConfigOutcome::Removed; + let removed_installed_root = remove_marketplace_root(&destination)?; + + if removed_installed_root.is_none() && !removed_config { + return Err(MarketplaceRemoveError::InvalidRequest(format!( + "marketplace `{marketplace_name}` is not configured or installed" + ))); + } + + Ok(MarketplaceRemoveOutcome { + marketplace_name, + removed_installed_root, + }) +} + +fn remove_marketplace_root(root: &Path) -> Result, MarketplaceRemoveError> { + if !root.exists() { + return Ok(None); + } + + let removed_root = AbsolutePathBuf::try_from(root.to_path_buf()).map_err(|err| { + MarketplaceRemoveError::Internal(format!( + "failed to resolve installed marketplace root {}: {err}", + root.display() + )) + })?; + let metadata = fs::symlink_metadata(root).map_err(|err| { + MarketplaceRemoveError::Internal(format!( + "failed to inspect installed marketplace root {}: {err}", + root.display() + )) + })?; + let remove_result = if metadata.is_dir() { + fs::remove_dir_all(root) + } else { + fs::remove_file(root) + }; + remove_result.map_err(|err| { + MarketplaceRemoveError::Internal(format!( + "failed to remove installed marketplace root {}: {err}", + root.display() + )) + })?; + Ok(Some(removed_root)) +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_config::MarketplaceConfigUpdate; + use codex_config::record_user_marketplace; + use pretty_assertions::assert_eq; + use tempfile::TempDir; + + #[test] + fn remove_marketplace_sync_removes_config_and_installed_root() { + let codex_home = TempDir::new().unwrap(); + record_user_marketplace( + codex_home.path(), + "debug", + &MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + }, + ) + .unwrap(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + fs::create_dir_all(installed_root.join(".agents/plugins")).unwrap(); + fs::write( + installed_root.join(".agents/plugins/marketplace.json"), + "{}", + ) + .unwrap(); + + let outcome = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "debug".to_string(), + }, + ) + .unwrap(); + + assert_eq!(outcome.marketplace_name, "debug"); + assert_eq!( + outcome.removed_installed_root, + Some(AbsolutePathBuf::try_from(installed_root.clone()).unwrap()) + ); + let config = + fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap(); + assert!(!config.contains("[marketplaces.debug]")); + assert!(!installed_root.exists()); + } + + #[test] + fn remove_marketplace_sync_rejects_unknown_marketplace() { + let codex_home = TempDir::new().unwrap(); + + let err = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "debug".to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "marketplace `debug` is not configured or installed" + ); + } + + #[test] + fn remove_marketplace_sync_rejects_case_mismatched_configured_name() { + let codex_home = TempDir::new().unwrap(); + record_user_marketplace( + codex_home.path(), + "debug", + &MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + }, + ) + .unwrap(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + fs::create_dir_all(&installed_root).unwrap(); + + let err = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "Debug".to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "marketplace `Debug` does not match configured marketplace `debug` exactly" + ); + assert!(installed_root.exists()); + let config = + fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap(); + assert!(config.contains("[marketplaces.debug]")); + } + + #[test] + fn remove_marketplace_sync_keeps_installed_root_when_config_removal_fails() { + let codex_home = TempDir::new().unwrap(); + fs::write( + codex_home.path().join(codex_config::CONFIG_TOML_FILE), + "[marketplaces.debug\n", + ) + .unwrap(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + fs::create_dir_all(&installed_root).unwrap(); + + let err = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "debug".to_string(), + }, + ) + .unwrap_err(); + + assert!( + err.to_string() + .contains("failed to remove marketplace 'debug' from user config.toml") + ); + assert!(installed_root.exists()); + } + + #[test] + fn remove_marketplace_sync_removes_file_installed_root() { + let codex_home = TempDir::new().unwrap(); + record_user_marketplace( + codex_home.path(), + "debug", + &MarketplaceConfigUpdate { + last_updated: "2026-04-13T00:00:00Z", + last_revision: None, + source_type: "git", + source: "https://github.com/owner/repo.git", + ref_name: Some("main"), + sparse_paths: &[], + }, + ) + .unwrap(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + fs::create_dir_all(installed_root.parent().unwrap()).unwrap(); + fs::write(&installed_root, "corrupt install root").unwrap(); + + let outcome = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "debug".to_string(), + }, + ) + .unwrap(); + + assert_eq!( + outcome, + MarketplaceRemoveOutcome { + marketplace_name: "debug".to_string(), + removed_installed_root: Some( + AbsolutePathBuf::try_from(installed_root.clone()).unwrap() + ), + } + ); + assert!(!installed_root.exists()); + let config = + fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap(); + assert!(!config.contains("[marketplaces.debug]")); + } + + #[test] + fn remove_marketplace_sync_removes_inline_config_entry() { + let codex_home = TempDir::new().unwrap(); + fs::write( + codex_home.path().join(codex_config::CONFIG_TOML_FILE), + r#" +marketplaces = { debug = { source_type = "git", source = "https://github.com/owner/repo.git" } } +"#, + ) + .unwrap(); + let installed_root = marketplace_install_root(codex_home.path()).join("debug"); + fs::create_dir_all(&installed_root).unwrap(); + + let outcome = remove_marketplace_sync( + codex_home.path(), + MarketplaceRemoveRequest { + marketplace_name: "debug".to_string(), + }, + ) + .unwrap(); + + assert_eq!(outcome.marketplace_name, "debug"); + assert_eq!( + outcome.removed_installed_root, + Some(AbsolutePathBuf::try_from(installed_root.clone()).unwrap()) + ); + assert!(!installed_root.exists()); + let config = + fs::read_to_string(codex_home.path().join(codex_config::CONFIG_TOML_FILE)).unwrap(); + assert!(!config.contains("debug")); + } +} diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 97b4db9dff..5806016b9a 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -5,6 +5,7 @@ mod injection; mod installed_marketplaces; mod manager; mod marketplace_add; +mod marketplace_remove; mod mentions; mod render; mod startup_sync; @@ -52,6 +53,10 @@ pub use marketplace_add::MarketplaceAddRequest; pub use marketplace_add::add_marketplace; pub(crate) use marketplace_add::is_local_marketplace_source; pub(crate) use marketplace_add::parse_marketplace_source; +pub use marketplace_remove::MarketplaceRemoveError; +pub use marketplace_remove::MarketplaceRemoveOutcome; +pub use marketplace_remove::MarketplaceRemoveRequest; +pub use marketplace_remove::remove_marketplace; pub(crate) use render::render_explicit_plugin_instructions; pub(crate) use render::render_plugins_section; pub(crate) use startup_sync::curated_plugins_repo_path; diff --git a/codex-rs/core/src/session/mcp.rs b/codex-rs/core/src/session/mcp.rs index 56628f0543..8b7935a50c 100644 --- a/codex-rs/core/src/session/mcp.rs +++ b/codex-rs/core/src/session/mcp.rs @@ -205,6 +205,13 @@ impl Session { turn_context.sub_id.clone(), self.get_tx_event(), turn_context.sandbox_policy.get().clone(), + McpRuntimeEnvironment::new( + turn_context + .environment + .clone() + .unwrap_or_else(|| Arc::new(Environment::default())), + turn_context.cwd.to_path_buf(), + ), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth.as_ref()), tool_plugin_provenance, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index e478ac6250..9a5349aad3 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -20,6 +20,7 @@ use crate::commit_attribution::commit_message_trailer_instruction; use crate::compact; use crate::config::ManagedFeatures; use crate::connectors; +use crate::default_skill_metadata_budget; use crate::exec_policy::ExecPolicyManager; use crate::installation_id::resolve_installation_id; use crate::parse_turn_item; @@ -28,6 +29,7 @@ use crate::realtime_conversation::RealtimeConversationManager; use crate::render_skills_section; use crate::rollout::find_thread_name_by_id; use crate::session_prefix::format_subagent_notification_message; +use crate::skills::SkillRenderSideEffects; use crate::skills_load_input_from_config; use crate::turn_metadata::TurnMetadataState; use async_channel::Receiver; @@ -53,6 +55,7 @@ use codex_login::CodexAuth; use codex_login::auth_env_telemetry::collect_auth_env_telemetry; use codex_login::default_client::originator; use codex_mcp::McpConnectionManager; +use codex_mcp::McpRuntimeEnvironment; use codex_mcp::ToolInfo; use codex_mcp::codex_apps_tools_cache_key; #[cfg(test)] @@ -365,9 +368,10 @@ pub struct Codex { pub(crate) type SessionLoopTermination = Shared>; -/// Wrapper returned by [`Codex::spawn`] containing the spawned [`Codex`], -/// the submission id for the initial `ConfigureSession` request and the -/// unique session id. +pub(crate) const THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE: &str = "Some enabled skills were not included in the model-visible skills list for this session. Mention a skill by name or path if you need it."; + +/// Wrapper returned by [`Codex::spawn`] containing the spawned [`Codex`] and +/// the unique session id. pub struct CodexSpawnOk { pub codex: Codex, pub thread_id: ThreadId, @@ -399,6 +403,7 @@ pub(crate) const INITIAL_SUBMIT_ID: &str = ""; pub(crate) const SUBMISSION_CHANNEL_CAPACITY: usize = 512; const CYBER_VERIFY_URL: &str = "https://chatgpt.com/cyber"; const CYBER_SAFETY_URL: &str = "https://developers.openai.com/codex/concepts/cyber-safety"; + impl Codex { /// Spawn a new [`Codex`] and initialize the session. pub(crate) async fn spawn(args: CodexSpawnArgs) -> CodexResult { @@ -2425,8 +2430,24 @@ impl Session { .turn_skills .outcome .allowed_skills_for_implicit_invocation(); - if let Some(skills_section) = render_skills_section(&implicit_skills) { - developer_sections.push(skills_section); + let rendered_skills = render_skills_section( + &implicit_skills, + default_skill_metadata_budget(turn_context.model_info.context_window), + SkillRenderSideEffects::ThreadStart { + session_telemetry: &self.services.session_telemetry, + }, + ); + if let Some(rendered_skills) = rendered_skills { + if rendered_skills.emit_warning { + self.send_event_raw(Event { + id: String::new(), + msg: EventMsg::Warning(WarningEvent { + message: THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE.to_string(), + }), + }) + .await; + } + developer_sections.push(rendered_skills.text); } let loaded_plugins = self .services diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index 766ac79ec1..ac5be4421c 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -676,7 +676,7 @@ impl Session { code_mode_service: crate::tools::code_mode::CodeModeService::new( config.js_repl_node_path.clone(), ), - environment, + environment: environment.clone(), }; services .model_client @@ -770,6 +770,12 @@ impl Session { INITIAL_SUBMIT_ID.to_owned(), tx_event.clone(), session_configuration.sandbox_policy.get().clone(), + McpRuntimeEnvironment::new( + environment + .clone() + .unwrap_or_else(|| Arc::new(Environment::default())), + session_configuration.cwd.to_path_buf(), + ), config.codex_home.to_path_buf(), codex_apps_tools_cache_key(auth), tool_plugin_provenance, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index be179bb417..11e2dfc873 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -8,9 +8,12 @@ use crate::config_loader::NetworkDomainPermissionToml; use crate::config_loader::NetworkDomainPermissionsToml; use crate::config_loader::RequirementSource; use crate::config_loader::Sourced; +use crate::config_loader::project_trust_key; use crate::exec::ExecCapturePolicy; use crate::function_tool::FunctionCallError; use crate::shell::default_user_shell; +use crate::skills::SkillRenderSideEffects; +use crate::skills::render::SkillMetadataBudget; use crate::tools::format_exec_output_str; use codex_features::Feature; @@ -21,6 +24,7 @@ use codex_models_manager::bundled_models_response; use codex_models_manager::model_info; use codex_protocol::AgentPath; use codex_protocol::ThreadId; +use codex_protocol::config_types::TrustLevel; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputPayload; @@ -53,10 +57,17 @@ use crate::tools::registry::ToolHandler; use crate::tools::router::ToolCallSource; use crate::turn_diff_tracker::TurnDiffTracker; use codex_app_server_protocol::AppInfo; +use codex_config::config_toml::ConfigToml; +use codex_config::config_toml::ProjectConfig; use codex_execpolicy::Decision; use codex_execpolicy::NetworkRuleProtocol; use codex_execpolicy::Policy; use codex_network_proxy::NetworkProxyConfig; +use codex_otel::MetricsClient; +use codex_otel::MetricsConfig; +use codex_otel::THREAD_SKILLS_ENABLED_TOTAL_METRIC; +use codex_otel::THREAD_SKILLS_KEPT_TOTAL_METRIC; +use codex_otel::THREAD_SKILLS_TRUNCATED_METRIC; use codex_otel::TelemetryAuthMode; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; @@ -82,6 +93,7 @@ use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::RealtimeVoicesList; use codex_protocol::protocol::ResumedHistory; use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::SkillScope; use codex_protocol::protocol::Submission; use codex_protocol::protocol::ThreadRolledBackEvent; use codex_protocol::protocol::TokenCountEvent; @@ -102,10 +114,16 @@ use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_mock_server; use core_test_support::test_codex::test_codex; +use core_test_support::test_path_buf; use core_test_support::tracing::install_test_tracing; use core_test_support::wait_for_event; use opentelemetry::trace::TraceContextExt; use opentelemetry::trace::TraceId; +use opentelemetry_sdk::metrics::InMemoryMetricExporter; +use opentelemetry_sdk::metrics::data::AggregatedMetrics; +use opentelemetry_sdk::metrics::data::Metric; +use opentelemetry_sdk::metrics::data::MetricData; +use opentelemetry_sdk::metrics::data::ResourceMetrics; use std::path::Path; use std::time::Duration; use tokio::time::sleep; @@ -151,6 +169,54 @@ fn assistant_message(text: &str) -> ResponseItem { } } +fn test_session_telemetry_without_metadata() -> SessionTelemetry { + let exporter = InMemoryMetricExporter::default(); + let metrics = MetricsClient::new( + MetricsConfig::in_memory("test", "codex-core", env!("CARGO_PKG_VERSION"), exporter) + .with_runtime_reader(), + ) + .expect("in-memory metrics client"); + SessionTelemetry::new( + ThreadId::new(), + "gpt-5.1", + "gpt-5.1", + /*account_id*/ None, + /*account_email*/ None, + /*auth_mode*/ None, + "test_originator".to_string(), + /*log_user_prompts*/ false, + "tty".to_string(), + SessionSource::Cli, + ) + .with_metrics_without_metadata_tags(metrics) +} + +fn find_metric<'a>(resource_metrics: &'a ResourceMetrics, name: &str) -> &'a Metric { + for scope_metrics in resource_metrics.scope_metrics() { + for metric in scope_metrics.metrics() { + if metric.name() == name { + return metric; + } + } + } + panic!("metric {name} missing"); +} + +fn histogram_sum(resource_metrics: &ResourceMetrics, name: &str) -> u64 { + let metric = find_metric(resource_metrics, name); + match metric.data() { + AggregatedMetrics::F64(data) => match data { + MetricData::Histogram(histogram) => { + let points: Vec<_> = histogram.data_points().collect(); + assert_eq!(points.len(), 1); + points[0].sum().round() as u64 + } + _ => panic!("unexpected histogram aggregation"), + }, + _ => panic!("unexpected metric data type"), + } +} + fn skill_message(text: &str) -> ResponseItem { ResponseItem::Message { id: None, @@ -303,6 +369,75 @@ fn user_input_texts(items: &[ResponseItem]) -> Vec<&str> { .collect() } +fn write_project_hooks(dot_codex: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dot_codex)?; + std::fs::write( + dot_codex.join("hooks.json"), + r#"{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "echo hello from hook" + } + ] + } + ] + } +}"#, + ) +} + +async fn write_project_trust_config( + codex_home: &Path, + trusted_projects: &[(&Path, TrustLevel)], +) -> std::io::Result<()> { + tokio::fs::write( + codex_home.join(codex_config::CONFIG_TOML_FILE), + toml::to_string(&ConfigToml { + projects: Some( + trusted_projects + .iter() + .map(|(project, trust_level)| { + ( + project_trust_key(project), + ProjectConfig { + trust_level: Some(*trust_level), + }, + ) + }) + .collect::>(), + ), + ..Default::default() + }) + .expect("serialize config"), + ) + .await +} + +async fn preview_session_start_hooks( + config: &crate::config::Config, +) -> std::io::Result> { + let hooks = Hooks::new(HooksConfig { + feature_enabled: true, + config_layer_stack: Some(config.config_layer_stack.clone()), + ..HooksConfig::default() + }); + + Ok( + hooks.preview_session_start(&codex_hooks::SessionStartRequest { + session_id: ThreadId::new(), + cwd: config.cwd.clone(), + transcript_path: None, + model: "gpt-5".to_string(), + permission_mode: "default".to_string(), + source: codex_hooks::SessionStartSource::Startup, + }), + ) +} + fn test_tool_runtime(session: Arc, turn_context: Arc) -> ToolCallRuntime { let router = Arc::new(ToolRouter::from_config( &turn_context.tools_config, @@ -4409,6 +4544,138 @@ async fn build_initial_context_omits_default_image_save_location_without_image_h ); } +#[tokio::test] +async fn build_initial_context_trims_skill_metadata_from_context_window_budget() { + let (session, mut turn_context) = make_session_and_context().await; + let mut outcome = SkillLoadOutcome::default(); + outcome.skills = vec![ + SkillMetadata { + name: "admin-skill".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(), + scope: SkillScope::Admin, + }, + SkillMetadata { + name: "repo-skill".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), + scope: SkillScope::Repo, + }, + ]; + turn_context.model_info.context_window = Some(100); + turn_context.turn_skills = TurnSkillsContext::new(Arc::new(outcome)); + + let initial_context = session.build_initial_context(&turn_context).await; + let developer_texts = developer_input_texts(&initial_context); + + assert!( + developer_texts + .iter() + .all(|text| !text.contains(THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE)), + "expected skill budget warning to stay out of the initial context, got {developer_texts:?}" + ); + assert!( + developer_texts + .iter() + .all(|text| !text.contains("- admin-skill:") && !text.contains("- repo-skill:")), + "expected no skill metadata entries to fit the tiny budget, got {developer_texts:?}" + ); +} + +#[test] +fn emit_thread_start_skill_metrics_records_enabled_kept_and_truncated_values() { + let session_telemetry = test_session_telemetry_without_metadata(); + let rendered = render_skills_section( + &[SkillMetadata { + name: "repo-skill".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), + scope: SkillScope::Repo, + }], + SkillMetadataBudget::Characters(1), + SkillRenderSideEffects::ThreadStart { + session_telemetry: &session_telemetry, + }, + ) + .expect("skills should render"); + + assert!(rendered.emit_warning); + let snapshot = session_telemetry + .snapshot_metrics() + .expect("runtime metrics snapshot"); + assert_eq!( + histogram_sum(&snapshot, THREAD_SKILLS_ENABLED_TOTAL_METRIC), + 1 + ); + assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_KEPT_TOTAL_METRIC), 0); + assert_eq!(histogram_sum(&snapshot, THREAD_SKILLS_TRUNCATED_METRIC), 1); +} + +#[tokio::test] +async fn build_initial_context_emits_thread_start_skill_warning_on_repeated_builds() { + let (session, turn_context, rx) = make_session_and_context_with_rx().await; + let mut turn_context = Arc::into_inner(turn_context).expect("sole turn context owner"); + let mut outcome = SkillLoadOutcome::default(); + outcome.skills = vec![ + SkillMetadata { + name: "admin-skill".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/admin-skill/SKILL.md").abs(), + scope: SkillScope::Admin, + }, + SkillMetadata { + name: "repo-skill".to_string(), + description: "desc".to_string(), + short_description: None, + interface: None, + dependencies: None, + policy: None, + path_to_skills_md: test_path_buf("/tmp/repo-skill/SKILL.md").abs(), + scope: SkillScope::Repo, + }, + ]; + turn_context.model_info.context_window = Some(100); + turn_context.turn_skills = TurnSkillsContext::new(Arc::new(outcome)); + + let _ = session.build_initial_context(&turn_context).await; + let warning_event = timeout(Duration::from_secs(1), rx.recv()) + .await + .expect("warning event should arrive") + .expect("warning event should be readable"); + assert!(matches!( + warning_event.msg, + EventMsg::Warning(WarningEvent { message }) + if message == THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE + )); + + let _ = session.build_initial_context(&turn_context).await; + let warning_event = timeout(Duration::from_secs(1), rx.recv()) + .await + .expect("warning event should arrive on repeated build") + .expect("warning event should be readable"); + assert!(matches!( + warning_event.msg, + EventMsg::Warning(WarningEvent { message }) + if message == THREAD_START_SKILLS_TRIMMED_WARNING_MESSAGE + )); +} + #[tokio::test] async fn handle_output_item_done_records_image_save_history_message() { let (session, turn_context) = make_session_and_context().await; @@ -6057,3 +6324,85 @@ fn prefix_compact_token_limit_defaults_to_sixty_percent_of_auto_compact() { fn prefix_compact_token_limit_is_none_for_tiny_auto_compact_limit() { assert_eq!(prefix_compact_token_limit(/*auto_compact_limit*/ 1), None); } + +#[tokio::test] +async fn session_start_hooks_only_load_from_trusted_project_layers() -> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let codex_home = temp.path().join("home"); + let project_root = temp.path().join("project"); + let nested = project_root.join("nested"); + let root_dot_codex = project_root.join(".codex"); + let nested_dot_codex = nested.join(".codex"); + + std::fs::create_dir_all(&codex_home)?; + std::fs::create_dir_all(&nested_dot_codex)?; + std::fs::write(project_root.join(".git"), "gitdir: here")?; + write_project_hooks(&root_dot_codex)?; + write_project_hooks(&nested_dot_codex)?; + write_project_trust_config(&codex_home, &[(&nested, TrustLevel::Trusted)]).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(nested)) + .build() + .await?; + + let preview = preview_session_start_hooks(&config).await?; + let expected_source_path = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path( + nested_dot_codex.join("hooks.json"), + )?; + assert_eq!( + preview + .iter() + .map(|run| &run.source_path) + .collect::>(), + vec![&expected_source_path], + ); + + Ok(()) +} + +#[tokio::test] +async fn session_start_hooks_require_project_trust_without_config_toml() -> std::io::Result<()> { + let temp = tempfile::tempdir()?; + let project_root = temp.path().join("project"); + let nested = project_root.join("nested"); + let dot_codex = project_root.join(".codex"); + std::fs::create_dir_all(&nested)?; + std::fs::write(project_root.join(".git"), "gitdir: here")?; + write_project_hooks(&dot_codex)?; + + let cases = [ + ("unknown", Vec::<(&Path, TrustLevel)>::new(), 0_usize), + ( + "untrusted", + vec![(&project_root as &Path, TrustLevel::Untrusted)], + 0_usize, + ), + ( + "trusted", + vec![(&project_root as &Path, TrustLevel::Trusted)], + 1_usize, + ), + ]; + + for (name, trust_entries, expected_hooks) in cases { + let codex_home = temp.path().join(format!("home_{name}")); + std::fs::create_dir_all(&codex_home)?; + write_project_trust_config(&codex_home, &trust_entries).await?; + + let config = ConfigBuilder::default() + .codex_home(codex_home) + .fallback_cwd(Some(nested.clone())) + .build() + .await?; + + assert_eq!( + preview_session_start_hooks(&config).await?.len(), + expected_hooks, + "unexpected hook count for {name}", + ); + } + + Ok(()) +} diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 29a7022a9c..4b767246c3 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -81,6 +81,7 @@ use codex_protocol::items::UserMessageItem; use codex_protocol::items::build_hook_prompt_message; use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; +use codex_protocol::models::MessagePhase; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AgentMessageContentDeltaEvent; @@ -1972,6 +1973,25 @@ async fn try_run_sampling_request( cancellation_token: cancellation_token.child_token(), }; + let preempt_for_mailbox_mail = match &item { + ResponseItem::Message { role, phase, .. } => { + role == "assistant" && matches!(phase, Some(MessagePhase::Commentary)) + } + ResponseItem::Reasoning { .. } => true, + ResponseItem::LocalShellCall { .. } + | ResponseItem::FunctionCall { .. } + | ResponseItem::ToolSearchCall { .. } + | ResponseItem::FunctionCallOutput { .. } + | ResponseItem::CustomToolCall { .. } + | ResponseItem::CustomToolCallOutput { .. } + | ResponseItem::ToolSearchOutput { .. } + | ResponseItem::WebSearchCall { .. } + | ResponseItem::ImageGenerationCall { .. } + | ResponseItem::GhostSnapshot { .. } + | ResponseItem::Compaction { .. } + | ResponseItem::Other => false, + }; + let output_result = match handle_output_item_done(&mut ctx, item, previously_active_item) .instrument(handle_responses) @@ -1987,6 +2007,13 @@ async fn try_run_sampling_request( last_agent_message = Some(agent_message); } needs_follow_up |= output_result.needs_follow_up; + // todo: remove before stabilizing multi-agent v2 + if preempt_for_mailbox_mail && sess.mailbox_rx.lock().await.has_pending() { + break Ok(SamplingRequestResult { + needs_follow_up: true, + last_agent_message, + }); + } } ResponseEvent::OutputItemAdded(item) => { if let ResponseItem::CustomToolCall { call_id, name, .. } = &item { diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index cec4ebcded..dd86804ee5 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -75,9 +75,11 @@ pub(crate) struct TurnContext { impl TurnContext { pub(crate) fn model_context_window(&self) -> Option { let effective_context_window_percent = self.model_info.effective_context_window_percent; - self.model_info.context_window.map(|context_window| { - context_window.saturating_mul(effective_context_window_percent) / 100 - }) + self.model_info + .resolved_context_window() + .map(|context_window| { + context_window.saturating_mul(effective_context_window_percent) / 100 + }) } pub(crate) fn apps_enabled(&self) -> bool { @@ -209,6 +211,8 @@ impl TurnContext { ) -> FileSystemSandboxContext { FileSystemSandboxContext { sandbox_policy: self.sandbox_policy.get().clone(), + sandbox_policy_cwd: Some(self.cwd.clone()), + file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self .config @@ -219,6 +223,19 @@ impl TurnContext { } } + fn non_legacy_file_system_sandbox_policy(&self) -> Option { + // Omit the derived split filesystem policy when it is equivalent to + // the legacy sandbox policy. This keeps turn-context payloads stable + // while both fields exist; once callers consume only the split policy, + // this comparison and the legacy projection should go away. + let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + self.sandbox_policy.get(), + &self.cwd, + ); + (self.file_system_sandbox_policy != legacy_file_system_sandbox_policy) + .then(|| self.file_system_sandbox_policy.clone()) + } + pub(crate) fn compact_prompt(&self) -> &str { self.compact_prompt .as_deref() @@ -226,18 +243,6 @@ impl TurnContext { } pub(crate) fn to_turn_context_item(&self) -> TurnContextItem { - let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - self.sandbox_policy.get(), - &self.cwd, - ); - // Omit the derived split filesystem policy when it is equivalent to - // the legacy sandbox policy. This keeps turn-context payloads stable - // while both fields exist; once callers consume only the split policy, - // this comparison and the legacy projection should go away. - let file_system_sandbox_policy = (self.file_system_sandbox_policy - != legacy_file_system_sandbox_policy) - .then(|| self.file_system_sandbox_policy.clone()); - TurnContextItem { turn_id: Some(self.sub_id.clone()), trace_id: self.trace_id.clone(), @@ -247,7 +252,7 @@ impl TurnContext { approval_policy: self.approval_policy.value(), sandbox_policy: self.sandbox_policy.get().clone(), network: self.turn_context_network_item(), - file_system_sandbox_policy, + file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(), model: self.model_info.slug.clone(), personality: self.personality, collaboration_mode: Some(self.collaboration_mode.clone()), diff --git a/codex-rs/core/src/skills.rs b/codex-rs/core/src/skills.rs index 84c0af5f3e..552587318b 100644 --- a/codex-rs/core/src/skills.rs +++ b/codex-rs/core/src/skills.rs @@ -21,11 +21,13 @@ pub use codex_core_skills::SkillError; pub use codex_core_skills::SkillLoadOutcome; pub use codex_core_skills::SkillMetadata; pub use codex_core_skills::SkillPolicy; +pub use codex_core_skills::SkillRenderReport; pub use codex_core_skills::SkillsLoadInput; pub use codex_core_skills::SkillsManager; pub use codex_core_skills::build_skill_name_counts; pub use codex_core_skills::collect_env_var_dependencies; pub use codex_core_skills::config_rules; +pub use codex_core_skills::default_skill_metadata_budget; pub use codex_core_skills::detect_implicit_skill_invocation_for_command; pub use codex_core_skills::filter_skill_load_outcome_for_product; pub use codex_core_skills::injection; @@ -37,6 +39,7 @@ pub use codex_core_skills::manager; pub use codex_core_skills::model; pub use codex_core_skills::remote; pub use codex_core_skills::render; +pub use codex_core_skills::render::SkillRenderSideEffects; pub use codex_core_skills::render_skills_section; pub use codex_core_skills::system; diff --git a/codex-rs/core/src/tools/code_mode/response_adapter.rs b/codex-rs/core/src/tools/code_mode/response_adapter.rs index b90448acf9..e20cf6a071 100644 --- a/codex-rs/core/src/tools/code_mode/response_adapter.rs +++ b/codex-rs/core/src/tools/code_mode/response_adapter.rs @@ -1,4 +1,5 @@ use codex_code_mode::ImageDetail as CodeModeImageDetail; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::ImageDetail; @@ -36,7 +37,9 @@ impl IntoProtocol codex_code_mode::FunctionCallOutputContentItem::InputImage { image_url, detail } => { FunctionCallOutputContentItem::InputImage { image_url, - detail: detail.map(IntoProtocol::into_protocol), + detail: detail + .map(IntoProtocol::into_protocol) + .or(Some(DEFAULT_IMAGE_DETAIL)), } } } diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index a2f3a7f7c6..4e144b5507 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -7,6 +7,7 @@ use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE; use crate::turn_diff_tracker::TurnDiffTracker; use crate::unified_exec::resolve_max_tokens; use codex_protocol::mcp::CallToolResult; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -463,10 +464,10 @@ pub(crate) fn response_input_to_code_mode_result(response: ResponseInputItem) -> | codex_protocol::models::ContentItem::OutputText { text } => { FunctionCallOutputContentItem::InputText { text } } - codex_protocol::models::ContentItem::InputImage { image_url } => { + codex_protocol::models::ContentItem::InputImage { image_url, detail } => { FunctionCallOutputContentItem::InputImage { image_url, - detail: None, + detail: detail.or(Some(DEFAULT_IMAGE_DETAIL)), } } }) diff --git a/codex-rs/core/src/tools/context_tests.rs b/codex-rs/core/src/tools/context_tests.rs index 8df9159edd..c62328dff5 100644 --- a/codex-rs/core/src/tools/context_tests.rs +++ b/codex-rs/core/src/tools/context_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use core_test_support::assert_regex_match; use pretty_assertions::assert_eq; use serde_json::json; @@ -173,7 +174,7 @@ fn mcp_tool_output_response_item_preserves_content_items() { }, FunctionCallOutputContentItem::InputImage { image_url: image_url.to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ] .as_slice() @@ -239,7 +240,7 @@ fn custom_tool_calls_can_derive_text_from_content_items() { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), @@ -259,7 +260,7 @@ fn custom_tool_calls_can_derive_text_from_content_items() { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index 33ce2054ac..8f3f69701f 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -1,3 +1,4 @@ +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; @@ -133,7 +134,11 @@ impl ToolHandler for ViewImageHandler { } else { PromptImageMode::ResizeToFit }; - let image_detail = use_original_detail.then_some(ImageDetail::Original); + let image_detail = Some(if use_original_detail { + ImageDetail::Original + } else { + DEFAULT_IMAGE_DETAIL + }); let image = load_for_prompt_bytes(abs_path.as_path(), file_bytes, image_mode).map_err(|error| { @@ -210,7 +215,7 @@ mod tests { fn code_mode_result_returns_image_url_object() { let output = ViewImageOutput { image_url: "data:image/png;base64,AAA".to_string(), - image_detail: None, + image_detail: Some(DEFAULT_IMAGE_DETAIL), }; let result = output.code_mode_result(&ToolPayload::Function { @@ -221,7 +226,7 @@ mod tests { result, json!({ "image_url": "data:image/png;base64,AAA", - "detail": null, + "detail": "high", }) ); } diff --git a/codex-rs/core/src/tools/js_repl/kernel.js b/codex-rs/core/src/tools/js_repl/kernel.js index 3e5cf855f7..3eb3e916ce 100644 --- a/codex-rs/core/src/tools/js_repl/kernel.js +++ b/codex-rs/core/src/tools/js_repl/kernel.js @@ -1225,9 +1225,9 @@ function parseImageDetail(detail) { if (typeof detail !== "string" || !detail) { throw new Error("codex.emitImage expected detail to be a non-empty string"); } - if (detail !== "original") { + if (!["auto", "low", "high", "original"].includes(detail)) { throw new Error( - 'codex.emitImage only supports detail "original"; omit detail for default behavior', + 'codex.emitImage expected detail to be one of "auto", "low", "high", or "original"', ); } return detail; @@ -1331,10 +1331,17 @@ function normalizeMcpImageData(data, mimeType) { } function parseMcpImageDetail(meta) { - if (!isPlainObject(meta) || meta["codex/imageDetail"] !== "original") { + if (!isPlainObject(meta)) { return undefined; } - return "original"; + const detail = meta["codex/imageDetail"]; + if ( + typeof detail !== "string" || + !["auto", "low", "high", "original"].includes(detail) + ) { + return undefined; + } + return detail; } function parseMcpToolResult(result) { diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index deaab9c2f3..23f4906e5f 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -10,6 +10,7 @@ use std::time::Duration; use codex_protocol::ThreadId; use codex_protocol::models::ContentItem; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ImageDetail; @@ -1750,7 +1751,8 @@ fn emitted_image_content_item( ) -> FunctionCallOutputContentItem { FunctionCallOutputContentItem::InputImage { image_url, - detail: normalize_output_image_detail(&turn.model_info, detail), + detail: normalize_output_image_detail(&turn.model_info, detail) + .or(Some(DEFAULT_IMAGE_DETAIL)), } } diff --git a/codex-rs/core/src/tools/js_repl/mod_tests.rs b/codex-rs/core/src/tools/js_repl/mod_tests.rs index 128fda60dd..af53a59757 100644 --- a/codex-rs/core/src/tools/js_repl/mod_tests.rs +++ b/codex-rs/core/src/tools/js_repl/mod_tests.rs @@ -5,6 +5,7 @@ use crate::turn_diff_tracker::TurnDiffTracker; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ImageDetail; @@ -253,7 +254,7 @@ fn summarize_tool_call_response_for_multimodal_function_output() { output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,abcd".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -277,7 +278,7 @@ fn summarize_tool_call_response_for_multimodal_function_output() { } #[tokio::test] -async fn emitted_image_content_item_drops_unsupported_explicit_detail() { +async fn emitted_image_content_item_preserves_explicit_non_original_detail() { let (_session, turn) = make_session_and_context().await; let content_item = emitted_image_content_item( &turn, @@ -288,7 +289,7 @@ async fn emitted_image_content_item_drops_unsupported_explicit_detail() { content_item, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(ImageDetail::Low), } ); } @@ -314,7 +315,7 @@ async fn emitted_image_content_item_allows_explicit_original_detail_when_support } #[tokio::test] -async fn emitted_image_content_item_drops_explicit_original_detail_when_unsupported() { +async fn emitted_image_content_item_defaults_to_high_for_unsupported_original_detail() { let (_session, turn) = make_session_and_context().await; let content_item = emitted_image_content_item( @@ -327,7 +328,7 @@ async fn emitted_image_content_item_drops_explicit_original_detail_when_unsuppor content_item, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), } ); } @@ -356,7 +357,7 @@ fn summarize_tool_call_response_for_multimodal_custom_output() { output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,abcd".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -1213,7 +1214,7 @@ console.log(out.type); image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" .to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] .as_slice() ); @@ -1268,7 +1269,7 @@ await codex.emitImage({ bytes: png, mimeType: "image/png" }); image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" .to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] .as_slice() ); @@ -1325,13 +1326,13 @@ await codex.emitImage( image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" .to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/gif;base64,R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=" .to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ] .as_slice() @@ -1387,7 +1388,7 @@ console.log("cell-complete"); image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==" .to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] .as_slice() ); @@ -1465,11 +1466,11 @@ console.log("helpers-ran"); vec![ FunctionCallOutputContentItem::InputImage { image_url: data_url.to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputImage { image_url: data_url.to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ] ); @@ -1701,7 +1702,7 @@ await codex.emitImage("DATA:image/png;base64,AAA"); result.content_items.as_slice(), [FunctionCallOutputContentItem::InputImage { image_url: "DATA:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] .as_slice() ); @@ -1751,10 +1752,7 @@ await codex.emitImage({ bytes: png, mimeType: "image/png", detail: "ultra" }); ) .await .expect_err("invalid detail should fail"); - assert!( - err.to_string() - .contains("only supports detail \"original\"") - ); + assert!(err.to_string().contains("expected detail to be one of")); assert!(session.get_pending_input().await.is_empty()); Ok(()) @@ -1804,7 +1802,7 @@ await codex.emitImage({ bytes: png, mimeType: "image/png", detail: null }); result.content_items.as_slice(), [FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] .as_slice() ); diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 1d5c8a1e36..aac8e6bc13 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -22,6 +22,7 @@ use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -74,8 +75,18 @@ impl ApplyPatchRuntime { return None; } + let legacy_file_system_sandbox_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( + attempt.policy, + attempt.sandbox_cwd, + ); + let file_system_sandbox_policy = (attempt.file_system_policy + != &legacy_file_system_sandbox_policy) + .then(|| attempt.file_system_policy.clone()); + Some(FileSystemSandboxContext { sandbox_policy: attempt.policy.clone(), + sandbox_policy_cwd: Some(attempt.sandbox_cwd.clone()), + file_system_sandbox_policy, windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, use_legacy_landlock: attempt.use_legacy_landlock, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 0ba3e131af..0511ca91b6 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -3,6 +3,9 @@ use crate::tools::sandboxing::SandboxAttempt; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::GranularApprovalConfig; @@ -99,7 +102,12 @@ fn file_system_sandbox_context_uses_active_attempt() { permissions_preapproved: false, }; let sandbox_policy = SandboxPolicy::new_read_only_policy(); - let file_system_policy = FileSystemSandboxPolicy::from(&sandbox_policy); + let mut file_system_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, path.as_path()); + file_system_policy.entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Path { path: path.clone() }, + access: FileSystemAccessMode::None, + }); let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::MacosSeatbelt, @@ -119,6 +127,11 @@ fn file_system_sandbox_context_uses_active_attempt() { .expect("sandbox context"); assert_eq!(sandbox.sandbox_policy, sandbox_policy); + assert_eq!(sandbox.sandbox_policy_cwd, Some(path.clone())); + assert_eq!( + sandbox.file_system_sandbox_policy, + Some(file_system_policy.clone()) + ); assert_eq!(sandbox.additional_permissions, Some(additional_permissions)); assert_eq!( sandbox.windows_sandbox_level, @@ -128,6 +141,47 @@ fn file_system_sandbox_context_uses_active_attempt() { assert_eq!(sandbox.use_legacy_landlock, true); } +#[test] +fn file_system_sandbox_context_omits_legacy_equivalent_policy() { + let path = std::env::temp_dir() + .join("apply-patch-runtime-legacy-equivalent.txt") + .abs(); + let req = ApplyPatchRequest { + action: ApplyPatchAction::new_add_for_test(&path, "hello".to_string()), + file_paths: vec![path.clone()], + changes: HashMap::new(), + exec_approval_requirement: ExecApprovalRequirement::Skip { + bypass_sandbox: false, + proposed_execpolicy_amendment: None, + }, + additional_permissions: None, + permissions_preapproved: false, + }; + let sandbox_policy = SandboxPolicy::new_read_only_policy(); + let file_system_policy = + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, path.as_path()); + let manager = SandboxManager::new(); + let attempt = SandboxAttempt { + sandbox: SandboxType::MacosSeatbelt, + policy: &sandbox_policy, + file_system_policy: &file_system_policy, + network_policy: NetworkSandboxPolicy::Restricted, + enforce_managed_network: false, + manager: &manager, + sandbox_cwd: &path, + codex_linux_sandbox_exe: None, + use_legacy_landlock: true, + windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, + windows_sandbox_private_desktop: true, + }; + + let sandbox = ApplyPatchRuntime::file_system_sandbox_context_for_attempt(&req, &attempt) + .expect("sandbox context"); + + assert_eq!(sandbox.sandbox_policy_cwd, Some(path)); + assert_eq!(sandbox.file_system_sandbox_policy, None); +} + #[test] fn no_sandbox_attempt_has_no_file_system_context() { let path = std::env::temp_dir() diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index b5087de5a5..33bc8d378b 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -24,6 +24,7 @@ use codex_protocol::config_types::Settings; use codex_protocol::config_types::Verbosity; use codex_protocol::error::CodexErr; use codex_protocol::models::ContentItem; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ImageDetail; @@ -511,6 +512,7 @@ async fn resume_replays_legacy_js_repl_image_rollout_shapes() { role: "user".to_string(), content: vec![ContentItem::InputImage { image_url: legacy_image_url.to_string(), + detail: Some(DEFAULT_IMAGE_DETAIL), }], end_turn: None, phase: None, diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 474d221d27..383e6050e2 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -1970,14 +1970,16 @@ image("data:image/png;base64,AAA"); items[1], serde_json::json!({ "type": "input_image", - "image_url": "https://example.com/image.jpg" + "image_url": "https://example.com/image.jpg", + "detail": "high" }), ); assert_eq!( items[2], serde_json::json!({ "type": "input_image", - "image_url": "data:image/png;base64,AAA" + "image_url": "data:image/png;base64,AAA", + "detail": "high" }), ); diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 8195bd0a86..a7ec8318f1 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -1,5 +1,6 @@ use anyhow::Context; use codex_protocol::models::ContentItem; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -51,7 +52,7 @@ fn find_user_message_with_image(text: &str) -> Option { fn extract_image_url(item: &ResponseItem) -> Option { match item { ResponseItem::Message { content, .. } => content.iter().find_map(|span| match span { - ContentItem::InputImage { image_url } => Some(image_url.clone()), + ContentItem::InputImage { image_url, .. } => Some(image_url.clone()), _ => None, }), _ => None, @@ -150,7 +151,10 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu ContentItem::InputText { text: codex_protocol::models::local_image_open_tag_text(/*label_number*/ 1), }, - ContentItem::InputImage { image_url }, + ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }, ContentItem::InputText { text: codex_protocol::models::image_close_tag_text(), }, @@ -234,7 +238,10 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> ContentItem::InputText { text: codex_protocol::models::image_open_tag_text(), }, - ContentItem::InputImage { image_url }, + ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }, ContentItem::InputText { text: codex_protocol::models::image_close_tag_text(), }, diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index 2b50a755e5..10b97721aa 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -97,6 +97,7 @@ fn test_model_info( supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), @@ -914,6 +915,7 @@ async fn model_switch_to_smaller_model_updates_token_context_window() -> Result< supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(large_context_window), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index 9dfc32fc73..bb0ecc85f9 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -350,6 +350,7 @@ fn test_remote_model(slug: &str, priority: i32) -> ModelInfo { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 2376da3d6c..6426059b12 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -321,7 +321,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn queued_inter_agent_mail_waits_for_request_boundary_after_reasoning_item() { +async fn queued_inter_agent_mail_triggers_follow_up_after_reasoning_item() { let (gate_reasoning_done_tx, gate_reasoning_done_rx) = oneshot::channel(); let first_chunks = vec![ @@ -331,18 +331,14 @@ async fn queued_inter_agent_mail_waits_for_request_boundary_after_reasoning_item gate_reasoning_done_rx, vec![ ev_reasoning_item("reason-1", &["thinking"], &[]), - ev_message_item_added("msg-preserved", ""), - ev_output_text_delta("preserved commentary"), - json!({ - "type": "response.output_item.done", - "item": { - "type": "message", - "role": "assistant", - "id": "msg-preserved", - "content": [{"type": "output_text", "text": "preserved commentary"}], - "phase": "commentary", - } - }), + ev_function_call( + "call-stale", + "shell", + r#"{"command":"echo stale tool call"}"#, + ), + ev_message_item_added("msg-stale", ""), + ev_output_text_delta("stale final"), + ev_message_item_done("msg-stale", "stale final"), ev_completed("resp-1"), ], ), @@ -370,7 +366,7 @@ async fn queued_inter_agent_mail_waits_for_request_boundary_after_reasoning_item } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn queued_inter_agent_mail_waits_for_request_boundary_after_commentary_message_item() { +async fn queued_inter_agent_mail_triggers_follow_up_after_commentary_message_item() { let (gate_message_done_tx, gate_message_done_rx) = oneshot::channel(); let first_chunks = vec![ @@ -379,18 +375,25 @@ async fn queued_inter_agent_mail_waits_for_request_boundary_after_commentary_mes gated_chunk( gate_message_done_rx, vec![ - ev_output_text_delta("first commentary"), + ev_output_text_delta("first answer"), json!({ "type": "response.output_item.done", "item": { "type": "message", "role": "assistant", "id": "msg-1", - "content": [{"type": "output_text", "text": "first commentary"}], + "content": [{"type": "output_text", "text": "first answer"}], "phase": "commentary", } }), - ev_function_call("call-preserved", "test_tool", "{}"), + ev_function_call( + "call-stale", + "shell", + r#"{"command":"echo stale tool call"}"#, + ), + ev_message_item_added("msg-stale", ""), + ev_output_text_delta("stale final"), + ev_message_item_done("msg-stale", "stale final"), ev_completed("resp-1"), ], ), @@ -416,7 +419,7 @@ async fn queued_inter_agent_mail_waits_for_request_boundary_after_commentary_mes let _ = gate_message_done_tx.send(()); - wait_for_agent_message(&codex, "first commentary").await; + wait_for_agent_message(&codex, "first answer").await; wait_for_turn_complete(&codex).await; diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 2bcd074189..53cb8f9bf7 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -666,6 +666,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(128_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), @@ -783,6 +784,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(128_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 80e725ab8a..6feb18a2e3 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -116,6 +116,233 @@ async fn remote_models_get_model_info_uses_longest_matching_prefix() -> Result<( Ok(()) } +/// Scenario: the model advertises a default 273k context window and a 400k max +/// context window, and the user explicitly configures 1M. This verifies the +/// runtime turn clamps the override to the advertised max window. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_models_config_context_window_override_clamps_to_max_context_window() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = MockServer::start().await; + let requested_model = "gpt-5.4-test"; + let mut remote_model = + test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000); + remote_model.context_window = Some(273_000); + remote_model.max_context_window = Some(400_000); + remote_model.effective_context_window_percent = 100; + mount_models_once( + &server, + ModelsResponse { + models: vec![remote_model], + }, + ) + .await; + mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let TestCodex { + codex, cwd, config, .. + } = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(|config| { + config.model = Some(requested_model.to_string()); + config.model_context_window = Some(1_000_000); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "check context window".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: cwd.path().to_path_buf(), + approval_policy: config.permissions.approval_policy.value(), + approvals_reviewer: None, + sandbox_policy: config.permissions.sandbox_policy.get().clone(), + model: requested_model.to_string(), + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + let turn_started_event = wait_for_event(&codex, |event| { + matches!( + event, + EventMsg::TurnStarted(started) + if started.model_context_window == Some(400_000) + ) + }) + .await; + let EventMsg::TurnStarted(turn_started) = turn_started_event else { + unreachable!("wait_for_event returned unexpected event"); + }; + + assert_eq!(turn_started.model_context_window, Some(400_000)); + + Ok(()) +} + +/// Scenario: the user explicitly configures a context window above the model's +/// max_context_window. This verifies the runtime window is clamped to the max +/// instead of using the oversized config value. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_models_config_override_above_max_uses_max_context_window() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = MockServer::start().await; + let requested_model = "gpt-5.4-test"; + let mut remote_model = + test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000); + remote_model.context_window = Some(273_000); + remote_model.max_context_window = Some(400_000); + remote_model.effective_context_window_percent = 100; + mount_models_once( + &server, + ModelsResponse { + models: vec![remote_model], + }, + ) + .await; + mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let TestCodex { + codex, cwd, config, .. + } = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(|config| { + config.model = Some(requested_model.to_string()); + config.model_context_window = Some(500_000); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "check context window".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: cwd.path().to_path_buf(), + approval_policy: config.permissions.approval_policy.value(), + approvals_reviewer: None, + sandbox_policy: config.permissions.sandbox_policy.get().clone(), + model: requested_model.to_string(), + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + let turn_started_event = wait_for_event(&codex, |event| { + matches!( + event, + EventMsg::TurnStarted(started) + if started.model_context_window == Some(400_000) + ) + }) + .await; + let EventMsg::TurnStarted(turn_started) = turn_started_event else { + unreachable!("wait_for_event returned unexpected event"); + }; + + assert_eq!(turn_started.model_context_window, Some(400_000)); + + Ok(()) +} + +/// Scenario: model metadata includes both context_window and max_context_window, +/// but the user did not configure an override. This verifies the runtime keeps +/// using the model's default context_window in the no-override path. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_models_use_context_window_when_config_override_is_absent() -> Result<()> { + skip_if_no_network!(Ok(())); + skip_if_sandbox!(Ok(())); + + let server = MockServer::start().await; + let requested_model = "gpt-5.4-test"; + let mut remote_model = + test_remote_model("gpt-5.4", ModelVisibility::List, /*priority*/ 1_000); + remote_model.context_window = Some(273_000); + remote_model.max_context_window = Some(400_000); + remote_model.effective_context_window_percent = 100; + mount_models_once( + &server, + ModelsResponse { + models: vec![remote_model], + }, + ) + .await; + mount_sse_once( + &server, + sse(vec![ev_response_created("resp-1"), ev_completed("resp-1")]), + ) + .await; + + let TestCodex { + codex, cwd, config, .. + } = test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(|config| { + config.model = Some(requested_model.to_string()); + }) + .build(&server) + .await?; + + codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "check context window".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: cwd.path().to_path_buf(), + approval_policy: config.permissions.approval_policy.value(), + approvals_reviewer: None, + sandbox_policy: config.permissions.sandbox_policy.get().clone(), + model: requested_model.to_string(), + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + let turn_started_event = wait_for_event(&codex, |event| { + matches!( + event, + EventMsg::TurnStarted(started) + if started.model_context_window == Some(273_000) + ) + }) + .await; + let EventMsg::TurnStarted(turn_started) = turn_started_event else { + unreachable!("wait_for_event returned unexpected event"); + }; + + assert_eq!(turn_started.model_context_window, Some(273_000)); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_models_long_model_slug_is_sent_with_high_reasoning() -> Result<()> { skip_if_no_network!(Ok(())); @@ -312,6 +539,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), @@ -561,6 +789,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), @@ -1044,6 +1273,7 @@ fn test_remote_model_with_policy( supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 8c7e7133a3..7875f09810 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1,17 +1,26 @@ +#![allow(clippy::expect_used)] + +use anyhow::Context as _; +use anyhow::ensure; use std::collections::HashMap; use std::ffi::OsStr; use std::ffi::OsString; use std::fs; use std::net::TcpListener; use std::path::Path; +use std::path::PathBuf; +use std::process::Command as StdCommand; use std::sync::Arc; +use std::sync::Mutex; use std::time::Duration; use std::time::SystemTime; use std::time::UNIX_EPOCH; use codex_config::types::McpServerConfig; +use codex_config::types::McpServerEnvVar; use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; +use codex_exec_server::CreateDirectoryOptions; use codex_features::Feature; use codex_login::CodexAuth; use codex_mcp::MCP_SANDBOX_STATE_META_CAPABILITY; @@ -34,6 +43,7 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; use codex_utils_cargo_bin::cargo_bin; use core_test_support::assert_regex_match; +use core_test_support::remote_env_env_var; use core_test_support::responses; use core_test_support::responses::ev_custom_tool_call; use core_test_support::responses::mount_models_once; @@ -54,6 +64,7 @@ use tokio::process::Child; use tokio::process::Command; use tokio::time::Instant; use tokio::time::sleep; +use wiremock::MockServer; static OPENAI_PNG: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAD0AAAA9CAYAAAAeYmHpAAAE6klEQVR4Aeyau44UVxCGx1fZsmRLlm3Zoe0XcGQ5cUiCCIgJeS9CHgAhMkISQnIuGQgJEkBcxLW+nqnZ6uqqc+nuWRC7q/P3qetf9e+MtOwyX25O4Nep6JPyop++0qev9HrfgZ+F6r2DuB/vHOrt/UIkqdDHYvujOW6fO7h/CNEI+a5jc+pBR8uy0jVFsziYu5HtfSUk+Io34q921hLNctFSX0gwww+S8wce8K1LfCU+cYW4888aov8NxqvQILUPPReLOrm6zyLxa4i+6VZuFbJo8d1MOHZm+7VUtB/aIvhPWc/3SWg49JcwFLlHxuXKjtyloo+YNhuW3VS+WPBuUEMvCFKjEDVgFBQHXrnazpqiSxNZCkQ1kYiozsbm9Oz7l4i2Il7vGccGNWAc3XosDrZe/9P3ZnMmzHNEQw4smf8RQ87XEAMsC7Az0Au+dgXerfH4+sHvEc0SYGic8WBBUGqFH2gN7yDrazy7m2pbRTeRmU3+MjZmr1h6LJgPbGy23SI6GlYT0brQ71IY8Us4PNQCm+zepSbaD2BY9xCaAsD9IIj/IzFmKMSdHHonwdZATbTnYREf6/VZGER98N9yCWIvXQwXDoDdhZJoT8jwLnJXDB9w4Sb3e6nK5ndzlkTLnP3JBu4LKkbrYrU69gCVceV0JvpyuW1xlsUVngzhwMetn/XamtTORF9IO5YnWNiyeF9zCAfqR3fUW+vZZKLtgP+ts8BmQRBREAdRDhH3o8QuRh/YucNFz2BEjxbRN6LGzphfKmvP6v6QhqIQyZ8XNJ0W0X83MR1PEcJBNO2KC2Z1TW/v244scp9FwRViZxIOBF0Lctk7ZVSavdLvRlV1hz/ysUi9sr8CIcB3nvWBwA93ykTz18eAYxQ6N/K2DkPA1lv3iXCwmDUT7YkjIby9siXueIJj9H+pzSqJ9oIuJWTUgSSt4WO7o/9GGg0viR4VinNRUDoIj34xoCd6pxD3aK3zfdbnx5v1J3ZNNEJsE0sBG7N27ReDrJc4sFxz7dI/ZAbOmmiKvHBitQXpAdR6+F7v+/ol/tOouUV01EeMZQF2BoQDn6dP4XNr+j9GZEtEK1/L8pFw7bd3a53tsTa7WD+054jOFmPg1XBKPQgnqFfmFcy32ZRvjmiIIQTYFvyDxQ8nH8WIwwGwlyDjDznnilYyFr6njrlZwsKkBpO59A7OwgdzPEWRm+G+oeb7IfyNuzjEEVLrOVxJsxvxwF8kmCM6I2QYmJunz4u4TrADpfl7mlbRTWQ7VmrBzh3+C9f6Grc3YoGN9dg/SXFthpRsT6vobfXRs2VBlgBHXVMLHjDNbIZv1sZ9+X3hB09cXdH1JKViyG0+W9bWZDa/r2f9zAFR71sTzGpMSWz2iI4YssWjWo3REy1MDGjdwe5e0dFSiAC1JakBvu4/CUS8Eh6dqHdU0Or0ioY3W5ClSqDXAy7/6SRfgw8vt4I+tbvvNtFT2kVDhY5+IGb1rCqYaXNF08vSALsXCPmt0kQNqJT1p5eI1mkIV/BxCY1z85lOzeFbPBQHURkkPTlwTYK9gTVE25l84IbFFN+YJDHjdpn0gq6mrHht0dkcjbM4UL9283O5p77GN+SPW/QwVB4IUYg7Or+Kp7naR6qktP98LNF2UxWo9yObPIT9KYg+hK4i56no4rfnM0qeyFf6AwAAAP//trwR3wAAAAZJREFUAwBZ0sR75itw5gAAAABJRU5ErkJggg=="; @@ -86,6 +97,73 @@ enum McpCallEvent { End(String), } +const REMOTE_MCP_ENVIRONMENT: &str = "remote"; + +fn remote_aware_experimental_environment() -> Option { + // These tests run locally in normal CI and against the Docker-backed + // executor in full-ci. Match that shared test environment instead of + // parameterizing each stdio MCP test with its own local/remote cases. + std::env::var_os(remote_env_env_var()).map(|_| REMOTE_MCP_ENVIRONMENT.to_string()) +} + +/// Returns the stdio MCP test server command path for the active test placement. +/// +/// Local test runs can execute the host-built test binary directly. Remote-aware +/// runs start MCP stdio through the executor inside Docker, so the host path +/// would be meaningless to the process that actually launches the server. When +/// the remote test environment is active, copy the binary into the executor +/// container and return that in-container path instead. +fn remote_aware_stdio_server_bin() -> anyhow::Result { + let bin = stdio_server_bin()?; + let Some(container_name) = std::env::var_os(remote_env_env_var()) else { + return Ok(bin); + }; + let container_name = container_name + .into_string() + .map_err(|value| anyhow::anyhow!("remote env container name must be utf-8: {value:?}"))?; + + // Keep the Docker path rewrite scoped to tests that use `build_remote_aware`. + // Other MCP tests still start their stdio server from the orchestrator test + // process, even when the full-ci remote env is present. + // + // Remote-aware MCP tests run the executor inside Docker. The stdio test + // server is built on the host, so hand the executor a copied in-container + // path instead of the host build artifact path. + // Several remote-aware MCP tests can run in parallel; give each copied + // binary its own path so one test cannot replace another test's executable. + let unique_suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let remote_path = format!( + "/tmp/codex-remote-env/test_stdio_server-{}-{unique_suffix}", + std::process::id() + ); + let container_target = format!("{container_name}:{remote_path}"); + let copy_output = StdCommand::new("docker") + .arg("cp") + .arg(&bin) + .arg(&container_target) + .output() + .with_context(|| format!("copy {bin} to remote MCP test env"))?; + ensure!( + copy_output.status.success(), + "docker cp test_stdio_server failed: stdout={} stderr={}", + String::from_utf8_lossy(©_output.stdout).trim(), + String::from_utf8_lossy(©_output.stderr).trim() + ); + + let chmod_output = StdCommand::new("docker") + .args(["exec", &container_name, "chmod", "+x", remote_path.as_str()]) + .output() + .context("mark remote test_stdio_server executable")?; + ensure!( + chmod_output.status.success(), + "docker chmod test_stdio_server failed: stdout={} stderr={}", + String::from_utf8_lossy(&chmod_output.stdout).trim(), + String::from_utf8_lossy(&chmod_output.stderr).trim() + ); + + Ok(remote_path) +} + async fn wait_for_mcp_tool(fixture: &TestCodex, tool_name: &str) -> anyhow::Result<()> { let tools_ready_deadline = Instant::now() + Duration::from_secs(30); loop { @@ -115,6 +193,7 @@ async fn wait_for_mcp_tool(fixture: &TestCodex, tool_name: &str) -> anyhow::Resu #[derive(Default)] struct TestMcpServerOptions { + experimental_environment: Option, supports_parallel_tool_calls: bool, tool_timeout_sec: Option, } @@ -122,14 +201,23 @@ struct TestMcpServerOptions { fn stdio_transport( command: String, env: Option>, - env_vars: Vec, + env_vars: Vec, +) -> McpServerTransportConfig { + stdio_transport_with_cwd(command, env, env_vars, /*cwd*/ None) +} + +fn stdio_transport_with_cwd( + command: String, + env: Option>, + env_vars: Vec, + cwd: Option, ) -> McpServerTransportConfig { McpServerTransportConfig::Stdio { command, args: Vec::new(), env, env_vars, - cwd: None, + cwd, } } @@ -144,7 +232,7 @@ fn insert_mcp_server( server_name.to_string(), McpServerConfig { transport, - experimental_environment: None, + experimental_environment: options.experimental_environment, enabled: true, required: false, supports_parallel_tool_calls: options.supports_parallel_tool_calls, @@ -164,6 +252,105 @@ fn insert_mcp_server( } } +async fn call_cwd_tool( + server: &MockServer, + fixture: &TestCodex, + server_name: &str, + call_id: &str, +) -> anyhow::Result { + let namespace = format!("mcp__{server_name}__"); + mount_sse_once( + server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call_with_namespace(call_id, &namespace, "cwd", r#"{}"#), + responses::ev_completed("resp-1"), + ]), + ) + .await; + mount_sse_once( + server, + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "rmcp cwd tool completed successfully."), + responses::ev_completed("resp-2"), + ]), + ) + .await; + + let session_model = fixture.session_configured.model.clone(); + fixture + .codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "call the rmcp cwd tool".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: fixture.config.cwd.to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: session_model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallBegin(_)) + }) + .await; + let end_event = wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallEnd(_)) + }) + .await; + let EventMsg::McpToolCallEnd(end) = end_event else { + unreachable!("event guard guarantees McpToolCallEnd"); + }; + let structured_content = end + .result + .as_ref() + .expect("rmcp cwd tool should return success") + .structured_content + .as_ref() + .expect("structured content") + .clone(); + + wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + Ok(structured_content) +} + +fn assert_cwd_tool_output(structured: &Value, expected_cwd: &Path) { + let actual_cwd = structured + .get("cwd") + .and_then(Value::as_str) + .expect("cwd tool should return a string cwd"); + + if std::env::var_os(remote_env_env_var()).is_some() { + assert_eq!( + structured, + &json!({ + "cwd": expected_cwd.to_string_lossy(), + }) + ); + return; + } + + // Local Windows can report the same absolute directory through an 8.3 path. + // Canonical paths keep the assertion focused on cwd precedence. + assert_eq!( + Path::new(actual_cwd) + .canonicalize() + .expect("cwd tool path should exist"), + expected_cwd + .canonicalize() + .expect("expected cwd should exist"), + ); +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[serial(mcp_test_value)] async fn stdio_server_round_trip() -> anyhow::Result<()> { @@ -199,7 +386,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { .await; let expected_env_value = "propagated-env"; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -214,10 +401,13 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { )])), Vec::new(), ), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -314,6 +504,118 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial(mcp_cwd)] +async fn stdio_server_uses_configured_cwd_before_runtime_fallback() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let server_name = "rmcp_configured_cwd"; + let expected_cwd = Arc::new(Mutex::new(None::)); + let expected_cwd_for_config = Arc::clone(&expected_cwd); + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; + + let fixture = test_codex() + .with_workspace_setup(|cwd, fs| async move { + fs.create_directory( + &cwd.join("mcp-configured-cwd"), + CreateDirectoryOptions { recursive: true }, + /*sandbox*/ None, + ) + .await?; + Ok::<(), anyhow::Error>(()) + }) + .with_config(move |config| { + let configured_cwd = config.cwd.join("mcp-configured-cwd").into_path_buf(); + *expected_cwd_for_config + .lock() + .expect("expected cwd lock should not be poisoned") = Some(configured_cwd.clone()); + insert_mcp_server( + config, + server_name, + stdio_transport_with_cwd( + rmcp_test_server_bin, + Some(HashMap::from([( + "MCP_TEST_VALUE".to_string(), + "configured-cwd".to_string(), + )])), + Vec::new(), + Some(configured_cwd), + ), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, + ); + }) + .build_remote_aware(&server) + .await?; + + let expected_cwd = expected_cwd + .lock() + .expect("expected cwd lock should not be poisoned") + .clone() + .expect("test config should record configured MCP cwd"); + let structured = call_cwd_tool(&server, &fixture, server_name, "call-configured-cwd").await?; + + assert_cwd_tool_output(&structured, &expected_cwd); + server.verify().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial(mcp_cwd)] +async fn remote_stdio_server_uses_runtime_fallback_cwd_when_config_omits_cwd() -> anyhow::Result<()> +{ + skip_if_no_network!(Ok(())); + if std::env::var_os(remote_env_env_var()).is_none() { + return Ok(()); + } + + let server = responses::start_mock_server().await; + let server_name = "rmcp_fallback_cwd"; + let expected_cwd = Arc::new(Mutex::new(None::)); + let expected_cwd_for_config = Arc::clone(&expected_cwd); + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; + + let fixture = test_codex() + .with_config(move |config| { + *expected_cwd_for_config + .lock() + .expect("expected cwd lock should not be poisoned") = + Some(config.cwd.to_path_buf()); + insert_mcp_server( + config, + server_name, + stdio_transport( + rmcp_test_server_bin, + Some(HashMap::from([( + "MCP_TEST_VALUE".to_string(), + "fallback-cwd".to_string(), + )])), + Vec::new(), + ), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, + ); + }) + .build_remote_aware(&server) + .await?; + + let expected_cwd = expected_cwd + .lock() + .expect("expected cwd lock should not be poisoned") + .clone() + .expect("test config should record runtime fallback cwd"); + let structured = call_cwd_tool(&server, &fixture, server_name, "call-fallback-cwd").await?; + + assert_cwd_tool_output(&structured, &expected_cwd); + server.verify().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -343,17 +645,20 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> ) .await; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { insert_mcp_server( config, server_name, stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let tools_ready_deadline = Instant::now() + Duration::from_secs(30); @@ -415,7 +720,7 @@ async fn stdio_mcp_tool_call_includes_sandbox_state_meta() -> anyhow::Result<()> ); assert_eq!( sandbox_meta.get("sandboxCwd").and_then(Value::as_str), - fixture.cwd.path().to_str() + fixture.config.cwd.as_path().to_str() ); assert_eq!(sandbox_meta.get("useLegacyLandlock"), Some(&json!(false))); @@ -455,7 +760,7 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow:: ) .await; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -464,12 +769,13 @@ async fn stdio_mcp_parallel_tool_calls_default_false_runs_serially() -> anyhow:: server_name, stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()), TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), tool_timeout_sec: Some(Duration::from_secs(2)), ..Default::default() }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -586,7 +892,7 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res ) .await; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -595,12 +901,13 @@ async fn stdio_mcp_parallel_tool_calls_opt_in_runs_concurrently() -> anyhow::Res server_name, stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()), TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), supports_parallel_tool_calls: true, tool_timeout_sec: Some(Duration::from_secs(2)), }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -676,7 +983,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { .await; // Build the stdio rmcp server and pass the image as data URL so it can construct ImageContent. - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -691,10 +998,13 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { )])), Vec::new(), ), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -787,7 +1097,8 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { output[1], json!({ "type": "input_image", - "image_url": OPENAI_PNG + "image_url": OPENAI_PNG, + "detail": "high" }) ); server.verify().await; @@ -829,7 +1140,7 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re ) .await; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_model("gpt-5.3-codex") @@ -838,10 +1149,13 @@ async fn stdio_image_responses_preserve_original_detail_metadata() -> anyhow::Re config, server_name, stdio_transport(rmcp_test_server_bin, /*env*/ None, Vec::new()), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -1020,6 +1334,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), @@ -1051,7 +1366,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re ) .await; - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) @@ -1067,10 +1382,13 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re )])), Vec::new(), ), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; fixture @@ -1166,7 +1484,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { let expected_env_value = "propagated-env-from-whitelist"; let _guard = EnvVarGuard::set("MCP_TEST_VALUE", OsStr::new(expected_env_value)); - let rmcp_test_server_bin = stdio_server_bin()?; + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; let fixture = test_codex() .with_config(move |config| { @@ -1176,12 +1494,15 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { stdio_transport( rmcp_test_server_bin, /*env*/ None, - vec!["MCP_TEST_VALUE".to_string()], + vec!["MCP_TEST_VALUE".into()], ), - TestMcpServerOptions::default(), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, ); }) - .build(&server) + .build_remote_aware(&server) .await?; let session_model = fixture.session_configured.model.clone(); @@ -1260,6 +1581,222 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial(mcp_env_source)] +async fn stdio_server_propagates_explicit_local_env_var_source() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let call_id = "call-local-source"; + let server_name = "rmcp_local_source"; + let namespace = format!("mcp__{server_name}__"); + let env_name = "MCP_TEST_LOCAL_SOURCE"; + let expected_env_value = "propagated-explicit-local-source"; + + mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call_with_namespace( + call_id, + &namespace, + "echo", + &format!(r#"{{"message":"ping","env_var":"{env_name}"}}"#), + ), + responses::ev_completed("resp-1"), + ]), + ) + .await; + mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "rmcp echo tool completed successfully."), + responses::ev_completed("resp-2"), + ]), + ) + .await; + + let _guard = EnvVarGuard::set(env_name, OsStr::new(expected_env_value)); + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; + + let fixture = test_codex() + .with_config(move |config| { + insert_mcp_server( + config, + server_name, + stdio_transport( + rmcp_test_server_bin, + /*env*/ None, + vec![McpServerEnvVar::Config { + name: env_name.to_string(), + source: Some("local".to_string()), + }], + ), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, + ); + }) + .build_remote_aware(&server) + .await?; + + let session_model = fixture.session_configured.model.clone(); + fixture + .codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "call the rmcp echo tool".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: fixture.cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: session_model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallBegin(_)) + }) + .await; + let end_event = wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallEnd(_)) + }) + .await; + let EventMsg::McpToolCallEnd(end) = end_event else { + unreachable!("event guard guarantees McpToolCallEnd"); + }; + let structured = end + .result + .as_ref() + .expect("rmcp echo tool should return success") + .structured_content + .as_ref() + .expect("structured content"); + assert_eq!(structured["env"], expected_env_value); + + wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + server.verify().await; + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[serial(mcp_env_source)] +async fn remote_stdio_env_var_source_does_not_copy_local_env() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + if std::env::var_os(remote_env_env_var()).is_none() { + return Ok(()); + } + + let server = responses::start_mock_server().await; + let call_id = "call-remote-source"; + let server_name = "rmcp_remote_source"; + let namespace = format!("mcp__{server_name}__"); + let env_name = "MCP_TEST_REMOTE_SOURCE_ONLY"; + + mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_function_call_with_namespace( + call_id, + &namespace, + "echo", + &format!(r#"{{"message":"ping","env_var":"{env_name}"}}"#), + ), + responses::ev_completed("resp-1"), + ]), + ) + .await; + mount_sse_once( + &server, + responses::sse(vec![ + responses::ev_assistant_message("msg-1", "rmcp echo tool completed successfully."), + responses::ev_completed("resp-2"), + ]), + ) + .await; + + let _guard = EnvVarGuard::set(env_name, OsStr::new("local-value-should-not-cross")); + let rmcp_test_server_bin = remote_aware_stdio_server_bin()?; + + let fixture = test_codex() + .with_config(move |config| { + insert_mcp_server( + config, + server_name, + stdio_transport( + rmcp_test_server_bin, + /*env*/ None, + vec![McpServerEnvVar::Config { + name: env_name.to_string(), + source: Some("remote".to_string()), + }], + ), + TestMcpServerOptions { + experimental_environment: remote_aware_experimental_environment(), + ..Default::default() + }, + ); + }) + .build_remote_aware(&server) + .await?; + + let session_model = fixture.session_configured.model.clone(); + fixture + .codex + .submit(Op::UserTurn { + items: vec![UserInput::Text { + text: "call the rmcp echo tool".into(), + text_elements: Vec::new(), + }], + final_output_json_schema: None, + cwd: fixture.cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + approvals_reviewer: None, + sandbox_policy: SandboxPolicy::new_read_only_policy(), + model: session_model, + effort: None, + summary: None, + service_tier: None, + collaboration_mode: None, + personality: None, + }) + .await?; + + wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallBegin(_)) + }) + .await; + let end_event = wait_for_event(&fixture.codex, |ev| { + matches!(ev, EventMsg::McpToolCallEnd(_)) + }) + .await; + let EventMsg::McpToolCallEnd(end) = end_event else { + unreachable!("event guard guarantees McpToolCallEnd"); + }; + let structured = end + .result + .as_ref() + .expect("rmcp echo tool should return success") + .structured_content + .as_ref() + .expect("structured content"); + assert_eq!(structured["env"], Value::Null); + + wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; + server.verify().await; + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index 404d876dc3..2edd63d3d2 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -21,6 +21,6 @@ Scenario: Pre-turn auto-compaction with a context override emits the context dif 04:message/user: 05:message/user[4]: [01] - [02] + [02] [03] [04] USER_THREE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_commentary.snap b/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_commentary.snap index d3889fcd32..e65a5f34f0 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_commentary.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_commentary.snap @@ -13,7 +13,5 @@ Scenario: /responses POST bodies (input only, redacted like other suite snapshot 00:message/developer: 01:message/user:> 02:message/user:first prompt -03:message/assistant:first commentary -04:function_call/test_tool -05:function_call_output:unsupported call: test_tool -06:message/assistant:{"author":"/root/worker","recipient":"/root","other_recipients":[],"content":"queued child update","trigger_turn":false} +03:message/assistant:first answer +04:message/assistant:{"author":"/root/worker","recipient":"/root","other_recipients":[],"content":"queued child update","trigger_turn":false} diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_reasoning.snap b/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_reasoning.snap index 8e23dfe52d..004196f97a 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_reasoning.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__pending_input__pending_input_queued_mail_after_reasoning.snap @@ -14,5 +14,4 @@ Scenario: /responses POST bodies (input only, redacted like other suite snapshot 01:message/user:> 02:message/user:first prompt 03:reasoning:summary=thinking:encrypted=true -04:message/assistant:preserved commentary -05:message/assistant:{"author":"/root/worker","recipient":"/root","other_recipients":[],"content":"queued child update","trigger_turn":false} +04:message/assistant:{"author":"/root/worker","recipient":"/root","other_recipients":[],"content":"queued child update","trigger_turn":false} diff --git a/codex-rs/core/tests/suite/spawn_agent_description.rs b/codex-rs/core/tests/suite/spawn_agent_description.rs index 8c131b16bc..1d2bd1ed05 100644 --- a/codex-rs/core/tests/suite/spawn_agent_description.rs +++ b/codex-rs/core/tests/suite/spawn_agent_description.rs @@ -82,6 +82,7 @@ fn test_model_info( supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 81c22e1b6c..d0c19f9be6 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -533,7 +533,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { ); assert_eq!( arr[1], - json!({"type": "input_image", "image_url": openai_png}) + json!({"type": "input_image", "image_url": openai_png, "detail": "high"}) ); Ok(()) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index f9d6d9402c..9f53e60d72 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -1,5 +1,6 @@ #![cfg(not(target_os = "windows"))] +use anyhow::Context; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_exec_server::CreateDirectoryOptions; @@ -127,10 +128,10 @@ async fn write_workspace_png( write_workspace_file(test, rel_path, png_bytes(width, height, rgba)?).await } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { - skip_if_no_network!(Ok(())); - +async fn assert_user_turn_local_image_resizes_to( + original_dimensions: (u32, u32), + expected_dimensions: (u32, u32), +) -> anyhow::Result<()> { let server = start_mock_server().await; let mut builder = test_codex(); @@ -142,8 +143,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { .. } = &test; - let original_width = 2304; - let original_height = 864; + let (original_width, original_height) = original_dimensions; let local_image_dir = tempfile::tempdir()?; let abs_path = local_image_dir.path().join("example.png"); let image = ImageBuffer::from_pixel(original_width, original_height, Rgba([20u8, 40, 60, 255])); @@ -187,7 +187,7 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { let body = mock.single_request().body_json(); let image_message = - find_image_message(&body).expect("pending input image message not included in request"); + find_image_message(&body).context("pending input image message not included in request")?; let image_url = image_message .get("content") .and_then(Value::as_array) @@ -200,26 +200,37 @@ async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { } }) }) - .expect("image_url present"); + .context("image_url present")?; let (prefix, encoded) = image_url .split_once(',') - .expect("image url contains data prefix"); + .context("image url contains data prefix")?; assert_eq!(prefix, "data:image/png;base64"); let decoded = BASE64_STANDARD .decode(encoded) - .expect("image data decodes from base64 for request"); - let resized = load_from_memory(&decoded).expect("load resized image"); + .context("image data decodes from base64 for request")?; + let resized = load_from_memory(&decoded).context("load resized image")?; let (width, height) = resized.dimensions(); - assert!(width <= 2048); - assert!(height <= 768); - assert!(width < original_width); - assert!(height < original_height); + assert_eq!((width, height), expected_dimensions); Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn user_turn_with_local_image_attaches_image() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + assert_user_turn_local_image_resizes_to((2304, 864), (2048, 768)).await +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn user_turn_with_vertical_local_image_resizes_to_square_bounds() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + assert_user_turn_local_image_resizes_to((1024, 4096), (512, 2048)).await +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); @@ -347,10 +358,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { .expect("image data decodes from base64 for request"); let resized = load_from_memory(&decoded).expect("load resized image"); let (resized_width, resized_height) = resized.dimensions(); - assert!(resized_width <= 2048); - assert!(resized_height <= 768); - assert!(resized_width < original_width); - assert!(resized_height < original_height); + assert_eq!((resized_width, resized_height), (2048, 768)); Ok(()) } @@ -623,7 +631,10 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { .and_then(Value::as_array) .expect("function_call_output should be a content item array"); assert_eq!(output_items.len(), 1); - assert_eq!(output_items[0].get("detail"), None); + assert_eq!( + output_items[0].get("detail").and_then(Value::as_str), + Some("high") + ); let image_url = output_items[0] .get("image_url") .and_then(Value::as_str) @@ -637,10 +648,7 @@ async fn view_image_tool_treats_null_detail_as_omitted() -> anyhow::Result<()> { .expect("image data decodes from base64 for request"); let resized = load_from_memory(&decoded).expect("load resized image"); let (width, height) = resized.dimensions(); - assert!(width <= 2048); - assert!(height <= 768); - assert!(width < original_width); - assert!(height < original_height); + assert_eq!((width, height), (2048, 768)); Ok(()) } @@ -723,7 +731,10 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a .and_then(Value::as_array) .expect("function_call_output should be a content item array"); assert_eq!(output_items.len(), 1); - assert_eq!(output_items[0].get("detail"), None); + assert_eq!( + output_items[0].get("detail").and_then(Value::as_str), + Some("high") + ); let image_url = output_items[0] .get("image_url") @@ -740,10 +751,7 @@ async fn view_image_tool_resizes_when_model_lacks_original_detail_support() -> a .expect("image data decodes from base64 for request"); let resized = load_from_memory(&decoded).expect("load resized image"); let (resized_width, resized_height) = resized.dimensions(); - assert!(resized_width <= 2048); - assert!(resized_height <= 768); - assert!(resized_width < original_width); - assert!(resized_height < original_height); + assert_eq!((resized_width, resized_height), (2048, 768)); Ok(()) } @@ -827,7 +835,10 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only .and_then(Value::as_array) .expect("function_call_output should be a content item array"); assert_eq!(output_items.len(), 1); - assert_eq!(output_items[0].get("detail"), None); + assert_eq!( + output_items[0].get("detail").and_then(Value::as_str), + Some("high") + ); let image_url = output_items[0] .get("image_url") .and_then(Value::as_str) @@ -841,10 +852,7 @@ async fn view_image_tool_does_not_force_original_resolution_with_capability_only .expect("image data decodes from base64 for request"); let resized = load_from_memory(&decoded).expect("load resized image"); let (resized_width, resized_height) = resized.dimensions(); - assert!(resized_width <= 2048); - assert!(resized_height <= 768); - assert!(resized_width < original_width); - assert!(resized_height < original_height); + assert_eq!((resized_width, resized_height), (2048, 768)); Ok(()) } @@ -1366,6 +1374,7 @@ async fn view_image_tool_returns_unsupported_message_for_text_only_model() -> an supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index b09347686a..64ebc891db 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -1,6 +1,7 @@ use async_trait::async_trait; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use tokio::io; @@ -41,6 +42,10 @@ pub struct ReadDirectoryEntry { #[serde(rename_all = "camelCase")] pub struct FileSystemSandboxContext { pub sandbox_policy: SandboxPolicy, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub sandbox_policy_cwd: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub file_system_sandbox_policy: Option, pub windows_sandbox_level: WindowsSandboxLevel, #[serde(default)] pub windows_sandbox_private_desktop: bool, @@ -53,6 +58,8 @@ impl FileSystemSandboxContext { pub fn new(sandbox_policy: SandboxPolicy) -> Self { Self { sandbox_policy, + sandbox_policy_cwd: None, + file_system_sandbox_policy: None, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, use_legacy_landlock: false, diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 04cded30eb..059b33c04d 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -12,6 +12,7 @@ use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxablePreference; +use codex_sandboxing::policy_transforms::merge_permission_profiles; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; use tokio::io::AsyncWriteExt; @@ -35,6 +36,13 @@ pub(crate) struct FileSystemSandboxRunner { helper_env: HashMap, } +struct HelperSandboxInputs { + sandbox_policy: SandboxPolicy, + file_system_policy: FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + cwd: AbsolutePathBuf, +} + impl FileSystemSandboxRunner { pub(crate) fn new(runtime_paths: ExecServerRuntimePaths) -> Self { Self { @@ -48,19 +56,14 @@ impl FileSystemSandboxRunner { sandbox: &FileSystemSandboxContext, request: FsHelperRequest, ) -> Result { - let helper_sandbox_policy = normalize_sandbox_policy_root_aliases( - sandbox_policy_with_helper_runtime_defaults(&sandbox.sandbox_policy), - ); - let cwd = current_sandbox_cwd().map_err(io_error)?; - let cwd = AbsolutePathBuf::from_absolute_path(cwd.as_path()) - .map_err(|err| invalid_request(format!("current directory is not absolute: {err}")))?; - let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy( - &helper_sandbox_policy, - cwd.as_path(), - ); - let network_policy = NetworkSandboxPolicy::Restricted; + let HelperSandboxInputs { + sandbox_policy, + file_system_policy, + network_policy, + cwd, + } = helper_sandbox_inputs(sandbox)?; let command = self.sandbox_exec_request( - &helper_sandbox_policy, + &sandbox_policy, &file_system_policy, network_policy, &cwd, @@ -92,8 +95,9 @@ impl FileSystemSandboxRunner { args: vec![CODEX_FS_HELPER_ARG1.to_string()], cwd: cwd.clone(), env: self.helper_env.clone(), - additional_permissions: Some( - self.helper_permissions(sandbox_context.additional_permissions.as_ref()), + additional_permissions: self.helper_permissions( + sandbox_context.additional_permissions.as_ref(), + /*include_helper_read_root*/ !sandbox_context.use_legacy_landlock, ), }; sandbox_manager @@ -117,36 +121,68 @@ impl FileSystemSandboxRunner { fn helper_permissions( &self, additional_permissions: Option<&PermissionProfile>, - ) -> PermissionProfile { - let helper_read_root = self - .runtime_paths - .codex_self_exe - .parent() - .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()); - let file_system = - match additional_permissions.and_then(|permissions| permissions.file_system.clone()) { - Some(mut file_system) => { - if let Some(helper_read_root) = &helper_read_root { - let read_paths = file_system.read.get_or_insert_with(Vec::new); - if !read_paths.contains(helper_read_root) { - read_paths.push(helper_read_root.clone()); - } - } - Some(file_system) - } - None => helper_read_root.map(|helper_read_root| FileSystemPermissions { + include_helper_read_root: bool, + ) -> Option { + let inherited_permissions = additional_permissions + .map(|permissions| PermissionProfile { + network: None, + file_system: permissions.file_system.clone(), + }) + .filter(|permissions| !permissions.is_empty()); + let helper_permissions = include_helper_read_root + .then(|| { + self.runtime_paths + .codex_self_exe + .parent() + .and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok()) + }) + .flatten() + .map(|helper_read_root| PermissionProfile { + network: None, + file_system: Some(FileSystemPermissions { read: Some(vec![helper_read_root]), write: None, }), - }; + }); - PermissionProfile { - network: None, - file_system, - } + merge_permission_profiles(inherited_permissions.as_ref(), helper_permissions.as_ref()) } } +fn helper_sandbox_inputs( + sandbox: &FileSystemSandboxContext, +) -> Result { + let sandbox_policy = normalize_sandbox_policy_root_aliases( + sandbox_policy_with_helper_runtime_defaults(&sandbox.sandbox_policy), + ); + let cwd = match &sandbox.sandbox_policy_cwd { + Some(cwd) => cwd.clone(), + None if sandbox.file_system_sandbox_policy.is_some() => { + return Err(invalid_request( + "fileSystemSandboxPolicy requires sandboxPolicyCwd".to_string(), + )); + } + None => { + let cwd = current_sandbox_cwd().map_err(io_error)?; + AbsolutePathBuf::from_absolute_path(cwd.as_path()).map_err(|err| { + invalid_request(format!("current directory is not absolute: {err}")) + })? + } + }; + let file_system_policy = sandbox + .file_system_sandbox_policy + .clone() + .unwrap_or_else(|| { + FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()) + }); + Ok(HelperSandboxInputs { + sandbox_policy, + file_system_policy, + network_policy: NetworkSandboxPolicy::Restricted, + cwd, + }) +} + fn normalize_sandbox_policy_root_aliases(sandbox_policy: SandboxPolicy) -> SandboxPolicy { let mut sandbox_policy = sandbox_policy; match &mut sandbox_policy { @@ -281,10 +317,21 @@ fn spawn_command( fn sandbox_policy_with_helper_runtime_defaults(sandbox_policy: &SandboxPolicy) -> SandboxPolicy { let mut sandbox_policy = sandbox_policy.clone(); match &mut sandbox_policy { - SandboxPolicy::ReadOnly { access, .. } => enable_platform_defaults(access), + SandboxPolicy::ReadOnly { + access, + network_access, + } => { + enable_platform_defaults(access); + *network_access = false; + } SandboxPolicy::WorkspaceWrite { - read_only_access, .. - } => enable_platform_defaults(read_only_access), + read_only_access, + network_access, + .. + } => { + enable_platform_defaults(read_only_access); + *network_access = false; + } SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {} } sandbox_policy @@ -326,11 +373,13 @@ mod tests { use pretty_assertions::assert_eq; use crate::ExecServerRuntimePaths; + use crate::FileSystemSandboxContext; use super::FileSystemSandboxRunner; use super::helper_env; use super::helper_env_from_vars; use super::helper_env_key_is_allowed; + use super::helper_sandbox_inputs; use super::sandbox_policy_with_helper_runtime_defaults; #[test] @@ -365,7 +414,7 @@ mod tests { include_platform_defaults: false, readable_roots: Vec::new(), }, - network_access: false, + network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }; @@ -387,6 +436,52 @@ mod tests { ); } + #[test] + fn helper_sandbox_inputs_use_context_cwd_and_file_system_policy() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute temp dir"); + let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + let file_system_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &sandbox_policy, + cwd.as_path(), + ); + let mut sandbox_context = FileSystemSandboxContext::new(sandbox_policy.clone()); + sandbox_context.sandbox_policy_cwd = Some(cwd.clone()); + sandbox_context.file_system_sandbox_policy = Some(file_system_policy.clone()); + + let inputs = helper_sandbox_inputs(&sandbox_context).expect("helper sandbox inputs"); + + assert_eq!(inputs.cwd, cwd); + assert_eq!(inputs.sandbox_policy, sandbox_policy); + assert_eq!(inputs.file_system_policy, file_system_policy); + assert_eq!(inputs.network_policy, NetworkSandboxPolicy::Restricted); + } + + #[test] + fn helper_sandbox_inputs_rejects_file_system_policy_without_cwd() { + let cwd = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) + .expect("absolute temp dir"); + let sandbox_policy = SandboxPolicy::new_workspace_write_policy(); + let file_system_policy = + codex_protocol::permissions::FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &sandbox_policy, + cwd.as_path(), + ); + let mut sandbox_context = FileSystemSandboxContext::new(sandbox_policy); + sandbox_context.file_system_sandbox_policy = Some(file_system_policy); + + let err = match helper_sandbox_inputs(&sandbox_context) { + Ok(_) => panic!("expected invalid sandbox inputs"), + Err(err) => err, + }; + + assert_eq!( + err.message, + "fileSystemSandboxPolicy requires sandboxPolicyCwd" + ); + } + #[test] fn helper_permissions_strip_network_grants() { let codex_self_exe = std::env::current_exe().expect("current exe"); @@ -403,15 +498,20 @@ mod tests { let writable = AbsolutePathBuf::from_absolute_path(std::env::temp_dir().as_path()) .expect("absolute writable path"); - let permissions = runner.helper_permissions(Some(&PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions { - read: Some(vec![]), - write: Some(vec![writable.clone()]), - }), - })); + let permissions = runner + .helper_permissions( + Some(&PermissionProfile { + network: Some(NetworkPermissions { + enabled: Some(true), + }), + file_system: Some(FileSystemPermissions { + read: Some(vec![]), + write: Some(vec![writable.clone()]), + }), + }), + /*include_helper_read_root*/ true, + ) + .expect("helper permissions"); assert_eq!(permissions.network, None); assert_eq!( @@ -537,7 +637,11 @@ mod tests { ) .expect("absolute readable path"); - let permissions = runner.helper_permissions(/*additional_permissions*/ None); + let permissions = runner + .helper_permissions( + /*additional_permissions*/ None, /*include_helper_read_root*/ true, + ) + .expect("helper permissions"); assert_eq!(permissions.network, None); assert_eq!( @@ -548,4 +652,19 @@ mod tests { }) ); } + + #[test] + fn legacy_landlock_helper_permissions_do_not_add_helper_read_root() { + let codex_self_exe = std::env::current_exe().expect("current exe"); + let runtime_paths = + ExecServerRuntimePaths::new(codex_self_exe, /*codex_linux_sandbox_exe*/ None) + .expect("runtime paths"); + let runner = FileSystemSandboxRunner::new(runtime_paths); + + let permissions = runner.helper_permissions( + /*additional_permissions*/ None, /*include_helper_read_root*/ false, + ); + + assert_eq!(permissions, None); + } } diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 7a2e70156f..4b61bbb602 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -156,6 +156,8 @@ pub enum Feature { ToolSuggest, /// Enable plugins. Plugins, + /// Show the startup prompt for migrating external agent config into Codex. + ExternalMigration, /// Allow the model to invoke the built-in image generation tool. ImageGeneration, /// Allow prompting and installing missing MCP dependencies. @@ -847,6 +849,16 @@ pub const FEATURES: &[FeatureSpec] = &[ stage: Stage::Stable, default_enabled: true, }, + FeatureSpec { + id: Feature::ExternalMigration, + key: "external_migration", + stage: Stage::Experimental { + name: "External migration", + menu_description: "Show a startup prompt when Codex detects migratable external agent config for this machine or project.", + announcement: "", + }, + default_enabled: false, + }, FeatureSpec { id: Feature::ImageGeneration, key: "image_generation", diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index d02c8e8fa3..120c598474 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -120,6 +120,23 @@ fn prefix_compaction_is_experimental_and_user_toggleable() { assert_eq!(Feature::PrefixCompaction.default_enabled(), false); } +#[test] +fn external_migration_is_experimental_and_disabled_by_default() { + let spec = Feature::ExternalMigration.info(); + let stage = spec.stage; + + assert!(matches!(stage, Stage::Experimental { .. })); + assert_eq!(stage.experimental_menu_name(), Some("External migration")); + assert_eq!( + stage.experimental_menu_description(), + Some( + "Show a startup prompt when Codex detects migratable external agent config for this machine or project." + ) + ); + assert_eq!(stage.experimental_announcement(), None); + assert_eq!(Feature::ExternalMigration.default_enabled(), false); +} + #[test] fn request_permissions_is_under_development() { assert_eq!( diff --git a/codex-rs/models-manager/models.json b/codex-rs/models-manager/models.json index 90b53216a6..fd089d8991 100644 --- a/codex-rs/models-manager/models.json +++ b/codex-rs/models-manager/models.json @@ -15,6 +15,7 @@ }, "supports_parallel_tool_calls": true, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "none", "slug": "gpt-5.3-codex", @@ -88,6 +89,7 @@ }, "supports_parallel_tool_calls": true, "context_window": 272000, + "max_context_window": 1000000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "none", "slug": "gpt-5.4", @@ -161,6 +163,7 @@ }, "supports_parallel_tool_calls": true, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5.2-codex", @@ -235,6 +238,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5.1-codex-max", @@ -302,6 +306,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5.1-codex", @@ -365,6 +370,7 @@ }, "supports_parallel_tool_calls": true, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "none", "default_reasoning_summary": "auto", "slug": "gpt-5.2", @@ -432,6 +438,7 @@ }, "supports_parallel_tool_calls": true, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "none", "default_reasoning_summary": "auto", "slug": "gpt-5.1", @@ -495,6 +502,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5-codex", @@ -558,6 +566,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "none", "default_reasoning_summary": "auto", "slug": "gpt-5", @@ -624,6 +633,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 128000, + "max_context_window": 128000, "reasoning_summary_format": "none", "default_reasoning_summary": "auto", "slug": "gpt-oss-120b", @@ -683,6 +693,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 128000, + "max_context_window": 128000, "reasoning_summary_format": "none", "default_reasoning_summary": "auto", "slug": "gpt-oss-20b", @@ -743,6 +754,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5.1-codex-mini", @@ -802,6 +814,7 @@ }, "supports_parallel_tool_calls": false, "context_window": 272000, + "max_context_window": 272000, "reasoning_summary_format": "experimental", "default_reasoning_summary": "auto", "slug": "gpt-5-codex-mini", diff --git a/codex-rs/models-manager/src/manager_tests.rs b/codex-rs/models-manager/src/manager_tests.rs index 23de81781a..a70f46567e 100644 --- a/codex-rs/models-manager/src/manager_tests.rs +++ b/codex-rs/models-manager/src/manager_tests.rs @@ -70,6 +70,7 @@ fn remote_model_with_visibility( "supports_parallel_tool_calls": false, "supports_image_detail_original": false, "context_window": 272_000, + "max_context_window": 272_000, "experimental_supported_tools": [], })) .expect("valid model") diff --git a/codex-rs/models-manager/src/model_info.rs b/codex-rs/models-manager/src/model_info.rs index fefec50ce7..8e8abae549 100644 --- a/codex-rs/models-manager/src/model_info.rs +++ b/codex-rs/models-manager/src/model_info.rs @@ -27,7 +27,13 @@ pub fn with_config_overrides(mut model: ModelInfo, config: &ModelsManagerConfig) model.supports_reasoning_summaries = true; } if let Some(context_window) = config.model_context_window { - model.context_window = Some(context_window); + model.context_window = Some( + model + .max_context_window + .map_or(context_window, |max_context_window| { + context_window.min(max_context_window) + }), + ); } if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit { model.auto_compact_token_limit = Some(auto_compact_token_limit); @@ -84,6 +90,7 @@ pub fn model_info_from_slug(slug: &str) -> ModelInfo { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: Some(272_000), + max_context_window: Some(272_000), auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: Vec::new(), diff --git a/codex-rs/models-manager/src/model_info_tests.rs b/codex-rs/models-manager/src/model_info_tests.rs index ec5b6ed85c..70ad3da8df 100644 --- a/codex-rs/models-manager/src/model_info_tests.rs +++ b/codex-rs/models-manager/src/model_info_tests.rs @@ -43,3 +43,32 @@ fn reasoning_summaries_override_false_is_noop_when_model_is_false() { assert_eq!(updated, model); } + +#[test] +fn model_context_window_override_clamps_to_max_context_window() { + let mut model = model_info_from_slug("unknown-model"); + model.context_window = Some(273_000); + model.max_context_window = Some(400_000); + let config = ModelsManagerConfig { + model_context_window: Some(500_000), + ..Default::default() + }; + + let updated = with_config_overrides(model.clone(), &config); + let mut expected = model; + expected.context_window = Some(400_000); + + assert_eq!(updated, expected); +} + +#[test] +fn model_context_window_uses_model_value_without_override() { + let mut model = model_info_from_slug("unknown-model"); + model.context_window = Some(273_000); + model.max_context_window = Some(400_000); + let config = ModelsManagerConfig::default(); + + let updated = with_config_overrides(model.clone(), &config); + + assert_eq!(updated, model); +} diff --git a/codex-rs/otel/src/metrics/names.rs b/codex-rs/otel/src/metrics/names.rs index fdc03fbaab..9dc718d2d7 100644 --- a/codex-rs/otel/src/metrics/names.rs +++ b/codex-rs/otel/src/metrics/names.rs @@ -37,3 +37,6 @@ pub const STARTUP_PREWARM_DURATION_METRIC: &str = "codex.startup_prewarm.duratio pub const STARTUP_PREWARM_AGE_AT_FIRST_TURN_METRIC: &str = "codex.startup_prewarm.age_at_first_turn_ms"; pub const THREAD_STARTED_METRIC: &str = "codex.thread.started"; +pub const THREAD_SKILLS_ENABLED_TOTAL_METRIC: &str = "codex.thread.skills.enabled_total"; +pub const THREAD_SKILLS_KEPT_TOTAL_METRIC: &str = "codex.thread.skills.kept_total"; +pub const THREAD_SKILLS_TRUNCATED_METRIC: &str = "codex.thread.skills.truncated"; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 8c55d056cd..02cb8c432d 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -208,9 +208,18 @@ pub enum ResponseInputItem { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ContentItem { - InputText { text: String }, - InputImage { image_url: String }, - OutputText { text: String }, + InputText { + text: String, + }, + InputImage { + image_url: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + detail: Option, + }, + OutputText { + text: String, + }, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] @@ -222,6 +231,8 @@ pub enum ImageDetail { Original, } +pub const DEFAULT_IMAGE_DETAIL: ImageDetail = ImageDetail::High; + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] /// Classifies an assistant message as interim commentary or final answer text. @@ -429,7 +440,7 @@ const APPROVAL_POLICY_ON_REQUEST_RULE: &str = include_str!("prompts/permissions/approval_policy/on_request.md"); const APPROVAL_POLICY_ON_REQUEST_RULE_REQUEST_PERMISSION: &str = include_str!("prompts/permissions/approval_policy/on_request_rule_request_permission.md"); -const GUARDIAN_SUBAGENT_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `guardian_subagent`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval."; +const AUTO_REVIEW_APPROVAL_SUFFIX: &str = "`approvals_reviewer` is `auto_review`: Sandbox escalations with require_escalated will be reviewed for compliance with the policy. If a rejection happens, you should proceed only with a materially safer alternative, or inform the user of the risk and send a final message to ask for approval."; const SANDBOX_MODE_DANGER_FULL_ACCESS: &str = include_str!("prompts/permissions/sandbox_mode/danger_full_access.md"); @@ -502,7 +513,7 @@ impl DeveloperInstructions { let text = if approvals_reviewer == ApprovalsReviewer::GuardianSubagent && approval_policy != AskForApproval::Never { - format!("{text}\n\n{GUARDIAN_SUBAGENT_APPROVAL_SUFFIX}") + format!("{text}\n\n{AUTO_REVIEW_APPROVAL_SUFFIX}") } else { text }; @@ -935,6 +946,7 @@ pub fn local_image_content_items_with_label_number( } items.push(ContentItem::InputImage { image_url: image.into_data_url(), + detail: Some(DEFAULT_IMAGE_DETAIL), }); if label_number.is_some() { items.push(ContentItem::InputText { @@ -1082,7 +1094,10 @@ impl From> for ResponseInputItem { ContentItem::InputText { text: image_open_tag_text(), }, - ContentItem::InputImage { image_url }, + ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }, ContentItem::InputText { text: image_close_tag_text(), }, @@ -1225,7 +1240,7 @@ impl From crate::dynamic_tools::DynamicToolCallOutputContentItem::InputImage { image_url } => { Self::InputImage { image_url, - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), } } } @@ -1462,9 +1477,13 @@ fn convert_mcp_content_to_items( .and_then(|meta| meta.get(CODEX_IMAGE_DETAIL_META_KEY)) .and_then(serde_json::Value::as_str) .and_then(|detail| match detail { + "auto" => Some(ImageDetail::Auto), + "low" => Some(ImageDetail::Low), + "high" => Some(ImageDetail::High), "original" => Some(ImageDetail::Original), _ => None, - }), + }) + .or(Some(DEFAULT_IMAGE_DETAIL)), } } Ok(McpContent::Unknown) | Err(_) => FunctionCallOutputContentItem::InputText { @@ -1555,7 +1574,7 @@ mod tests { items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,Zm9v".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] ); } @@ -1630,7 +1649,7 @@ mod tests { items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,Zm9v".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] ); } @@ -1653,7 +1672,7 @@ mod tests { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "line 2".to_string(), @@ -1672,7 +1691,7 @@ mod tests { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]; @@ -1695,7 +1714,7 @@ mod tests { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]); @@ -1944,7 +1963,8 @@ mod tests { ) .into_text(); - assert!(text.contains("`approvals_reviewer` is `guardian_subagent`")); + assert!(text.contains("`approvals_reviewer` is `auto_review`")); + assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`")); assert!(text.contains("materially safer alternative")); } @@ -1959,6 +1979,7 @@ mod tests { ) .into_text(); + assert!(!text.contains("`approvals_reviewer` is `auto_review`")); assert!(!text.contains("`approvals_reviewer` is `guardian_subagent`")); } @@ -2265,7 +2286,7 @@ mod tests { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ] ); @@ -2292,7 +2313,7 @@ mod tests { output: FunctionCallOutputPayload::from_content_items(vec![ FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]), }; @@ -2328,7 +2349,7 @@ mod tests { items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }] ); @@ -2368,7 +2389,7 @@ mod tests { } #[test] - fn ignores_unknown_mcp_image_detail_metadata() -> Result<()> { + fn preserves_standard_detail_metadata_on_mcp_images() -> Result<()> { let call_tool_result = CallToolResult { content: vec![serde_json::json!({ "type": "image", @@ -2392,7 +2413,7 @@ mod tests { items, vec![FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BASE64".into(), - detail: None, + detail: Some(ImageDetail::High), }] ); @@ -2572,7 +2593,10 @@ mod tests { ContentItem::InputText { text: image_open_tag_text(), }, - ContentItem::InputImage { image_url }, + ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }, ContentItem::InputText { text: image_close_tag_text(), }, @@ -2777,7 +2801,13 @@ mod tests { text: image_open_tag_text(), }) ); - assert_eq!(content.get(1), Some(&ContentItem::InputImage { image_url })); + assert_eq!( + content.get(1), + Some(&ContentItem::InputImage { + image_url, + detail: Some(DEFAULT_IMAGE_DETAIL), + }) + ); assert_eq!( content.get(2), Some(&ContentItem::InputText { diff --git a/codex-rs/protocol/src/openai_models.rs b/codex-rs/protocol/src/openai_models.rs index 3339e674be..41275e6a6b 100644 --- a/codex-rs/protocol/src/openai_models.rs +++ b/codex-rs/protocol/src/openai_models.rs @@ -277,6 +277,9 @@ pub struct ModelInfo { pub supports_image_detail_original: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub context_window: Option, + /// Maximum context window allowed for config overrides. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub max_context_window: Option, /// Token threshold for automatic compaction. When omitted, core derives it /// from `context_window` (90%). When provided, core clamps it to 90% of the /// context window when available. @@ -300,9 +303,13 @@ pub struct ModelInfo { } impl ModelInfo { + pub fn resolved_context_window(&self) -> Option { + self.context_window.or(self.max_context_window) + } + pub fn auto_compact_token_limit(&self) -> Option { let context_limit = self - .context_window + .resolved_context_window() .map(|context_window| (context_window * 9) / 10); let config_limit = self.auto_compact_token_limit; if let Some(context_limit) = context_limit { @@ -555,6 +562,7 @@ mod tests { supports_parallel_tool_calls: false, supports_image_detail_original: false, context_window: None, + max_context_window: None, auto_compact_token_limit: None, effective_context_window_percent: 95, experimental_supported_tools: vec![], @@ -780,6 +788,29 @@ mod tests { assert!(!model.supports_search_tool); } + #[test] + fn resolved_context_window_prefers_context_window() { + let model = ModelInfo { + context_window: Some(273_000), + max_context_window: Some(400_000), + ..test_model(/*spec*/ None) + }; + + assert_eq!(model.resolved_context_window(), Some(273_000)); + } + + #[test] + fn resolved_context_window_falls_back_to_max_context_window() { + let model = ModelInfo { + context_window: None, + max_context_window: Some(400_000), + ..test_model(/*spec*/ None) + }; + + assert_eq!(model.resolved_context_window(), Some(400_000)); + assert_eq!(model.auto_compact_token_limit(), Some(360_000)); + } + #[test] fn model_preset_preserves_availability_nux() { let preset = ModelPreset::from(ModelInfo { diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index aa5ab5eee6..b81c224a44 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -15,6 +15,7 @@ axum = { workspace = true, default-features = false, features = [ ] } codex-client = { workspace = true } codex-config = { workspace = true } +codex-exec-server = { workspace = true } codex-keyring-store = { workspace = true } codex-protocol = { workspace = true } codex-utils-pty = { workspace = true } diff --git a/codex-rs/rmcp-client/src/bin/rmcp_test_server.rs b/codex-rs/rmcp-client/src/bin/rmcp_test_server.rs index e513f12780..5bf32124d0 100644 --- a/codex-rs/rmcp-client/src/bin/rmcp_test_server.rs +++ b/codex-rs/rmcp-client/src/bin/rmcp_test_server.rs @@ -74,7 +74,6 @@ impl TestToolServer { #[derive(Deserialize)] struct EchoArgs { message: String, - #[allow(dead_code)] env_var: Option, } @@ -125,9 +124,10 @@ impl ServerHandler for TestToolServer { }; let env_snapshot: HashMap = std::env::vars().collect(); + let env_name = args.env_var.as_deref().unwrap_or("MCP_TEST_VALUE"); let structured_content = json!({ "echo": args.message, - "env": env_snapshot.get("MCP_TEST_VALUE"), + "env": env_snapshot.get(env_name), }); Ok(CallToolResult { diff --git a/codex-rs/rmcp-client/src/bin/test_stdio_server.rs b/codex-rs/rmcp-client/src/bin/test_stdio_server.rs index 82bc387d99..4d3a2ba775 100644 --- a/codex-rs/rmcp-client/src/bin/test_stdio_server.rs +++ b/codex-rs/rmcp-client/src/bin/test_stdio_server.rs @@ -68,6 +68,7 @@ impl TestToolServer { let tools = vec![ Self::echo_tool(), Self::echo_dash_tool(), + Self::cwd_tool(), Self::sync_tool(), Self::image_tool(), Self::image_scenario_tool(), @@ -124,7 +125,7 @@ impl TestToolServer { { "type": "string" }, { "type": "null" } ] - } + }, }, "required": ["echo", "env"], "additionalProperties": false @@ -135,6 +136,35 @@ impl TestToolServer { tool } + fn cwd_tool() -> Tool { + #[expect(clippy::expect_used)] + let schema: JsonObject = serde_json::from_value(json!({ + "type": "object", + "properties": {}, + "additionalProperties": false + })) + .expect("cwd tool schema should deserialize"); + + let mut tool = Tool::new( + Cow::Borrowed("cwd"), + Cow::Borrowed("Return the current working directory of this test server process."), + Arc::new(schema), + ); + #[expect(clippy::expect_used)] + let output_schema: JsonObject = serde_json::from_value(json!({ + "type": "object", + "properties": { + "cwd": { "type": "string" } + }, + "required": ["cwd"], + "additionalProperties": false + })) + .expect("cwd tool output schema should deserialize"); + tool.output_schema = Some(Arc::new(output_schema)); + tool.annotations = Some(ToolAnnotations::new().read_only(true)); + tool + } + fn sync_tool() -> Tool { #[expect(clippy::expect_used)] let schema: JsonObject = serde_json::from_value(json!({ @@ -292,7 +322,6 @@ impl TestToolServer { #[derive(Deserialize)] struct EchoArgs { message: String, - #[allow(dead_code)] env_var: Option, } @@ -453,6 +482,17 @@ impl ServerHandler for TestToolServer { is_error: Some(false), meta: None, }), + "cwd" => { + let cwd = std::env::current_dir() + .map(|path| path.to_string_lossy().into_owned()) + .map_err(|err| McpError::internal_error(err.to_string(), None))?; + Ok(CallToolResult { + content: Vec::new(), + structured_content: Some(json!({ "cwd": cwd })), + is_error: Some(false), + meta: None, + }) + } "echo" | "echo-tool" => { let args: EchoArgs = match request.arguments { Some(arguments) => serde_json::from_value(serde_json::Value::Object( @@ -468,9 +508,10 @@ impl ServerHandler for TestToolServer { }; let env_snapshot: HashMap = std::env::vars().collect(); + let env_name = args.env_var.as_deref().unwrap_or("MCP_TEST_VALUE"); let structured_content = json!({ "echo": format!("ECHOING: {}", args.message), - "env": env_snapshot.get("MCP_TEST_VALUE"), + "env": env_snapshot.get(env_name), }); Ok(CallToolResult { diff --git a/codex-rs/rmcp-client/src/executor_process_transport.rs b/codex-rs/rmcp-client/src/executor_process_transport.rs new file mode 100644 index 0000000000..41f0b7660d --- /dev/null +++ b/codex-rs/rmcp-client/src/executor_process_transport.rs @@ -0,0 +1,376 @@ +//! rmcp transport adapter for an executor-managed MCP stdio process. +//! +//! This module owns the lower-level byte translation after +//! `stdio_server_launcher` has already started a process through +//! `ExecBackend::start`. It does not choose where the MCP server runs and it +//! does not implement MCP lifecycle behavior. MCP protocol ownership stays in +//! `RmcpClient` and rmcp: +//! +//! 1. rmcp serializes a JSON-RPC message and calls [`Transport::send`]. +//! 2. This transport appends the stdio newline delimiter and writes those bytes +//! to executor `process/write`. +//! 3. The executor writes the bytes to the child process stdin. +//! 4. The child writes newline-delimited JSON-RPC messages to stdout. +//! 5. The executor reports stdout bytes through pushed process events. +//! 6. This transport buffers stdout until it has one full line, deserializes +//! that line, and returns the rmcp message from [`Transport::receive`]. +//! +//! Stderr is deliberately not part of the MCP byte stream. It is logged for +//! diagnostics only, matching the local stdio implementation. + +use std::future::Future; +use std::io; +use std::mem::take; +use std::sync::Arc; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use codex_exec_server::ExecOutputStream; +use codex_exec_server::ExecProcess; +use codex_exec_server::ExecProcessEvent; +use codex_exec_server::ExecProcessEventReceiver; +use codex_exec_server::ProcessId; +use codex_exec_server::ProcessOutputChunk; +use codex_exec_server::WriteStatus; +use rmcp::service::RoleClient; +use rmcp::service::RxJsonRpcMessage; +use rmcp::service::TxJsonRpcMessage; +use rmcp::transport::Transport; +use serde_json::from_slice; +use serde_json::to_vec; +use tokio::runtime::Handle; +use tokio::sync::broadcast; +use tracing::debug; +use tracing::info; +use tracing::warn; + +static PROCESS_COUNTER: AtomicUsize = AtomicUsize::new(1); + +// Remote public implementation. + +/// A client-side rmcp transport backed by an executor-managed process. +/// +/// The orchestrator owns this value and calls rmcp on it. The process it wraps +/// may be local or remote depending on the `ExecBackend` used to create it, but +/// for remote MCP stdio the process lives on the executor and all interaction +/// crosses the executor process RPC boundary. +pub(super) struct ExecutorProcessTransport { + /// Logical process handle returned by the executor process API. + /// + /// `write` forwards stdin bytes. `terminate` stops the child when rmcp + /// closes the transport. + process: Arc, + + /// Pushed output/lifecycle stream for the process. + /// + /// The executor process API still supports retained-output reads, but MCP + /// stdio is naturally streaming. This receiver lets rmcp wait for stdout + /// chunks without issuing `process/read` after each output notification. + events: ExecProcessEventReceiver, + + /// Human-readable program name used only in diagnostics. + program_name: String, + + /// Buffered child stdout bytes that have not yet formed a complete + /// newline-delimited JSON-RPC message. + stdout: Vec, + + /// Buffered stderr bytes for diagnostic logging. + stderr: Vec, + + /// Whether the executor has reported process closure or a terminal + /// subscription failure. Once closed, any remaining partial stdout line is + /// flushed once and then rmcp receives EOF. + closed: bool, + + /// Whether this transport already asked the executor to terminate the MCP + /// server process. + terminated: bool, + + /// Highest executor process event sequence observed by this transport. + /// + /// When the pushed event stream lags, use this as the retained-output read + /// cursor to recover missed stdout/stderr chunks from the executor. + last_seq: u64, +} + +impl ExecutorProcessTransport { + pub(super) fn new(process: Arc, program_name: String) -> Self { + // Subscribe before returning the transport to rmcp. Some test servers + // can emit output or exit quickly after `process/start`, and the + // process event log will replay anything that landed before this + // subscriber was attached. + let events = process.subscribe_events(); + Self { + process, + events, + program_name, + stdout: Vec::new(), + stderr: Vec::new(), + closed: false, + terminated: false, + last_seq: 0, + } + } + + pub(super) fn next_process_id() -> ProcessId { + // Process IDs are logical handles scoped to the executor connection, + // not OS pids. A monotonic client-side id is enough to avoid + // collisions between MCP servers started in the same session. + let index = PROCESS_COUNTER.fetch_add(1, Ordering::Relaxed); + ProcessId::from(format!("mcp-stdio-{index}")) + } +} + +impl Transport for ExecutorProcessTransport { + type Error = io::Error; + + fn send( + &mut self, + item: TxJsonRpcMessage, + ) -> impl Future> + Send + 'static { + let process = Arc::clone(&self.process); + async move { + // rmcp hands us a structured JSON-RPC message. Stdio transport on + // the wire is JSON plus one newline delimiter. + let mut bytes = to_vec(&item).map_err(io::Error::other)?; + bytes.push(b'\n'); + let response = process.write(bytes).await.map_err(io::Error::other)?; + match response.status { + WriteStatus::Accepted => Ok(()), + WriteStatus::UnknownProcess => { + Err(io::Error::new(io::ErrorKind::BrokenPipe, "unknown process")) + } + WriteStatus::StdinClosed => { + Err(io::Error::new(io::ErrorKind::BrokenPipe, "stdin closed")) + } + WriteStatus::Starting => Err(io::Error::new( + io::ErrorKind::WouldBlock, + "process is starting", + )), + } + } + } + + fn receive(&mut self) -> impl Future>> + Send { + self.receive_message() + } + + async fn close(&mut self) -> std::result::Result<(), Self::Error> { + self.process.terminate().await.map_err(io::Error::other)?; + self.terminated = true; + Ok(()) + } +} + +impl ExecutorProcessTransport { + async fn receive_message(&mut self) -> Option> { + loop { + // rmcp stdio framing is line-oriented JSON. We first drain any + // complete line already buffered from an earlier process event. + if let Some(message) = self.take_stdout_message(/*allow_partial*/ self.closed) { + return Some(message); + } + if self.closed { + self.flush_stderr(); + return None; + } + + match self.events.recv().await { + Ok(ExecProcessEvent::Output(chunk)) => { + // The executor pushes raw process bytes. This is the only + // place where those bytes are split back into the stdout + // protocol stream and stderr diagnostics. + self.push_process_output_if_new(chunk); + } + Ok(ExecProcessEvent::Exited { seq, .. }) => { + self.note_seq(seq); + // Wait for `Closed` before ending the rmcp stream so any + // output flushed during process shutdown can still be + // decoded into JSON-RPC messages. + } + Ok(ExecProcessEvent::Closed { seq }) => { + self.note_seq(seq); + self.closed = true; + } + Ok(ExecProcessEvent::Failed(message)) => { + warn!( + "Remote MCP server process failed ({}): {message}", + self.program_name + ); + self.closed = true; + } + Err(broadcast::error::RecvError::Lagged(skipped)) => { + warn!( + "Remote MCP server output stream lagged ({}): skipped {skipped} events", + self.program_name + ); + if let Err(error) = self.recover_lagged_events().await { + warn!( + "Failed to recover remote MCP server output stream ({}): {error}", + self.program_name + ); + self.closed = true; + } + } + Err(broadcast::error::RecvError::Closed) => { + self.closed = true; + } + } + } + } + + fn note_seq(&mut self, seq: u64) { + self.last_seq = self.last_seq.max(seq); + } + + fn should_accept_seq(&mut self, seq: u64) -> bool { + if seq <= self.last_seq { + return false; + } + self.last_seq = seq; + true + } + + async fn recover_lagged_events(&mut self) -> io::Result<()> { + let response = self + .process + .read( + Some(self.last_seq), + /*max_bytes*/ None, + /*wait_ms*/ Some(0), + ) + .await + .map_err(io::Error::other)?; + for chunk in response.chunks { + self.push_process_output_if_new(chunk); + } + self.last_seq = self.last_seq.max(response.next_seq.saturating_sub(1)); + if let Some(message) = response.failure { + warn!( + "Remote MCP server process failed ({}): {message}", + self.program_name + ); + self.closed = true; + } else if response.closed { + self.closed = true; + } + Ok(()) + } + + fn push_process_output_if_new(&mut self, chunk: ProcessOutputChunk) { + if !self.should_accept_seq(chunk.seq) { + return; + } + self.push_process_output(chunk); + } + + fn push_process_output(&mut self, chunk: ProcessOutputChunk) { + let bytes = chunk.chunk.into_inner(); + match chunk.stream { + // MCP stdio uses stdout as the protocol stream. PTY output is + // accepted defensively because the executor process API has a + // unified stream enum, but remote MCP starts with `tty=false`. + ExecOutputStream::Stdout | ExecOutputStream::Pty => { + self.stdout.extend_from_slice(&bytes); + } + // Stderr is intentionally out-of-band. It should help debug server + // startup failures without entering rmcp framing. + ExecOutputStream::Stderr => { + self.push_stderr(&bytes); + } + } + } + + fn take_stdout_message(&mut self, allow_partial: bool) -> Option> { + // A normal MCP stdio server emits one JSON-RPC message per newline. + // If the process has already closed, accept a final unterminated line + // so EOF after a complete JSON object behaves like local rmcp's + // `decode_eof` handling. + loop { + let line_end = self.stdout.iter().position(|byte| *byte == b'\n'); + let line = match (line_end, allow_partial && !self.stdout.is_empty()) { + (Some(index), _) => { + let mut line = self.stdout.drain(..=index).collect::>(); + line.pop(); + line + } + (None, true) => self.stdout.drain(..).collect(), + (None, false) => return None, + }; + let line = Self::trim_trailing_carriage_return(line); + match from_slice::>(&line) { + Ok(message) => return Some(message), + Err(error) => { + debug!( + "Failed to parse remote MCP server message ({}): {error}", + self.program_name + ); + } + } + } + } + + fn push_stderr(&mut self, bytes: &[u8]) { + // Keep stderr line-oriented in logs so a chatty MCP server does not + // produce one log record per byte chunk. + self.stderr.extend_from_slice(bytes); + while let Some(index) = self.stderr.iter().position(|byte| *byte == b'\n') { + let mut line = self.stderr.drain(..=index).collect::>(); + line.pop(); + if line.last() == Some(&b'\r') { + line.pop(); + } + info!( + "MCP server stderr ({}): {}", + self.program_name, + String::from_utf8_lossy(&line) + ); + } + } + + fn flush_stderr(&mut self) { + if self.stderr.is_empty() { + return; + } + let line = take(&mut self.stderr); + info!( + "MCP server stderr ({}): {}", + self.program_name, + String::from_utf8_lossy(&line) + ); + } + + fn trim_trailing_carriage_return(mut line: Vec) -> Vec { + if line.last() == Some(&b'\r') { + line.pop(); + } + line + } +} + +impl Drop for ExecutorProcessTransport { + fn drop(&mut self) { + if self.terminated { + return; + } + + let process = Arc::clone(&self.process); + let program_name = self.program_name.clone(); + let Ok(handle) = Handle::try_current() else { + warn!( + "Could not schedule remote MCP server process termination on drop ({}): no Tokio runtime is available", + self.program_name + ); + return; + }; + + std::mem::drop(handle.spawn(async move { + if let Err(error) = process.terminate().await { + warn!( + "Failed to terminate remote MCP server process on drop ({program_name}): {error}" + ); + } + })); + } +} diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index f02167b6f6..f3870b9319 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -1,5 +1,6 @@ mod auth_status; mod elicitation_client_service; +mod executor_process_transport; mod logging_client_handler; mod oauth; mod perform_oauth_login; @@ -30,5 +31,6 @@ pub use rmcp_client::ListToolsWithConnectorIdResult; pub use rmcp_client::RmcpClient; pub use rmcp_client::SendElicitation; pub use rmcp_client::ToolWithConnectorId; +pub use stdio_server_launcher::ExecutorStdioServerLauncher; pub use stdio_server_launcher::LocalStdioServerLauncher; pub use stdio_server_launcher::StdioServerLauncher; diff --git a/codex-rs/rmcp-client/src/program_resolver.rs b/codex-rs/rmcp-client/src/program_resolver.rs index d9be2741c7..c20cac3747 100644 --- a/codex-rs/rmcp-client/src/program_resolver.rs +++ b/codex-rs/rmcp-client/src/program_resolver.rs @@ -157,7 +157,7 @@ mod tests { #[cfg(windows)] extra_env.insert(OsString::from("PATHEXT"), Self::ensure_cmd_extension()); - let mcp_env = create_env_for_mcp_server(Some(extra_env), &[]); + let mcp_env = create_env_for_mcp_server(Some(extra_env), &[])?; Ok(Self { _temp_dir: temp_dir, diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index 6bdc5dd29f..b80e335a59 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -13,6 +13,7 @@ use std::time::Instant; use anyhow::Result; use anyhow::anyhow; use codex_client::build_reqwest_client_with_custom_ca; +use codex_config::types::McpServerEnvVar; use futures::FutureExt; use futures::StreamExt; use futures::future::BoxFuture; @@ -503,7 +504,7 @@ impl RmcpClient { program: OsString, args: Vec, env: Option>, - env_vars: &[String], + env_vars: &[McpServerEnvVar], cwd: Option, launcher: Arc, ) -> io::Result { diff --git a/codex-rs/rmcp-client/src/stdio_server_launcher.rs b/codex-rs/rmcp-client/src/stdio_server_launcher.rs index 2fd610e5cb..4e9020e369 100644 --- a/codex-rs/rmcp-client/src/stdio_server_launcher.rs +++ b/codex-rs/rmcp-client/src/stdio_server_launcher.rs @@ -1,12 +1,15 @@ //! Launch MCP stdio servers and return the transport rmcp should use. //! -//! This module owns the "where does the server process run?" boundary for -//! stdio MCP servers. In this PR there is only the local launcher, which keeps -//! the existing behavior: the orchestrator starts the configured command and -//! rmcp talks to the child process through local stdin/stdout pipes. +//! This module owns the "where does the server process run?" decision: //! -//! Later stack entries add an executor-backed launcher without changing -//! `RmcpClient`'s MCP lifecycle code. +//! - [`LocalStdioServerLauncher`] starts the configured command as a child of +//! the orchestrator process. +//! - [`ExecutorStdioServerLauncher`] starts the configured command through the +//! executor process API. +//! +//! Both paths return [`StdioServerTransport`], so `RmcpClient` can hand the +//! resulting byte stream to rmcp without knowing where the process lives. The +//! executor-specific byte adaptation lives in `executor_process_transport`. use std::collections::HashMap; use std::ffi::OsString; @@ -14,6 +17,7 @@ use std::future::Future; use std::io; use std::path::PathBuf; use std::process::Stdio; +use std::sync::Arc; #[cfg(unix)] use std::thread::sleep; #[cfg(unix)] @@ -21,6 +25,13 @@ use std::thread::spawn; #[cfg(unix)] use std::time::Duration; +use anyhow::Result; +use anyhow::anyhow; +use codex_config::types::McpServerEnvVar; +use codex_config::types::ShellEnvironmentPolicyInherit; +use codex_exec_server::ExecBackend; +use codex_exec_server::ExecEnvPolicy; +use codex_exec_server::ExecParams; #[cfg(unix)] use codex_utils_pty::process_group::kill_process_group; #[cfg(unix)] @@ -38,8 +49,11 @@ use tokio::process::Command; use tracing::info; use tracing::warn; +use crate::executor_process_transport::ExecutorProcessTransport; use crate::program_resolver; use crate::utils::create_env_for_mcp_server; +use crate::utils::create_env_overlay_for_remote_mcp_server; +use crate::utils::remote_mcp_env_var_names; // General purpose public code. @@ -63,7 +77,7 @@ pub struct StdioServerCommand { program: OsString, args: Vec, env: Option>, - env_vars: Vec, + env_vars: Vec, cwd: Option, } @@ -74,11 +88,16 @@ pub struct StdioServerCommand { /// directly to `rmcp::service::serve_client`. pub struct StdioServerTransport { inner: StdioServerTransportInner, + // Local child processes can leave subprocesses behind, so the local + // variant keeps a process-group guard with the transport. Executor-backed + // processes are owned and cleaned up by the executor, so that variant uses + // `None`. _process_group_guard: Option, } enum StdioServerTransportInner { Local(TokioChildProcess), + Executor(ExecutorProcessTransport), } impl Transport for StdioServerTransport { @@ -88,20 +107,29 @@ impl Transport for StdioServerTransport { &mut self, item: TxJsonRpcMessage, ) -> impl Future> + Send + 'static { + // Both variants already implement rmcp's transport contract. This + // wrapper keeps process placement private while leaving rmcp's send + // semantics unchanged. match &mut self.inner { StdioServerTransportInner::Local(transport) => transport.send(item).boxed(), + StdioServerTransportInner::Executor(transport) => transport.send(item).boxed(), } } fn receive(&mut self) -> impl Future>> + Send { + // rmcp reads from the same transport shape for both placements. The + // executor variant turns pushed process-output events back into the + // line-delimited JSON stream expected by rmcp. match &mut self.inner { StdioServerTransportInner::Local(transport) => transport.receive().boxed(), + StdioServerTransportInner::Executor(transport) => transport.receive().boxed(), } } async fn close(&mut self) -> std::result::Result<(), Self::Error> { match &mut self.inner { StdioServerTransportInner::Local(transport) => transport.close().await, + StdioServerTransportInner::Executor(transport) => transport.close().await, } } } @@ -113,7 +141,7 @@ impl StdioServerCommand { program: OsString, args: Vec, env: Option>, - env_vars: Vec, + env_vars: Vec, cwd: Option, ) -> Self { Self { @@ -174,7 +202,7 @@ impl LocalStdioServerLauncher { cwd, } = command; let program_name = program.to_string_lossy().into_owned(); - let envs = create_env_for_mcp_server(env, &env_vars); + let envs = create_env_for_mcp_server(env, &env_vars).map_err(io::Error::other)?; let resolved_program = program_resolver::resolve(program, &envs).map_err(io::Error::other)?; @@ -266,3 +294,229 @@ impl Drop for ProcessGroupGuard { } } } + +// Remote public implementation. + +/// Starts MCP stdio servers through the executor process API. +/// +/// MCP framing still runs in the orchestrator. The executor only owns the +/// child process and transports raw stdin/stdout/stderr bytes, so it does not +/// need to know about MCP methods such as `initialize` or `tools/list`. +#[derive(Clone)] +pub struct ExecutorStdioServerLauncher { + exec_backend: Arc, + fallback_cwd: PathBuf, +} + +impl ExecutorStdioServerLauncher { + /// Creates a stdio server launcher backed by the executor process API. + /// + /// `fallback_cwd` is used only when the MCP server config omits `cwd`. + /// Executor `process/start` requires an explicit working directory, unlike + /// local `tokio::process::Command`, which can inherit the orchestrator cwd. + pub fn new(exec_backend: Arc, fallback_cwd: PathBuf) -> Self { + Self { + exec_backend, + fallback_cwd, + } + } +} + +impl StdioServerLauncher for ExecutorStdioServerLauncher { + fn launch( + &self, + command: StdioServerCommand, + ) -> BoxFuture<'static, io::Result> { + let exec_backend = Arc::clone(&self.exec_backend); + let fallback_cwd = self.fallback_cwd.clone(); + async move { Self::launch_server(command, exec_backend, fallback_cwd).await }.boxed() + } +} + +// Remote private implementation. + +impl private::Sealed for ExecutorStdioServerLauncher {} + +impl ExecutorStdioServerLauncher { + async fn launch_server( + command: StdioServerCommand, + exec_backend: Arc, + fallback_cwd: PathBuf, + ) -> io::Result { + let StdioServerCommand { + program, + args, + env, + env_vars, + cwd, + } = command; + let program_name = program.to_string_lossy().into_owned(); + let envs = create_env_overlay_for_remote_mcp_server(env, &env_vars); + let remote_env_vars = remote_mcp_env_var_names(&env_vars); + // The executor protocol carries argv/env as UTF-8 strings. Local stdio can + // accept arbitrary OsString values because it calls the OS directly; remote + // stdio must reject non-Unicode command, argument, or environment data + // before sending an executor request. + let argv = Self::process_api_argv(&program, &args).map_err(io::Error::other)?; + let env = Self::process_api_env(envs).map_err(io::Error::other)?; + let process_id = ExecutorProcessTransport::next_process_id(); + // Start the MCP server process on the executor with raw pipes. `tty=false` + // keeps stdout as a clean protocol stream, while `pipe_stdin=true` lets + // rmcp write JSON-RPC requests after the process starts. + let started = exec_backend + .start(ExecParams { + process_id, + argv, + cwd: cwd.unwrap_or(fallback_cwd), + env_policy: Some(Self::remote_env_policy(&remote_env_vars)), + env, + tty: false, + pipe_stdin: true, + arg0: None, + }) + .await + .map_err(io::Error::other)?; + + Ok(StdioServerTransport { + inner: StdioServerTransportInner::Executor(ExecutorProcessTransport::new( + started.process, + program_name, + )), + _process_group_guard: None, + }) + } + + fn process_api_argv(program: &OsString, args: &[OsString]) -> Result> { + let mut argv = Vec::with_capacity(args.len() + 1); + argv.push(Self::os_string_to_process_api_string( + program.clone(), + "command", + )?); + for arg in args { + argv.push(Self::os_string_to_process_api_string( + arg.clone(), + "argument", + )?); + } + Ok(argv) + } + + fn process_api_env(env: HashMap) -> Result> { + env.into_iter() + .map(|(key, value)| { + Ok(( + Self::os_string_to_process_api_string(key, "environment variable name")?, + Self::os_string_to_process_api_string(value, "environment variable value")?, + )) + }) + .collect() + } + + fn os_string_to_process_api_string(value: OsString, label: &str) -> Result { + value + .into_string() + .map_err(|_| anyhow!("{label} must be valid Unicode for remote MCP stdio")) + } + + fn remote_env_policy(remote_env_vars: &[String]) -> ExecEnvPolicy { + let include_only = if remote_env_vars.is_empty() { + Vec::new() + } else { + // `source = "remote"` means the value is read from the executor's + // environment, not copied from Codex. Start from `All` only so the + // named remote variable is available to the filter below; the + // effective child env is still limited by `include_only`. + crate::utils::DEFAULT_ENV_VARS + .iter() + .map(|name| (*name).to_string()) + .chain(remote_env_vars.iter().cloned()) + .collect() + }; + ExecEnvPolicy { + inherit: if remote_env_vars.is_empty() { + ShellEnvironmentPolicyInherit::Core + } else { + ShellEnvironmentPolicyInherit::All + }, + ignore_default_excludes: true, + exclude: Vec::new(), + r#set: HashMap::new(), + include_only, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_config::shell_environment; + use codex_config::types::EnvironmentVariablePattern; + use codex_config::types::ShellEnvironmentPolicy; + + #[test] + fn remote_env_policy_uses_core_env_without_remote_source_vars() { + let policy = ExecutorStdioServerLauncher::remote_env_policy(&[]); + + assert_eq!(policy.inherit, ShellEnvironmentPolicyInherit::Core); + assert!(policy.include_only.is_empty()); + } + + #[test] + fn remote_env_policy_includes_remote_source_vars_without_full_env() { + let policy = ExecutorStdioServerLauncher::remote_env_policy(&["REMOTE_TOKEN".to_string()]); + + assert_eq!(policy.inherit, ShellEnvironmentPolicyInherit::All); + assert!( + policy.include_only.contains(&"REMOTE_TOKEN".to_string()), + "remote source var should be included in executor env policy" + ); + assert!( + policy + .include_only + .contains(&crate::utils::DEFAULT_ENV_VARS[0].to_string()), + "remote default env vars should remain available" + ); + } + + #[test] + fn remote_env_policy_effectively_filters_unrequested_vars() { + let exec_policy = + ExecutorStdioServerLauncher::remote_env_policy(&["REMOTE_TOKEN".to_string()]); + let policy = ShellEnvironmentPolicy { + inherit: exec_policy.inherit, + ignore_default_excludes: exec_policy.ignore_default_excludes, + exclude: exec_policy + .exclude + .iter() + .map(|pattern| EnvironmentVariablePattern::new_case_insensitive(pattern)) + .collect(), + r#set: exec_policy.r#set, + include_only: exec_policy + .include_only + .iter() + .map(|pattern| EnvironmentVariablePattern::new_case_insensitive(pattern)) + .collect(), + use_profile: false, + }; + + let env = shell_environment::create_env_from_vars( + [ + ("PATH".to_string(), "/remote/bin".to_string()), + ("REMOTE_TOKEN".to_string(), "remote-secret".to_string()), + ( + "UNREQUESTED_SECRET".to_string(), + "must-not-pass".to_string(), + ), + ], + &policy, + /*thread_id*/ None, + ); + + assert_eq!(env.get("PATH").map(String::as_str), Some("/remote/bin")); + assert_eq!( + env.get("REMOTE_TOKEN").map(String::as_str), + Some("remote-secret") + ); + assert!(!env.contains_key("UNREQUESTED_SECRET")); + } +} diff --git a/codex-rs/rmcp-client/src/utils.rs b/codex-rs/rmcp-client/src/utils.rs index cf8d4cbb26..6176f071ce 100644 --- a/codex-rs/rmcp-client/src/utils.rs +++ b/codex-rs/rmcp-client/src/utils.rs @@ -1,4 +1,6 @@ use anyhow::Result; +use anyhow::anyhow; +use codex_config::types::McpServerEnvVar; use reqwest::ClientBuilder; use reqwest::header::HeaderMap; use reqwest::header::HeaderName; @@ -9,17 +11,52 @@ use std::ffi::OsString; pub(crate) fn create_env_for_mcp_server( extra_env: Option>, - env_vars: &[String], -) -> HashMap { - DEFAULT_ENV_VARS + env_vars: &[McpServerEnvVar], +) -> Result> { + let additional_env_vars = local_stdio_env_var_names(env_vars)?; + let env = DEFAULT_ENV_VARS .iter() .copied() - .chain(env_vars.iter().map(String::as_str)) + .chain(additional_env_vars) .filter_map(|var| env::var_os(var).map(|value| (OsString::from(var), value))) .chain(extra_env.unwrap_or_default()) + .collect(); + Ok(env) +} + +pub(crate) fn create_env_overlay_for_remote_mcp_server( + extra_env: Option>, + env_vars: &[McpServerEnvVar], +) -> HashMap { + // Remote stdio should inherit PATH/HOME/etc. from the executor side, not + // from the orchestrator process. Only forward variables explicitly named + // by the MCP config plus literal env overrides from that config. + env_vars + .iter() + .filter(|var| !var.is_remote_source()) + .filter_map(|var| env::var_os(var.name()).map(|value| (OsString::from(var.name()), value))) + .chain(extra_env.unwrap_or_default()) .collect() } +pub(crate) fn remote_mcp_env_var_names(env_vars: &[McpServerEnvVar]) -> Vec { + env_vars + .iter() + .filter(|var| var.is_remote_source()) + .map(|var| var.name().to_string()) + .collect() +} + +fn local_stdio_env_var_names(env_vars: &[McpServerEnvVar]) -> Result> { + if let Some(remote_var) = env_vars.iter().find(|var| var.is_remote_source()) { + return Err(anyhow!( + "env_vars entry `{}` uses source `remote`, which requires remote MCP stdio", + remote_var.name() + )); + } + Ok(env_vars.iter().map(McpServerEnvVar::name)) +} + pub(crate) fn build_default_headers( http_headers: Option>, env_http_headers: Option>, @@ -182,7 +219,8 @@ mod tests { let env = create_env_for_mcp_server( Some(HashMap::from([(OsString::from("TZ"), expected.clone())])), &[], - ); + ) + .expect("local MCP env should build"); assert_eq!(env.get(OsStr::new("TZ")), Some(&expected)); } @@ -193,10 +231,92 @@ mod tests { let value = "from-env"; let expected = OsString::from(value); let _guard = EnvVarGuard::set(custom_var, value); - let env = create_env_for_mcp_server(/*extra_env*/ None, &[custom_var.to_string()]); + let env = create_env_for_mcp_server(/*extra_env*/ None, &[custom_var.into()]) + .expect("local MCP env should build"); assert_eq!(env.get(OsStr::new(custom_var)), Some(&expected)); } + #[test] + #[serial(extra_rmcp_env)] + fn create_remote_env_overlay_only_forwards_explicit_variables() { + let default_var = DEFAULT_ENV_VARS[0]; + let custom_var = "EXTRA_REMOTE_RMCP_ENV"; + let custom_value = OsString::from("from-env"); + let _default_guard = EnvVarGuard::set(default_var, "from-default"); + let _custom_guard = EnvVarGuard::set(custom_var, &custom_value); + + let env = + create_env_overlay_for_remote_mcp_server(/*extra_env*/ None, &[custom_var.into()]); + + assert_eq!( + env, + HashMap::from([(OsString::from(custom_var), custom_value)]) + ); + } + + #[test] + #[serial(extra_rmcp_env)] + fn create_remote_env_overlay_does_not_copy_remote_source_variables() { + let remote_var = "REMOTE_ONLY_RMCP_ENV"; + let local_var = "LOCAL_RMCP_ENV"; + let local_value = OsString::from("from-local-env"); + let _remote_guard = EnvVarGuard::set(remote_var, "should-not-be-copied"); + let _local_guard = EnvVarGuard::set(local_var, &local_value); + + let env = create_env_overlay_for_remote_mcp_server( + /*extra_env*/ None, + &[ + McpServerEnvVar::Config { + name: remote_var.to_string(), + source: Some("remote".to_string()), + }, + McpServerEnvVar::Config { + name: local_var.to_string(), + source: Some("local".to_string()), + }, + ], + ); + + assert_eq!( + env, + HashMap::from([(OsString::from(local_var), local_value)]) + ); + } + + #[test] + fn remote_mcp_env_var_names_returns_remote_source_names() { + let names = remote_mcp_env_var_names(&[ + "LEGACY".into(), + McpServerEnvVar::Config { + name: "LOCAL".to_string(), + source: Some("local".to_string()), + }, + McpServerEnvVar::Config { + name: "REMOTE".to_string(), + source: Some("remote".to_string()), + }, + ]); + + assert_eq!(names, vec!["REMOTE".to_string()]); + } + + #[test] + fn create_local_env_rejects_remote_source_variables() { + let err = create_env_for_mcp_server( + /*extra_env*/ None, + &[McpServerEnvVar::Config { + name: "REMOTE".to_string(), + source: Some("remote".to_string()), + }], + ) + .expect_err("remote source should require remote stdio"); + + assert!( + err.to_string().contains("requires remote MCP stdio"), + "unexpected error: {err}" + ); + } + #[cfg(unix)] #[test] #[serial(extra_rmcp_env)] @@ -207,7 +327,8 @@ mod tests { let expected = raw_path.to_os_string(); let _guard = EnvVarGuard::set("PATH", raw_path); - let env = create_env_for_mcp_server(/*extra_env*/ None, &[]); + let env = + create_env_for_mcp_server(/*extra_env*/ None, &[]).expect("local MCP env should build"); assert_eq!(env.get(OsStr::new("PATH")), Some(&expected)); } diff --git a/codex-rs/tools/src/image_detail.rs b/codex-rs/tools/src/image_detail.rs index 56987e4831..37086f691d 100644 --- a/codex-rs/tools/src/image_detail.rs +++ b/codex-rs/tools/src/image_detail.rs @@ -1,3 +1,4 @@ +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::ModelInfo; @@ -14,7 +15,8 @@ pub fn normalize_output_image_detail( Some(ImageDetail::Original) if can_request_original_image_detail(model_info) => { Some(ImageDetail::Original) } - Some(ImageDetail::Original) | Some(_) | None => None, + Some(ImageDetail::Original) | None => None, + Some(ImageDetail::Auto | ImageDetail::Low | ImageDetail::High) => detail, } } @@ -30,7 +32,7 @@ pub fn sanitize_original_image_detail( if let FunctionCallOutputContentItem::InputImage { detail, .. } = item && matches!(detail, Some(ImageDetail::Original)) { - *detail = None; + *detail = Some(DEFAULT_IMAGE_DETAIL); } } } diff --git a/codex-rs/tools/src/image_detail_tests.rs b/codex-rs/tools/src/image_detail_tests.rs index c1a0f8ca1a..393a962ac4 100644 --- a/codex-rs/tools/src/image_detail_tests.rs +++ b/codex-rs/tools/src/image_detail_tests.rs @@ -1,4 +1,5 @@ use super::*; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::ImageDetail; use codex_protocol::openai_models::ModelInfo; @@ -66,17 +67,21 @@ fn explicit_original_is_dropped_without_model_support() { } #[test] -fn unsupported_non_original_detail_is_dropped() { +fn explicit_non_original_detail_is_preserved() { let model_info = model_info(); assert_eq!( normalize_output_image_detail(&model_info, Some(ImageDetail::Low)), - None + Some(ImageDetail::Low) + ); + assert_eq!( + normalize_output_image_detail(&model_info, Some(ImageDetail::High)), + Some(ImageDetail::High) ); } #[test] -fn sanitize_original_drops_original_without_support() { +fn sanitize_original_falls_back_to_high_without_support() { let mut items = vec![ FunctionCallOutputContentItem::InputText { text: "header".to_string(), @@ -101,7 +106,7 @@ fn sanitize_original_drops_original_without_support() { }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,AAA".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputImage { image_url: "data:image/png;base64,BBB".to_string(), diff --git a/codex-rs/tui/BUILD.bazel b/codex-rs/tui/BUILD.bazel index 33cc133431..d0aab0805a 100644 --- a/codex-rs/tui/BUILD.bazel +++ b/codex-rs/tui/BUILD.bazel @@ -24,4 +24,7 @@ codex_rust_crate( "//codex-rs/cli:codex", ], rustc_flags_extra = MACOS_WEBRTC_RUSTC_LINK_FLAGS, + test_shard_counts = { + "tui-unit-tests": 8, + }, ) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 7254272138..925dba245b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -28,6 +28,8 @@ use crate::cwd_prompt::CwdPromptAction; use crate::diff_render::DiffSummary; use crate::exec_command::split_command_string; use crate::exec_command::strip_bash_lc_and_escape; +use crate::external_agent_config_migration_startup::ExternalAgentConfigMigrationStartupOutcome; +use crate::external_agent_config_migration_startup::handle_external_agent_config_migration_prompt_if_needed; use crate::external_editor; use crate::file_search::FileSearchManager; use crate::history_cell; @@ -75,6 +77,8 @@ use codex_app_server_client::TypedRequestError; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo; use codex_app_server_protocol::ConfigLayerSource; +use codex_app_server_protocol::ConfigValueWriteParams; +use codex_app_server_protocol::ConfigWriteResponse; use codex_app_server_protocol::FeedbackUploadParams; use codex_app_server_protocol::FeedbackUploadResponse; use codex_app_server_protocol::GetAccountRateLimitsResponse; @@ -82,6 +86,7 @@ use codex_app_server_protocol::ListMcpServerStatusParams; use codex_app_server_protocol::ListMcpServerStatusResponse; use codex_app_server_protocol::McpServerStatus; use codex_app_server_protocol::McpServerStatusDetail; +use codex_app_server_protocol::MergeStrategy; use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallResponse; use codex_app_server_protocol::PluginListParams; @@ -479,16 +484,12 @@ fn emit_project_config_warnings(app_event_tx: &AppEventSender, config: &Config) let ConfigLayerSource::Project { dot_codex_folder } = &layer.name else { continue; }; - if layer.disabled_reason.is_none() { + let Some(disabled_reason) = &layer.disabled_reason else { continue; - } + }; disabled_folders.push(( dot_codex_folder.as_path().display().to_string(), - layer - .disabled_reason - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| "config.toml is disabled.".to_string()), + disabled_reason.clone(), )); } @@ -497,8 +498,8 @@ fn emit_project_config_warnings(app_event_tx: &AppEventSender, config: &Config) } let mut message = concat!( - "Project config.toml files are disabled in the following folders. ", - "Settings in those files are ignored, but skills and exec policies still load.\n", + "Project-local config, hooks, and exec policies are disabled in the following folders ", + "until the project is trusted, but skills still load.\n", ) .to_string(); for (index, (folder, reason)) in disabled_folders.iter().enumerate() { @@ -1045,6 +1046,10 @@ pub(crate) struct App { primary_session_configured: Option, pending_primary_events: VecDeque, pending_app_server_requests: PendingAppServerRequests, + // Serialize plugin enablement writes per plugin so stale completions cannot + // overwrite a newer toggle, even if the plugin is toggled from different + // cwd contexts. + pending_plugin_enabled_writes: HashMap>, } #[derive(Default)] @@ -2170,6 +2175,48 @@ impl App { }); } + fn set_plugin_enabled( + &mut self, + app_server: &AppServerSession, + cwd: PathBuf, + plugin_id: String, + enabled: bool, + ) { + if let Some(queued_enabled) = self.pending_plugin_enabled_writes.get_mut(&plugin_id) { + *queued_enabled = Some(enabled); + return; + } + + self.pending_plugin_enabled_writes + .insert(plugin_id.clone(), None); + self.spawn_plugin_enabled_write(app_server, cwd, plugin_id, enabled); + } + + fn spawn_plugin_enabled_write( + &mut self, + app_server: &AppServerSession, + cwd: PathBuf, + plugin_id: String, + enabled: bool, + ) { + let request_handle = app_server.request_handle(); + let app_event_tx = self.app_event_tx.clone(); + tokio::spawn(async move { + let cwd_for_event = cwd.clone(); + let plugin_id_for_event = plugin_id.clone(); + let result = write_plugin_enabled(request_handle, plugin_id, enabled) + .await + .map(|_| ()) + .map_err(|err| format!("Failed to update plugin config: {err}")); + app_event_tx.send(AppEvent::PluginEnabledSet { + cwd: cwd_for_event, + plugin_id: plugin_id_for_event, + enabled, + result, + }); + }); + } + fn refresh_plugin_mentions(&mut self) { let config = self.config.clone(); let app_event_tx = self.app_event_tx.clone(); @@ -3816,6 +3863,7 @@ impl App { session_selection: SessionSelection, feedback: codex_feedback::CodexFeedback, is_first_run: bool, + entered_trust_nux: bool, should_prompt_windows_sandbox_nux_at_startup: bool, remote_app_server_url: Option, remote_app_server_auth_token: Option, @@ -3833,6 +3881,38 @@ impl App { let harness_overrides = normalize_harness_overrides_for_cwd(harness_overrides, &config.cwd)?; + let external_agent_config_migration_outcome = + handle_external_agent_config_migration_prompt_if_needed( + tui, + &mut app_server, + &mut config, + &cli_kv_overrides, + &harness_overrides, + entered_trust_nux, + ) + .await?; + let external_agent_config_migration_message = match external_agent_config_migration_outcome + { + ExternalAgentConfigMigrationStartupOutcome::Continue { success_message } => { + success_message + } + ExternalAgentConfigMigrationStartupOutcome::ExitRequested => { + app_server + .shutdown() + .await + .inspect_err(|err| { + tracing::warn!("app-server shutdown failed: {err}"); + }) + .ok(); + return Ok(AppExitInfo { + token_usage: TokenUsage::default(), + thread_id: None, + thread_name: None, + update_action: None, + exit_reason: ExitReason::UserRequested, + }); + } + }; let bootstrap = app_server.bootstrap(&config).await?; let mut model = bootstrap.default_model; let available_models = bootstrap.available_models; @@ -4003,6 +4083,9 @@ impl App { (ChatWidget::new_with_app_event(init), Some(forked)) } }; + if let Some(message) = external_agent_config_migration_message { + chat_widget.add_info_message(message, /*hint*/ None); + } chat_widget .maybe_prompt_windows_sandbox_enable(should_prompt_windows_sandbox_nux_at_startup); @@ -4051,6 +4134,7 @@ impl App { primary_session_configured: None, pending_primary_events: VecDeque::new(), pending_app_server_requests: PendingAppServerRequests::default(), + pending_plugin_enabled_writes: HashMap::new(), }; if let Some(started) = initial_started_thread { app.enqueue_primary_thread_session(started.session, started.turns) @@ -4742,6 +4826,13 @@ impl App { } => { self.fetch_plugin_uninstall(app_server, cwd, plugin_id, plugin_display_name); } + AppEvent::SetPluginEnabled { + cwd, + plugin_id, + enabled, + } => { + self.set_plugin_enabled(app_server, cwd, plugin_id, enabled); + } AppEvent::PluginInstallLoaded { cwd, marketplace_path, @@ -4772,13 +4863,54 @@ impl App { app_server, cwd, PluginReadParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name, }, ); } } } + AppEvent::PluginEnabledSet { + cwd, + plugin_id, + enabled, + result, + } => { + let queued_enabled = self + .pending_plugin_enabled_writes + .get_mut(&plugin_id) + .and_then(Option::take); + let should_apply_result = if let Some(queued_enabled) = queued_enabled + && (result.is_err() || queued_enabled != enabled) + { + self.spawn_plugin_enabled_write( + app_server, + cwd.clone(), + plugin_id.clone(), + queued_enabled, + ); + false + } else { + true + }; + if should_apply_result { + self.pending_plugin_enabled_writes.remove(&plugin_id); + let update_succeeded = result.is_ok(); + if update_succeeded { + if let Err(err) = self.refresh_in_memory_config_from_disk().await { + tracing::warn!( + error = %err, + "failed to refresh config after plugin toggle" + ); + } + self.chat_widget.refresh_plugin_mentions(); + self.chat_widget.submit_op(AppCommand::reload_user_config()); + } + self.chat_widget + .on_plugin_enabled_set(cwd, plugin_id, enabled, result); + } + } AppEvent::FetchMcpInventory => { self.fetch_mcp_inventory(app_server); } @@ -6502,7 +6634,6 @@ async fn fetch_plugins_list( request_id, params: PluginListParams { cwds: Some(vec![cwd]), - force_remote_sync: false, }, }) .await @@ -6540,9 +6671,9 @@ async fn fetch_plugin_install( .request_typed(ClientRequest::PluginInstall { request_id, params: PluginInstallParams { - marketplace_path, + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, plugin_name, - force_remote_sync: false, }, }) .await @@ -6557,15 +6688,33 @@ async fn fetch_plugin_uninstall( request_handle .request_typed(ClientRequest::PluginUninstall { request_id, - params: PluginUninstallParams { - plugin_id, - force_remote_sync: false, - }, + params: PluginUninstallParams { plugin_id }, }) .await .wrap_err("plugin/uninstall failed in TUI") } +async fn write_plugin_enabled( + request_handle: AppServerRequestHandle, + plugin_id: String, + enabled: bool, +) -> Result { + let request_id = RequestId::String(format!("plugin-enable-{}", Uuid::new_v4())); + request_handle + .request_typed(ClientRequest::ConfigValueWrite { + request_id, + params: ConfigValueWriteParams { + key_path: format!("plugins.{plugin_id}"), + value: serde_json::json!({ "enabled": enabled }), + merge_strategy: MergeStrategy::Upsert, + file_path: None, + expected_version: None, + }, + }) + .await + .wrap_err("config/value/write failed while updating plugin enablement in TUI") +} + fn build_feedback_upload_params( origin_thread_id: Option, rollout_path: Option, @@ -6759,19 +6908,18 @@ mod tests { marketplaces: vec![ PluginMarketplaceEntry { name: "openai-bundled".to_string(), - path: test_absolute_path("/marketplaces/openai-bundled"), + path: Some(test_absolute_path("/marketplaces/openai-bundled")), interface: None, plugins: Vec::new(), }, PluginMarketplaceEntry { name: "openai-curated".to_string(), - path: test_absolute_path("/marketplaces/openai-curated"), + path: Some(test_absolute_path("/marketplaces/openai-curated")), interface: None, plugins: Vec::new(), }, ], marketplace_load_errors: Vec::new(), - remote_sync_error: None, featured_plugin_ids: Vec::new(), }; @@ -6781,7 +6929,7 @@ mod tests { response.marketplaces, vec![PluginMarketplaceEntry { name: "openai-curated".to_string(), - path: test_absolute_path("/marketplaces/openai-curated"), + path: Some(test_absolute_path("/marketplaces/openai-curated")), interface: None, plugins: Vec::new(), }] @@ -9817,6 +9965,7 @@ guardian_approval = true primary_session_configured: None, pending_primary_events: VecDeque::new(), pending_app_server_requests: PendingAppServerRequests::default(), + pending_plugin_enabled_writes: HashMap::new(), } } @@ -9874,6 +10023,7 @@ guardian_approval = true primary_session_configured: None, pending_primary_events: VecDeque::new(), pending_app_server_requests: PendingAppServerRequests::default(), + pending_plugin_enabled_writes: HashMap::new(), }, rx, op_rx, diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index 3dfc9fd235..f0abb5326e 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -421,6 +421,7 @@ fn server_notification_thread_target( ServerNotification::ThreadRealtimeClosed(notification) => { Some(notification.thread_id.as_str()) } + ServerNotification::Warning(notification) => notification.thread_id.as_deref(), ServerNotification::SkillsChanged(_) | ServerNotification::McpServerStatusUpdated(_) | ServerNotification::McpServerOauthLoginCompleted(_) @@ -1049,8 +1050,10 @@ fn app_server_codex_error_info_to_core( #[cfg(test)] mod tests { + use super::ServerNotificationThreadTarget; use super::command_execution_started_event; use super::server_notification_thread_events; + use super::server_notification_thread_target; use super::thread_snapshot_events; use super::turn_snapshot_events; use codex_app_server_protocol::AgentMessageDeltaNotification; @@ -1070,6 +1073,7 @@ mod tests { use codex_app_server_protocol::TurnCompletedNotification; use codex_app_server_protocol::TurnError; use codex_app_server_protocol::TurnStatus; + use codex_app_server_protocol::WarningNotification; use codex_protocol::ThreadId; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; @@ -1664,4 +1668,17 @@ mod tests { assert_eq!(raw_reasoning.text, "hidden chain"); assert!(matches!(events[3].msg, EventMsg::TurnComplete(_))); } + + #[test] + fn warning_notifications_route_to_threads_when_thread_id_is_present() { + let thread_id = ThreadId::new(); + let notification = ServerNotification::Warning(WarningNotification { + thread_id: Some(thread_id.to_string()), + message: "warning".to_string(), + }); + + let target = server_notification_thread_target(¬ification); + + assert_eq!(target, ServerNotificationThreadTarget::Thread(thread_id)); + } } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 106bb7a912..3f138b219d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -283,6 +283,21 @@ pub(crate) enum AppEvent { result: Result, }, + /// Enable or disable an installed plugin. + SetPluginEnabled { + cwd: PathBuf, + plugin_id: String, + enabled: bool, + }, + + /// Result of enabling or disabling a plugin. + PluginEnabledSet { + cwd: PathBuf, + plugin_id: String, + enabled: bool, + result: Result<(), String>, + }, + /// Refresh plugin mention bindings from the current config. RefreshPluginMentions, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 7db3dd7d39..d31d2e9732 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -14,6 +14,11 @@ use codex_app_server_protocol::AuthMode; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::ConfigBatchWriteParams; use codex_app_server_protocol::ConfigWriteResponse; +use codex_app_server_protocol::ExternalAgentConfigDetectParams; +use codex_app_server_protocol::ExternalAgentConfigDetectResponse; +use codex_app_server_protocol::ExternalAgentConfigImportParams; +use codex_app_server_protocol::ExternalAgentConfigImportResponse; +use codex_app_server_protocol::ExternalAgentConfigMigrationItem; use codex_app_server_protocol::GetAccountParams; use codex_app_server_protocol::GetAccountRateLimitsResponse; use codex_app_server_protocol::GetAccountResponse; @@ -286,6 +291,31 @@ impl AppServerSession { .wrap_err("account/read failed during TUI bootstrap") } + pub(crate) async fn external_agent_config_detect( + &mut self, + params: ExternalAgentConfigDetectParams, + ) -> Result { + let request_id = self.next_request_id(); + self.client + .request_typed(ClientRequest::ExternalAgentConfigDetect { request_id, params }) + .await + .wrap_err("externalAgentConfig/detect failed during TUI startup") + } + + pub(crate) async fn external_agent_config_import( + &mut self, + migration_items: Vec, + ) -> Result { + let request_id = self.next_request_id(); + self.client + .request_typed(ClientRequest::ExternalAgentConfigImport { + request_id, + params: ExternalAgentConfigImportParams { migration_items }, + }) + .await + .wrap_err("externalAgentConfig/import failed during TUI startup") + } + pub(crate) async fn next_event(&mut self) -> Option { self.client.next_event().await } diff --git a/codex-rs/tui/src/bottom_pane/list_selection_view.rs b/codex-rs/tui/src/bottom_pane/list_selection_view.rs index eecf1bc580..bbcd2f1c7e 100644 --- a/codex-rs/tui/src/bottom_pane/list_selection_view.rs +++ b/codex-rs/tui/src/bottom_pane/list_selection_view.rs @@ -101,6 +101,12 @@ pub(crate) enum SelectionRowDisplay { /// One selectable item in the generic selection list. pub(crate) type SelectionAction = Box; +pub(crate) type SelectionToggleAction = dyn Fn(bool, &AppEventSender) + Send + Sync; + +pub(crate) struct SelectionToggle { + pub is_on: bool, + pub action: Box, +} /// Callback invoked whenever the highlighted item changes (arrow keys, search /// filter, number-key jump). Receives the *actual* index into the unfiltered @@ -122,6 +128,8 @@ pub(crate) type OnCancelCallback = Option>, + pub toggle: Option, + pub toggle_placeholder: Option<&'static str>, pub display_shortcut: Option, pub description: Option, pub selected_description: Option, @@ -345,6 +353,15 @@ impl ListSelectionView { .unwrap_or(self.items.as_slice()) } + fn active_items_mut(&mut self) -> &mut [SelectionItem] { + if let Some(idx) = self.active_tab_idx + && let Some(tab) = self.tabs.get_mut(idx) + { + return tab.items.as_mut_slice(); + } + self.items.as_mut_slice() + } + fn active_header(&self) -> &dyn Renderable { self.active_tab_idx .and_then(|idx| self.tabs.get(idx)) @@ -454,6 +471,11 @@ impl ListSelectionView { let wrap_prefix_width = UnicodeWidthStr::width(wrap_prefix.as_str()); let mut name_prefix_spans = Vec::new(); name_prefix_spans.push(wrap_prefix.into()); + if let Some(toggle) = &item.toggle { + name_prefix_spans.push(if toggle.is_on { "[*] " } else { "[ ] " }.into()); + } else if let Some(placeholder) = item.toggle_placeholder { + name_prefix_spans.push(placeholder.into()); + } name_prefix_spans.extend(item.name_prefix_spans.clone()); let description = is_selected .then(|| item.selected_description.clone()) @@ -514,6 +536,44 @@ impl ListSelectionView { self.state.scroll_top = 0; } + fn selected_item_has_toggle(&self) -> bool { + self.selected_actual_idx() + .and_then(|actual_idx| self.active_items().get(actual_idx)) + .is_some_and(|item| { + item.toggle.is_some() && item.disabled_reason.is_none() && !item.is_disabled + }) + } + + fn selected_item_has_toggle_placeholder(&self) -> bool { + self.selected_actual_idx() + .and_then(|actual_idx| self.active_items().get(actual_idx)) + .is_some_and(|item| { + item.toggle.is_none() + && item.toggle_placeholder.is_some() + && item.disabled_reason.is_none() + && !item.is_disabled + }) + } + + fn toggle_selected(&mut self) { + let Some(actual_idx) = self.selected_actual_idx() else { + return; + }; + let app_event_tx = self.app_event_tx.clone(); + let Some(item) = self.active_items_mut().get_mut(actual_idx) else { + return; + }; + if item.is_disabled || item.disabled_reason.is_some() { + return; + } + let Some(toggle) = item.toggle.as_mut() else { + return; + }; + + toggle.is_on = !toggle.is_on; + (toggle.action)(toggle.is_on, &app_event_tx); + } + fn move_up(&mut self) { let before = self.selected_actual_idx(); let len = self.visible_len(); @@ -742,6 +802,22 @@ impl BottomPaneView for ListSelectionView { self.search_query.pop(); self.apply_filter(); } + KeyEvent { + code: KeyCode::Char(' '), + modifiers: KeyModifiers::NONE, + .. + } if self.selected_item_has_toggle() + && (!self.is_searchable || self.search_query.is_empty()) => + { + self.toggle_selected() + } + KeyEvent { + code: KeyCode::Char(' '), + modifiers: KeyModifiers::NONE, + .. + } if self.is_searchable + && self.search_query.is_empty() + && self.selected_item_has_toggle_placeholder() => {} KeyEvent { code: KeyCode::Esc, .. } => { @@ -1536,6 +1612,45 @@ mod tests { assert_eq!(view.selected_actual_idx(), Some(1)); } + #[test] + fn space_appends_to_active_search_instead_of_toggling_selected_item() { + let (tx_raw, mut rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx_raw); + let mut view = ListSelectionView::new( + SelectionViewParams { + items: vec![SelectionItem { + name: "Plugin".to_string(), + toggle: Some(SelectionToggle { + is_on: false, + action: Box::new(|_enabled, tx: &_| { + tx.send(AppEvent::OpenApprovalsPopup); + }), + }), + ..Default::default() + }], + is_searchable: true, + ..Default::default() + }, + tx, + ); + view.set_search_query("plugin".to_string()); + + view.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); + + assert_eq!(view.search_query, "plugin "); + assert!( + !view.active_items()[0] + .toggle + .as_ref() + .is_some_and(|toggle| toggle.is_on), + "expected Space to leave the toggle state unchanged while search is active" + ); + assert!( + rx.try_recv().is_err(), + "expected Space with an active search query to avoid firing the toggle action" + ); + } + #[test] fn single_line_row_display_truncates_instead_of_wrapping() { let (tx_raw, _rx) = unbounded_channel::(); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 427a77084e..6411f0182e 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -91,6 +91,7 @@ mod slash_commands; pub(crate) use footer::CollaborationModeIndicator; pub(crate) use list_selection_view::ColumnWidthMode; pub(crate) use list_selection_view::SelectionRowDisplay; +pub(crate) use list_selection_view::SelectionToggle; pub(crate) use list_selection_view::SelectionViewParams; pub(crate) use list_selection_view::SideContentWidth; pub(crate) use list_selection_view::popup_content_width; diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6257b9c878..4112ae00b9 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6201,6 +6201,7 @@ impl ChatWidget { self.refresh_skills_for_current_cwd(/*force_reload*/ true); } ServerNotification::ModelRerouted(_) => {} + ServerNotification::Warning(notification) => self.on_warning(notification.message), ServerNotification::DeprecationNotice(notification) => { self.on_deprecation_notice(DeprecationNoticeEvent { summary: notification.summary, diff --git a/codex-rs/tui/src/chatwidget/plugins.rs b/codex-rs/tui/src/chatwidget/plugins.rs index 964a986314..f4dfe6dbd9 100644 --- a/codex-rs/tui/src/chatwidget/plugins.rs +++ b/codex-rs/tui/src/chatwidget/plugins.rs @@ -5,9 +5,11 @@ use std::time::Instant; use super::ChatWidget; use crate::app_event::AppEvent; use crate::bottom_pane::ColumnWidthMode; +use crate::bottom_pane::SelectionAction; use crate::bottom_pane::SelectionItem; use crate::bottom_pane::SelectionRowDisplay; use crate::bottom_pane::SelectionTab; +use crate::bottom_pane::SelectionToggle; use crate::bottom_pane::SelectionViewParams; use crate::history_cell; use crate::legacy_core::plugins::OPENAI_CURATED_MARKETPLACE_NAME; @@ -40,7 +42,7 @@ const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection"; const ALL_PLUGINS_TAB_ID: &str = "all-plugins"; const INSTALLED_PLUGINS_TAB_ID: &str = "installed-plugins"; const OPENAI_CURATED_TAB_ID: &str = "marketplace:openai-curated"; -const PLUGIN_ROW_PREFIX_WIDTH: usize = 2; +const PLUGIN_ROW_PREFIX_WIDTH: usize = 6; const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1); const LOADING_ANIMATION_INTERVAL: Duration = Duration::from_millis(100); @@ -233,9 +235,12 @@ impl ChatWidget { fn open_plugins_popup(&mut self, response: &PluginListResponse) { self.plugins_active_tab_id = Some(ALL_PLUGINS_TAB_ID.to_string()); - self.bottom_pane.show_selection_view( - self.plugins_popup_params(response, self.plugins_active_tab_id.clone()), - ); + self.bottom_pane + .show_selection_view(self.plugins_popup_params( + response, + self.plugins_active_tab_id.clone(), + /*initial_selected_idx*/ None, + )); } pub(crate) fn open_plugin_detail_loading_popup(&mut self, plugin_display_name: &str) { @@ -356,6 +361,49 @@ impl ChatWidget { } } + pub(crate) fn on_plugin_enabled_set( + &mut self, + cwd: PathBuf, + plugin_id: String, + enabled: bool, + result: Result<(), String>, + ) { + if self.config.cwd.as_path() != cwd.as_path() { + return; + } + + if let Err(err) = result { + self.add_error_message(format!( + "Failed to update plugin config for {plugin_id}: {err}" + )); + if let PluginsCacheState::Ready(response) = self.plugins_cache_for_current_cwd() { + self.refresh_plugins_popup_if_open(&response); + } + return; + } + + let refreshed_response = match &mut self.plugins_cache { + PluginsCacheState::Ready(response) + if self.plugins_fetch_state.cache_cwd.as_deref() == Some(cwd.as_path()) => + { + for plugin in response + .marketplaces + .iter_mut() + .flat_map(|marketplace| marketplace.plugins.iter_mut()) + .filter(|plugin| plugin.id == plugin_id) + { + plugin.enabled = enabled; + } + Some(response.clone()) + } + _ => None, + }; + + if let Some(response) = refreshed_response { + self.refresh_plugins_popup_if_open(&response); + } + } + pub(crate) fn on_plugin_uninstall_loaded( &mut self, cwd: PathBuf, @@ -563,7 +611,11 @@ impl ChatWidget { let tab_id = self.plugins_active_tab_id.clone(); let _ = self.bottom_pane.replace_selection_view_if_active( PLUGINS_SELECTION_VIEW_ID, - self.plugins_popup_params(&plugins_response, tab_id), + self.plugins_popup_params( + &plugins_response, + tab_id, + /*initial_selected_idx*/ None, + ), ); } } @@ -574,10 +626,13 @@ impl ChatWidget { .active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID) .map(str::to_string) .or_else(|| self.plugins_active_tab_id.clone()); + let selected_idx = self + .bottom_pane + .selected_index_for_active_view(PLUGINS_SELECTION_VIEW_ID); self.plugins_active_tab_id = active_tab_id.clone(); let _ = self.bottom_pane.replace_selection_view_if_active( PLUGINS_SELECTION_VIEW_ID, - self.plugins_popup_params(response, active_tab_id), + self.plugins_popup_params(response, active_tab_id, selected_idx), ); } @@ -727,6 +782,7 @@ impl ChatWidget { &self, response: &PluginListResponse, active_tab_id: Option, + initial_selected_idx: Option, ) -> SelectionViewParams { let marketplaces: Vec<&PluginMarketplaceEntry> = response.marketplaces.iter().collect(); @@ -760,7 +816,6 @@ impl ChatWidget { header: plugins_header( "Browse plugins from available marketplaces.".to_string(), format!("Installed {installed} of {total} available plugins."), - response.remote_sync_error.as_deref(), ), items: self.plugin_selection_items( all_entries, @@ -776,7 +831,6 @@ impl ChatWidget { header: plugins_header( "Installed plugins.".to_string(), format!("Showing {installed} installed plugins."), - response.remote_sync_error.as_deref(), ), items: self.plugin_selection_items( installed_entries, @@ -804,7 +858,6 @@ impl ChatWidget { header: plugins_header( "OpenAI Curated marketplace.".to_string(), format!("Installed {curated_installed} of {curated_total} OpenAI Curated plugins."), - response.remote_sync_error.as_deref(), ), items: self.plugin_selection_items( curated_entries, @@ -848,7 +901,6 @@ impl ChatWidget { format!( "Installed {marketplace_installed} of {marketplace_total} {label} plugins." ), - response.remote_sync_error.as_deref(), ), items: self.plugin_selection_items( entries, @@ -870,6 +922,7 @@ impl ChatWidget { col_width_mode: ColumnWidthMode::AutoAllRows, row_display: SelectionRowDisplay::SingleLine, name_column_width, + initial_selected_idx, ..Default::default() } } @@ -1036,9 +1089,22 @@ impl ChatWidget { } else { plugin_brief_description_without_marketplace(plugin, status_label_width) }; + let can_view_details = marketplace.path.is_some(); let selected_status_label = format!("{status_label: = if let Some(marketplace_path) = marketplace_path { + vec![Box::new(move |tx| { tx.send(AppEvent::OpenPluginDetailLoading { plugin_display_name: plugin_display_name.clone(), }); tx.send(AppEvent::FetchPluginDetail { cwd: cwd.clone(), params: codex_app_server_protocol::PluginReadParams { - marketplace_path: marketplace_path.clone(), + marketplace_path: Some(marketplace_path.clone()), + remote_marketplace_name: None, plugin_name: plugin_name.clone(), }, }); - })], + })] + } else { + Vec::new() + }; + let is_disabled = !can_view_details && !plugin.installed; + let disabled_reason = + is_disabled.then(|| "remote plugin details are not available yet".to_string()); + + items.push(SelectionItem { + name: display_name, + toggle, + toggle_placeholder: (!plugin.installed).then_some("[-] "), + description: Some(description), + selected_description: Some(selected_description), + search_value: Some(search_value), + actions, + is_disabled, + disabled_reason, ..Default::default() }); } @@ -1082,27 +1173,18 @@ impl ChatWidget { } fn plugins_popup_hint_line() -> Line<'static> { - Line::from("←/→ select marketplace · enter view details · esc close") + Line::from("space enable/disable · ←/→ select marketplace · enter view details · esc close") } fn plugin_detail_hint_line() -> Line<'static> { Line::from("Press esc to close.") } -fn plugins_header( - subtitle: String, - count_line: String, - remote_sync_error: Option<&str>, -) -> Box { +fn plugins_header(subtitle: String, count_line: String) -> Box { let mut header = ColumnRenderable::new(); header.push(Line::from("Plugins".bold())); header.push(Line::from(subtitle.dim())); header.push(Line::from(count_line.dim())); - if let Some(remote_sync_error) = remote_sync_error { - header.push(Line::from( - format!("Using cached marketplace data: {remote_sync_error}").dim(), - )); - } Box::new(header) } @@ -1138,7 +1220,10 @@ fn sort_plugin_entries(entries: &mut [(&PluginMarketplaceEntry, &PluginSummary, } fn marketplace_tab_id(marketplace: &PluginMarketplaceEntry) -> String { - format!("marketplace:{}", marketplace.path.display()) + match marketplace.path.as_ref() { + Some(path) => format!("marketplace:{}", path.display()), + None => format!("marketplace:{}", marketplace.name), + } } fn disambiguate_duplicate_tab_labels(labels: Vec) -> Vec { @@ -1236,7 +1321,7 @@ fn plugin_status_label(plugin: &PluginSummary) -> &'static str { match plugin.install_policy { PluginInstallPolicy::NotAvailable => "Not installable", PluginInstallPolicy::Available => "Available", - PluginInstallPolicy::InstalledByDefault => "Available by default", + PluginInstallPolicy::InstalledByDefault => "Available", } } } diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap index e43251eb9d..e0f99b443e 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_curated_marketplace.snap @@ -1,18 +1,17 @@ --- -source: tui/src/chatwidget/tests.rs +source: tui/src/chatwidget/tests/popups_and_settings.rs expression: popup --- Plugins Browse plugins from available marketplaces. Installed 1 of 4 available plugins. - Using cached marketplace data: remote sync timed out [All Plugins] Installed (1) OpenAI Curated Repo Marketplace Type to search plugins -› Alpha Sync Disabled Press Enter to view plugin details. - Bravo Search Available · ChatGPT Marketplace · Search docs and tickets. - Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins. - Starter Available by default · ChatGPT Marketplace · Included by default. +› [ ] Alpha Sync Disabled Space to enable; Enter view details. + [-] Bravo Search Available · ChatGPT Marketplace · Search docs and tickets. + [-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins. + [-] Starter Available · ChatGPT Marketplace · Included by default. - ←/→ select marketplace · enter view details · esc close + space enable/disable · ←/→ select marketplace · enter view details · esc close diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap index 49f89be0bb..da1b0913fe 100644 --- a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__plugins_popup_search_filtered.snap @@ -9,6 +9,6 @@ expression: popup [All Plugins] Installed (0) OpenAI Curated sla -› Slack Available Press Enter to view plugin details. +› [-] Slack Available Press Enter to view plugin details. - ←/→ select marketplace · enter view details · esc close + space enable/disable · ←/→ select marketplace · enter view details · esc close diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index e92a809e80..6067f3a768 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -46,6 +46,7 @@ pub(super) use codex_app_server_protocol::CommandAction as AppServerCommandActio pub(super) use codex_app_server_protocol::CommandExecutionRequestApprovalParams as AppServerCommandExecutionRequestApprovalParams; pub(super) use codex_app_server_protocol::CommandExecutionSource as AppServerCommandExecutionSource; pub(super) use codex_app_server_protocol::CommandExecutionStatus as AppServerCommandExecutionStatus; +pub(super) use codex_app_server_protocol::ConfigWarningNotification; pub(super) use codex_app_server_protocol::ErrorNotification; pub(super) use codex_app_server_protocol::FileUpdateChange; pub(super) use codex_app_server_protocol::GuardianApprovalReview; @@ -94,6 +95,7 @@ pub(super) use codex_app_server_protocol::TurnError as AppServerTurnError; pub(super) use codex_app_server_protocol::TurnStartedNotification; pub(super) use codex_app_server_protocol::TurnStatus as AppServerTurnStatus; pub(super) use codex_app_server_protocol::UserInput as AppServerUserInput; +pub(super) use codex_app_server_protocol::WarningNotification; pub(super) use codex_config::types::ApprovalsReviewer; pub(super) use codex_config::types::Notifications; #[cfg(target_os = "windows")] diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index acfd358a25..702bd08ef9 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -184,6 +184,57 @@ async fn live_app_server_turn_started_sets_feedback_turn_id() { ); } +#[tokio::test] +async fn live_app_server_warning_notification_renders_message() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.handle_server_notification( + ServerNotification::Warning(WarningNotification { + thread_id: None, + message: "Some enabled skills were not included in the model-visible skills list for this session. Mention a skill by name or path if you need it.".to_string(), + }), + /*replay_kind*/ None, + ); + + let cells = drain_insert_history(&mut rx); + assert_eq!(cells.len(), 1, "expected one warning history cell"); + let rendered = lines_to_single_string(&cells[0]); + let normalized = rendered.split_whitespace().collect::>().join(" "); + assert!( + normalized.contains( + "Some enabled skills were not included in the model-visible skills list for this session." + ), + "expected warning notification message, got {rendered}" + ); + assert!( + normalized.contains("Mention a skill by name or path if you need it."), + "expected warning guidance, got {rendered}" + ); +} + +#[tokio::test] +async fn live_app_server_config_warning_prefixes_summary() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.handle_server_notification( + ServerNotification::ConfigWarning(ConfigWarningNotification { + summary: "Invalid configuration; using defaults.".to_string(), + details: None, + path: None, + range: None, + }), + /*replay_kind*/ None, + ); + + let cells = drain_insert_history(&mut rx); + assert_eq!(cells.len(), 1, "expected one warning history cell"); + let rendered = lines_to_single_string(&cells[0]); + assert!( + rendered.contains("Invalid configuration; using defaults."), + "expected config warning summary, got {rendered}" + ); +} + #[tokio::test] async fn live_app_server_file_change_item_started_preserves_changes() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tests/helpers.rs b/codex-rs/tui/src/chatwidget/tests/helpers.rs index 89de317f72..66df972ebf 100644 --- a/codex-rs/tui/src/chatwidget/tests/helpers.rs +++ b/codex-rs/tui/src/chatwidget/tests/helpers.rs @@ -859,8 +859,11 @@ pub(super) fn plugins_test_interface( default_prompt: None, brand_color: None, composer_icon: None, + composer_icon_url: None, logo: None, + logo_url: None, screenshots: Vec::new(), + screenshot_urls: Vec::new(), } } @@ -896,7 +899,7 @@ pub(super) fn plugins_test_curated_marketplace( ) -> PluginMarketplaceEntry { PluginMarketplaceEntry { name: OPENAI_CURATED_MARKETPLACE_NAME.to_string(), - path: plugins_test_absolute_path("marketplaces/chatgpt"), + path: Some(plugins_test_absolute_path("marketplaces/chatgpt")), interface: Some(MarketplaceInterface { display_name: Some("ChatGPT Marketplace".to_string()), }), @@ -907,7 +910,7 @@ pub(super) fn plugins_test_curated_marketplace( pub(super) fn plugins_test_repo_marketplace(plugins: Vec) -> PluginMarketplaceEntry { PluginMarketplaceEntry { name: "repo".to_string(), - path: plugins_test_absolute_path("marketplaces/repo"), + path: Some(plugins_test_absolute_path("marketplaces/repo")), interface: Some(MarketplaceInterface { display_name: Some("Repo Marketplace".to_string()), }), @@ -921,7 +924,6 @@ pub(super) fn plugins_test_response( PluginListResponse { marketplaces, marketplace_load_errors: Vec::new(), - remote_sync_error: None, featured_plugin_ids: Vec::new(), } } diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index 859be3b94a..a3792c31f4 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -105,7 +105,7 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_ let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); - let mut response = plugins_test_response(vec![ + let response = plugins_test_response(vec![ plugins_test_curated_marketplace(vec![ plugins_test_summary( "plugin-bravo", @@ -145,8 +145,6 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_ PluginInstallPolicy::Available, )]), ]); - response.remote_sync_error = Some("remote sync timed out".to_string()); - let popup = render_loaded_plugins_popup(&mut chat, response); assert_chatwidget_snapshot!("plugins_popup_curated_marketplace", popup); assert!( @@ -249,7 +247,7 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() { } #[tokio::test] -async fn plugins_popup_refresh_replaces_selection_with_first_row() { +async fn plugins_popup_refresh_preserves_selected_row_position() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); @@ -278,7 +276,7 @@ async fn plugins_popup_refresh_replaces_selection_with_first_row() { let before = render_bottom_popup(&chat, /*width*/ 100); assert!( - before.contains("› Slack"), + before.contains("› [-] Slack"), "expected Slack to be selected before refresh, got:\n{before}" ); @@ -316,8 +314,12 @@ async fn plugins_popup_refresh_replaces_selection_with_first_row() { let after = render_bottom_popup(&chat, /*width*/ 100); assert!( - after.contains("› Airtable"), - "expected refresh to rebuild the popup from the new first row, got:\n{after}" + after.contains("› [-] Notion"), + "expected refresh to preserve the selected row position, got:\n{after}" + ); + assert!( + after.contains("Airtable"), + "expected refreshed popup to include the updated plugin list, got:\n{after}" ); assert!( after.contains("Slack"), @@ -389,11 +391,153 @@ async fn plugins_popup_refreshes_installed_counts_after_install() { "expected /plugins to refresh installed counts after install, got:\n{after}" ); assert!( - after.contains("Installed Press Enter to view plugin details."), + after.contains("Installed Space to disable; Enter view details."), "expected refreshed selected row copy to reflect the installed plugin state, got:\n{after}" ); } +#[tokio::test] +async fn plugins_popup_space_toggles_installed_plugin_from_list() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); + + let cwd = chat.config.cwd.to_path_buf(); + render_loaded_plugins_popup( + &mut chat, + plugins_test_response(vec![plugins_test_curated_marketplace(vec![ + plugins_test_summary( + "plugin-calendar", + "calendar", + Some("Calendar"), + Some("Schedule management."), + /*installed*/ true, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + plugins_test_summary( + "plugin-drive", + "drive", + Some("Drive"), + Some("Document access."), + /*installed*/ true, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + ])]), + ); + + while rx.try_recv().is_ok() {} + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); + + match rx.try_recv() { + Ok(AppEvent::SetPluginEnabled { + cwd: event_cwd, + plugin_id, + enabled, + }) => { + assert_eq!(event_cwd, cwd); + assert_eq!(plugin_id, "plugin-drive"); + assert!(!enabled); + } + other => panic!("expected SetPluginEnabled event, got {other:?}"), + } + + chat.on_plugin_enabled_set( + cwd, + "plugin-drive".to_string(), + /*enabled*/ false, + Ok(()), + ); + + let popup = render_bottom_popup(&chat, /*width*/ 100); + assert!( + popup.contains("› [ ] Drive"), + "expected selected plugin row to stay selected after refresh, got:\n{popup}" + ); +} + +#[tokio::test] +async fn plugins_popup_space_on_uninstalled_row_does_not_start_search() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); + + render_loaded_plugins_popup( + &mut chat, + plugins_test_response(vec![plugins_test_curated_marketplace(vec![ + plugins_test_summary( + "plugin-calendar", + "calendar", + Some("Calendar"), + Some("Schedule management."), + /*installed*/ false, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + plugins_test_summary( + "plugin-drive", + "drive", + Some("Drive"), + Some("Document access."), + /*installed*/ false, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + ])]), + ); + + while rx.try_recv().is_ok() {} + let before = render_bottom_popup(&chat, /*width*/ 100); + chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); + let after = render_bottom_popup(&chat, /*width*/ 100); + + assert!( + rx.try_recv().is_err(), + "did not expect Space on an uninstalled plugin to emit an event" + ); + assert_eq!(after, before); +} + +#[tokio::test] +async fn plugins_popup_space_with_active_search_does_not_toggle_installed_plugin() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true); + + render_loaded_plugins_popup( + &mut chat, + plugins_test_response(vec![plugins_test_curated_marketplace(vec![ + plugins_test_summary( + "plugin-calendar", + "calendar", + Some("Calendar"), + Some("Schedule management."), + /*installed*/ true, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + plugins_test_summary( + "plugin-drive", + "drive", + Some("Drive"), + Some("Document access."), + /*installed*/ true, + /*enabled*/ true, + PluginInstallPolicy::Available, + ), + ])]), + ); + + while rx.try_recv().is_ok() {} + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + type_plugins_search_query(&mut chat, "dr"); + chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); + + assert!( + rx.try_recv().is_err(), + "did not expect Space with an active plugin search to emit a toggle event" + ); +} + #[tokio::test] async fn plugins_popup_search_filters_visible_rows_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -544,7 +688,9 @@ async fn plugins_popup_refresh_preserves_duplicate_marketplace_tab_by_path() { let response = plugins_test_response(vec![ PluginMarketplaceEntry { name: "duplicate".to_string(), - path: plugins_test_absolute_path("marketplaces/home/marketplace.json"), + path: Some(plugins_test_absolute_path( + "marketplaces/home/marketplace.json", + )), interface: Some(MarketplaceInterface { display_name: Some("Duplicate Marketplace".to_string()), }), @@ -560,7 +706,9 @@ async fn plugins_popup_refresh_preserves_duplicate_marketplace_tab_by_path() { }, PluginMarketplaceEntry { name: "duplicate".to_string(), - path: plugins_test_absolute_path("marketplaces/repo/marketplace.json"), + path: Some(plugins_test_absolute_path( + "marketplaces/repo/marketplace.json", + )), interface: Some(MarketplaceInterface { display_name: Some("Duplicate Marketplace".to_string()), }), diff --git a/codex-rs/tui/src/external_agent_config_migration.rs b/codex-rs/tui/src/external_agent_config_migration.rs new file mode 100644 index 0000000000..2b667b802a --- /dev/null +++ b/codex-rs/tui/src/external_agent_config_migration.rs @@ -0,0 +1,1024 @@ +use crate::diff_render::display_path_for; +use crate::key_hint; +use crate::line_truncation::truncate_line_with_ellipsis_if_overflow; +use crate::render::Insets; +use crate::render::RectExt as _; +use crate::selection_list::selection_option_row_with_dim; +use crate::tui::FrameRequester; +use crate::tui::Tui; +use crate::tui::TuiEvent; +use codex_app_server_protocol::ExternalAgentConfigMigrationItem; +use codex_app_server_protocol::PluginsMigration; +use crossterm::event::KeyCode; +use crossterm::event::KeyEvent; +use crossterm::event::KeyEventKind; +use crossterm::event::KeyModifiers; +use ratatui::buffer::Buffer; +use ratatui::layout::Constraint; +use ratatui::layout::Layout; +use ratatui::layout::Rect; +use ratatui::prelude::Stylize as _; +use ratatui::text::Line; +use ratatui::widgets::Clear; +use ratatui::widgets::Paragraph; +use ratatui::widgets::Widget; +use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; +use tokio_stream::StreamExt; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ExternalAgentConfigMigrationOutcome { + Proceed(Vec), + Skip, + SkipForever, + Exit, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum FocusArea { + Items, + Actions, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ActionMenuOption { + Proceed, + Skip, + SkipForever, +} + +impl ActionMenuOption { + fn label(self) -> &'static str { + match self { + Self::Proceed => "Proceed with selected", + Self::Skip => "Skip for now", + Self::SkipForever => "Don't ask again", + } + } + + fn previous(self) -> Option { + match self { + Self::Proceed => None, + Self::Skip => Some(Self::Proceed), + Self::SkipForever => Some(Self::Skip), + } + } + + fn next(self) -> Option { + match self { + Self::Proceed => Some(Self::Skip), + Self::Skip => Some(Self::SkipForever), + Self::SkipForever => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct MigrationSelection { + item: ExternalAgentConfigMigrationItem, + enabled: bool, +} + +struct RenderLineEntry { + item_idx: Option, + kind: RenderLineKind, + line: Line<'static>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum RenderLineKind { + Section, + Item, + ItemDetail, +} + +pub(crate) async fn run_external_agent_config_migration_prompt( + tui: &mut Tui, + items: &[ExternalAgentConfigMigrationItem], + selected_items: &[ExternalAgentConfigMigrationItem], + error: Option<&str>, +) -> ExternalAgentConfigMigrationOutcome { + let mut screen = ExternalAgentConfigMigrationScreen::new( + tui.frame_requester(), + items, + selected_items, + error.map(str::to_owned), + ); + + let _ = tui.draw(u16::MAX, |frame| { + frame.render_widget_ref(&screen, frame.area()); + }); + + let events = tui.event_stream(); + tokio::pin!(events); + + while !screen.is_done() { + if let Some(event) = events.next().await { + match event { + TuiEvent::Key(key_event) => screen.handle_key(key_event), + TuiEvent::Paste(_) => {} + TuiEvent::Draw => { + let _ = tui.draw(u16::MAX, |frame| { + frame.render_widget_ref(&screen, frame.area()); + }); + } + } + } else { + screen.skip(); + break; + } + } + + screen.outcome() +} + +struct ExternalAgentConfigMigrationScreen { + request_frame: FrameRequester, + items: Vec, + selected_item_idx: Option, + scroll_top: usize, + focus: FocusArea, + highlighted_action: ActionMenuOption, + done: bool, + outcome: ExternalAgentConfigMigrationOutcome, + error: Option, +} + +impl ExternalAgentConfigMigrationScreen { + fn proceed_enabled(&self) -> bool { + self.selected_count() > 0 + } + + fn first_available_action(&self) -> ActionMenuOption { + if self.proceed_enabled() { + ActionMenuOption::Proceed + } else { + ActionMenuOption::Skip + } + } + + fn previous_available_action(&self, action: ActionMenuOption) -> Option { + let mut candidate = action.previous(); + while let Some(option) = candidate { + if option != ActionMenuOption::Proceed || self.proceed_enabled() { + return Some(option); + } + candidate = option.previous(); + } + None + } + + fn next_available_action(&self, action: ActionMenuOption) -> Option { + let mut candidate = action.next(); + while let Some(option) = candidate { + if option != ActionMenuOption::Proceed || self.proceed_enabled() { + return Some(option); + } + candidate = option.next(); + } + None + } + + fn normalize_highlighted_action(&mut self) { + if self.highlighted_action == ActionMenuOption::Proceed && !self.proceed_enabled() { + self.highlighted_action = self.first_available_action(); + } + } + + fn display_description(item: &ExternalAgentConfigMigrationItem) -> String { + let Some(cwd) = item.cwd.as_deref() else { + return item.description.clone(); + }; + + fn reformat_description( + description: &str, + prefix: &str, + separator: &str, + cwd: &std::path::Path, + ) -> Option { + let remainder = description.strip_prefix(prefix)?; + let (left, right) = remainder.split_once(separator)?; + Some(format!( + "{prefix}{}{}{}", + display_path_for(std::path::Path::new(left), cwd), + separator, + display_path_for(std::path::Path::new(right), cwd) + )) + } + + if let Some(reformatted) = + reformat_description(&item.description, "Migrate ", " into ", cwd) + { + return reformatted; + } + + if let Some(reformatted) = + reformat_description(&item.description, "Migrate skills from ", " to ", cwd) + { + return reformatted; + } + + if let Some(reformatted) = reformat_description(&item.description, "Migrate ", " to ", cwd) + { + return reformatted; + } + + if let Some(reformatted) = reformat_description(&item.description, "Import ", " to ", cwd) { + return reformatted; + } + + if let Some(source) = item + .description + .strip_prefix("Migrate enabled plugins from ") + { + let description = format!( + "Migrate enabled plugins from {}", + display_path_for(std::path::Path::new(source), cwd) + ); + if let Some(details) = &item.details { + let marketplace_count = details.plugins.len(); + let plugin_count = details + .plugins + .iter() + .map(|plugin_group| plugin_group.plugin_names.len()) + .sum::(); + return format!( + "{description} ({marketplace_count} {}, {plugin_count} {})", + if marketplace_count == 1 { + "marketplace" + } else { + "marketplaces" + }, + if plugin_count == 1 { + "plugin" + } else { + "plugins" + } + ); + } + return description; + } + + item.description.clone() + } + + fn new( + request_frame: FrameRequester, + items: &[ExternalAgentConfigMigrationItem], + selected_items: &[ExternalAgentConfigMigrationItem], + error: Option, + ) -> Self { + let items = items + .iter() + .cloned() + .map(|item| MigrationSelection { + enabled: selected_items.contains(&item), + item, + }) + .collect::>(); + let selected_item_idx = (!items.is_empty()).then_some(0); + Self { + request_frame, + items, + selected_item_idx, + scroll_top: 0, + focus: FocusArea::Items, + highlighted_action: ActionMenuOption::Proceed, + done: false, + outcome: ExternalAgentConfigMigrationOutcome::Skip, + error, + } + } + + fn plugin_detail_lines(plugin_groups: &[PluginsMigration]) -> Vec> { + let mut lines = plugin_groups + .iter() + .take(3) + .map(|plugin_group| { + let mut plugin_names = plugin_group + .plugin_names + .iter() + .take(2) + .cloned() + .collect::>(); + let hidden_plugin_count = plugin_group + .plugin_names + .len() + .saturating_sub(plugin_names.len()); + if hidden_plugin_count > 0 { + plugin_names.push(format!("+{hidden_plugin_count} more")); + } + Line::from(format!( + " • {}: {}", + plugin_group.marketplace_name, + plugin_names.join(", ") + )) + }) + .collect::>(); + let hidden_marketplace_count = plugin_groups.len().saturating_sub(lines.len()); + if hidden_marketplace_count > 0 { + lines.push(Line::from(format!( + " • +{hidden_marketplace_count} more marketplaces" + ))); + } + lines + } + + fn is_done(&self) -> bool { + self.done + } + + fn outcome(&self) -> ExternalAgentConfigMigrationOutcome { + self.outcome.clone() + } + + fn finish_with(&mut self, outcome: ExternalAgentConfigMigrationOutcome) { + self.outcome = outcome; + self.done = true; + self.request_frame.schedule_frame(); + } + + fn proceed(&mut self) { + let selected = self.selected_items(); + if selected.is_empty() { + self.error = Some("Select at least one item or choose a skip option.".to_string()); + self.request_frame.schedule_frame(); + return; + } + + self.finish_with(ExternalAgentConfigMigrationOutcome::Proceed(selected)); + } + + fn skip(&mut self) { + self.finish_with(ExternalAgentConfigMigrationOutcome::Skip); + } + + fn skip_forever(&mut self) { + self.finish_with(ExternalAgentConfigMigrationOutcome::SkipForever); + } + + fn exit(&mut self) { + self.finish_with(ExternalAgentConfigMigrationOutcome::Exit); + } + + fn selected_items(&self) -> Vec { + self.items + .iter() + .filter(|item| item.enabled) + .map(|item| item.item.clone()) + .collect() + } + + fn selected_count(&self) -> usize { + self.items.iter().filter(|item| item.enabled).count() + } + + fn set_all_enabled(&mut self, enabled: bool) { + for item in &mut self.items { + item.enabled = enabled; + } + self.error = None; + self.normalize_highlighted_action(); + self.request_frame.schedule_frame(); + } + + fn toggle_selected_item(&mut self) { + if self.focus != FocusArea::Items { + return; + } + let Some(selected_idx) = self.selected_item_idx else { + return; + }; + let Some(item) = self.items.get_mut(selected_idx) else { + return; + }; + + item.enabled = !item.enabled; + self.error = None; + self.normalize_highlighted_action(); + self.request_frame.schedule_frame(); + } + + fn move_up(&mut self) { + match self.focus { + FocusArea::Items => match self.selected_item_idx { + Some(0) => { + self.focus = FocusArea::Actions; + self.highlighted_action = ActionMenuOption::SkipForever; + } + Some(idx) => { + self.selected_item_idx = Some(idx.saturating_sub(1)); + } + None => { + self.focus = FocusArea::Actions; + self.highlighted_action = ActionMenuOption::SkipForever; + } + }, + FocusArea::Actions => { + if let Some(previous) = self.previous_available_action(self.highlighted_action) { + self.highlighted_action = previous; + } else { + self.focus = FocusArea::Items; + if !self.items.is_empty() { + self.selected_item_idx = Some(self.items.len() - 1); + } + } + } + } + self.ensure_selected_item_visible(); + self.request_frame.schedule_frame(); + } + + fn move_down(&mut self) { + match self.focus { + FocusArea::Items => match self.selected_item_idx { + Some(idx) if idx + 1 < self.items.len() => { + self.selected_item_idx = Some(idx + 1); + } + _ => { + self.focus = FocusArea::Actions; + self.highlighted_action = self.first_available_action(); + } + }, + FocusArea::Actions => { + if let Some(next) = self.next_available_action(self.highlighted_action) { + self.highlighted_action = next; + } else { + self.focus = FocusArea::Items; + if !self.items.is_empty() { + self.selected_item_idx = Some(0); + } + } + } + } + self.ensure_selected_item_visible(); + self.request_frame.schedule_frame(); + } + + fn confirm_selection(&mut self) { + match self.focus { + FocusArea::Items => self.toggle_selected_item(), + FocusArea::Actions => match self.highlighted_action { + ActionMenuOption::Proceed => self.proceed(), + ActionMenuOption::Skip => self.skip(), + ActionMenuOption::SkipForever => self.skip_forever(), + }, + } + } + + fn handle_key(&mut self, key_event: KeyEvent) { + if key_event.kind == KeyEventKind::Release { + return; + } + + if is_ctrl_exit_combo(key_event) { + self.exit(); + return; + } + + match key_event.code { + KeyCode::Up | KeyCode::Char('k') => self.move_up(), + KeyCode::Down | KeyCode::Char('j') => self.move_down(), + KeyCode::Char('1') => { + self.focus = FocusArea::Actions; + self.highlighted_action = ActionMenuOption::Proceed; + self.proceed(); + } + KeyCode::Char('2') => { + self.focus = FocusArea::Actions; + self.highlighted_action = ActionMenuOption::Skip; + self.skip(); + } + KeyCode::Char('3') => { + self.focus = FocusArea::Actions; + self.highlighted_action = ActionMenuOption::SkipForever; + self.skip_forever(); + } + KeyCode::Char(' ') => self.toggle_selected_item(), + KeyCode::Char('a') => self.set_all_enabled(/*enabled*/ true), + KeyCode::Char('n') => self.set_all_enabled(/*enabled*/ false), + KeyCode::Enter => self.confirm_selection(), + KeyCode::Esc => self.skip(), + _ => {} + } + } + + fn ensure_selected_item_visible(&mut self) { + let Some(selected_idx) = self.selected_item_idx else { + self.scroll_top = 0; + return; + }; + let selected_render_idx = self.selected_render_line_index(selected_idx); + let visible_rows = self.render_line_count().max(1); + if selected_render_idx < self.scroll_top { + self.scroll_top = selected_render_idx; + } else { + let bottom = self.scroll_top + visible_rows.saturating_sub(1); + if selected_render_idx > bottom { + self.scroll_top = selected_render_idx + 1 - visible_rows; + } + } + } + + fn render_line_count(&self) -> usize { + self.build_render_lines().len() + } + + fn selected_render_line_index(&self, selected_item_idx: usize) -> usize { + self.build_render_lines() + .iter() + .position(|entry| entry.item_idx == Some(selected_item_idx)) + .unwrap_or(selected_item_idx) + } + + fn section_title(cwd: Option<&std::path::Path>) -> Line<'static> { + match cwd { + Some(cwd) => Line::from(vec!["Project: ".bold(), cwd.display().to_string().dim()]), + None => Line::from("Home".bold()), + } + } + + fn build_render_lines(&self) -> Vec { + let mut lines = Vec::new(); + let mut current_scope: Option> = None; + for (idx, item) in self.items.iter().enumerate() { + let scope = item.item.cwd.as_deref(); + if current_scope != Some(scope) { + if current_scope.is_some() { + lines.push(RenderLineEntry { + item_idx: None, + kind: RenderLineKind::Section, + line: Line::from(""), + }); + } + lines.push(RenderLineEntry { + item_idx: None, + kind: RenderLineKind::Section, + line: Self::section_title(scope), + }); + current_scope = Some(scope); + } + lines.push(RenderLineEntry { + item_idx: Some(idx), + kind: RenderLineKind::Item, + line: Line::from(format!( + " [{}] {}", + if item.enabled { "x" } else { " " }, + Self::display_description(&item.item) + )), + }); + if let Some(details) = &item.item.details { + for line in Self::plugin_detail_lines(&details.plugins) { + lines.push(RenderLineEntry { + item_idx: None, + kind: RenderLineKind::ItemDetail, + line, + }); + } + } + } + lines + } + + fn render_items(&self, area: Rect, buf: &mut Buffer) { + if area.height == 0 || area.width == 0 { + return; + } + let rows = self.build_render_lines(); + let visible_rows = area.height as usize; + let mut start_idx = self.scroll_top.min(rows.len().saturating_sub(1)); + if let Some(selected_item_idx) = self.selected_item_idx { + let selected_render_idx = self.selected_render_line_index(selected_item_idx); + if selected_render_idx < start_idx { + start_idx = selected_render_idx; + } else if visible_rows > 0 { + let bottom = start_idx + visible_rows - 1; + if selected_render_idx > bottom { + start_idx = selected_render_idx + 1 - visible_rows; + } + } + } + + let mut y = area.y; + for entry in rows.iter().skip(start_idx).take(visible_rows) { + if y >= area.y + area.height { + break; + } + + let selected = + self.focus == FocusArea::Items && self.selected_item_idx == entry.item_idx; + let mut line = entry.line.clone(); + if selected { + line.spans.iter_mut().for_each(|span| { + span.style = span.style.cyan().bold(); + }); + } else if entry.kind != RenderLineKind::Item && !line.spans.is_empty() { + line.spans.iter_mut().for_each(|span| { + span.style = span.style.dim(); + }); + } + let line = truncate_line_with_ellipsis_if_overflow(line, area.width as usize); + line.render( + Rect { + x: area.x, + y, + width: area.width, + height: 1, + }, + buf, + ); + y = y.saturating_add(1); + } + } +} + +impl WidgetRef for &ExternalAgentConfigMigrationScreen { + fn render_ref(&self, area: Rect, buf: &mut Buffer) { + Clear.render(area, buf); + + let inner_area = area.inset(Insets::vh(/*v*/ 1, /*h*/ 2)); + let error_height = u16::from(self.error.is_some()); + let fixed_height = 1u16 + 2u16 + error_height + 1u16 + 4u16 + 1u16; + let list_height = + self.render_line_count() + .max(1) + .min(inner_area.height.saturating_sub(fixed_height) as usize) as u16; + let [ + header_area, + intro_area, + error_area, + list_area, + list_gap_area, + actions_area, + footer_area, + _spacer_area, + ] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(2), + Constraint::Length(error_height), + Constraint::Length(list_height), + Constraint::Length(1), + Constraint::Length(4), + Constraint::Length(1), + Constraint::Fill(1), + ]) + .areas(inner_area); + + let heading = Line::from(vec!["> ".into(), "External agent config detected".bold()]); + heading.render(header_area, buf); + + Paragraph::new(vec![ + Line::from("We found settings from another agent that you can add to this project."), + Line::from("Select what to import"), + ]) + .wrap(Wrap { trim: false }) + .render(intro_area, buf); + + if let Some(error) = &self.error { + Paragraph::new(error.clone().red().to_string()) + .wrap(Wrap { trim: false }) + .render(error_area, buf); + } + + self.render_items(list_area, buf); + Clear.render(list_gap_area, buf); + + let [ + actions_intro_area, + proceed_area, + skip_area, + skip_forever_area, + ] = Layout::vertical([ + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + ]) + .areas(actions_area); + let actions_intro = format!( + "Selected {} of {} item(s).", + self.selected_count(), + self.items.len() + ); + Paragraph::new(actions_intro) + .wrap(Wrap { trim: false }) + .render(actions_intro_area, buf); + selection_option_row_with_dim( + /*index*/ 0, + ActionMenuOption::Proceed.label().to_string(), + self.focus == FocusArea::Actions + && self.highlighted_action == ActionMenuOption::Proceed, + /*dim*/ self.focus != FocusArea::Actions || !self.proceed_enabled(), + ) + .render(proceed_area, buf); + selection_option_row_with_dim( + /*index*/ 1, + ActionMenuOption::Skip.label().to_string(), + self.focus == FocusArea::Actions && self.highlighted_action == ActionMenuOption::Skip, + /*dim*/ self.focus != FocusArea::Actions, + ) + .render(skip_area, buf); + selection_option_row_with_dim( + /*index*/ 2, + ActionMenuOption::SkipForever.label().to_string(), + self.focus == FocusArea::Actions + && self.highlighted_action == ActionMenuOption::SkipForever, + /*dim*/ self.focus != FocusArea::Actions, + ) + .render(skip_forever_area, buf); + + Line::from(vec![ + "Use ".dim(), + key_hint::plain(KeyCode::Up).into(), + "/".dim(), + key_hint::plain(KeyCode::Down).into(), + " to move, ".dim(), + key_hint::plain(KeyCode::Char(' ')).into(), + " to toggle, ".dim(), + "1".cyan(), + "/".dim(), + "2".cyan(), + "/".dim(), + "3".cyan(), + " to choose, ".dim(), + "a".cyan(), + "/".dim(), + "n".cyan(), + " for all/none".dim(), + ]) + .render(footer_area, buf); + } +} + +fn is_ctrl_exit_combo(key_event: KeyEvent) -> bool { + key_event.modifiers.contains(KeyModifiers::CONTROL) + && matches!(key_event.code, KeyCode::Char('c') | KeyCode::Char('d')) +} + +#[cfg(test)] +mod tests { + use super::ActionMenuOption; + use super::ExternalAgentConfigMigrationOutcome; + use super::ExternalAgentConfigMigrationScreen; + use super::FocusArea; + use crate::custom_terminal::Terminal; + use crate::test_backend::VT100Backend; + use crate::tui::FrameRequester; + use codex_app_server_protocol::ExternalAgentConfigMigrationItem; + use codex_app_server_protocol::ExternalAgentConfigMigrationItemType; + use codex_app_server_protocol::PluginsMigration; + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + use insta::assert_snapshot; + use pretty_assertions::assert_eq; + use ratatui::layout::Rect; + use std::path::PathBuf; + + fn sample_items() -> Vec { + vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: + "Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/config.toml" + .to_string(), + cwd: None, + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: + "Migrate enabled plugins from /workspace/project/.claude/settings.json" + .to_string(), + cwd: Some(PathBuf::from("/workspace/project")), + details: Some(codex_app_server_protocol::MigrationDetails { + plugins: vec![ + PluginsMigration { + marketplace_name: "acme-tools".to_string(), + plugin_names: vec![ + "deployer".to_string(), + "formatter".to_string(), + "lint".to_string(), + ], + }, + PluginsMigration { + marketplace_name: "team-marketplace".to_string(), + plugin_names: vec!["asana".to_string()], + }, + PluginsMigration { + marketplace_name: "debug".to_string(), + plugin_names: vec!["sample".to_string()], + }, + PluginsMigration { + marketplace_name: "data-tools".to_string(), + plugin_names: vec!["warehouse".to_string()], + }, + ], + }), + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: "Migrate /workspace/project/CLAUDE.md to /workspace/project/AGENTS.md" + .to_string(), + cwd: Some(PathBuf::from("/workspace/project")), + details: None, + }, + ] + } + + fn render_screen( + screen: &ExternalAgentConfigMigrationScreen, + width: u16, + height: u16, + ) -> String { + let backend = VT100Backend::new(width, height); + let mut terminal = Terminal::with_options(backend).expect("terminal"); + terminal.set_viewport_area(Rect::new(0, 0, width, height)); + { + let mut frame = terminal.get_frame(); + frame.render_widget_ref(screen, frame.area()); + } + terminal.flush().expect("flush"); + terminal.backend().to_string() + } + + #[test] + fn prompt_snapshot() { + let items = sample_items(); + let screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 21); + assert_snapshot!("external_agent_config_migration_prompt", rendered); + } + + #[test] + fn proceed_returns_selected_items() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(screen.is_done()); + assert_eq!( + screen.outcome(), + ExternalAgentConfigMigrationOutcome::Proceed(items) + ); + } + + #[test] + fn toggle_item_then_proceed_keeps_remaining_selection() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.handle_key(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert!(screen.is_done()); + assert_eq!( + screen.outcome(), + ExternalAgentConfigMigrationOutcome::Proceed(vec![items[1].clone(), items[2].clone(),]) + ); + } + + #[test] + fn escape_skips_prompt() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert!(screen.is_done()); + assert_eq!(screen.outcome(), ExternalAgentConfigMigrationOutcome::Skip); + } + + #[test] + fn skip_forever_returns_skip_forever_outcome() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.move_down(); + screen.move_down(); + screen.move_down(); + screen.move_down(); + screen.move_down(); + screen.confirm_selection(); + + assert_eq!( + screen.outcome(), + ExternalAgentConfigMigrationOutcome::SkipForever + ); + } + + #[test] + fn proceed_requires_at_least_one_selected_item() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)); + + assert!(!screen.is_done()); + assert_eq!(screen.highlighted_action, ActionMenuOption::Proceed); + let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 20); + assert!( + rendered.contains("Select at least one item or choose a skip option."), + "expected inline validation error, got:\n{rendered}" + ); + } + + #[test] + fn proceed_action_is_skipped_when_no_items_are_selected() { + let items = sample_items(); + let mut screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + + screen.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + screen.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)); + + assert_eq!(screen.focus, FocusArea::Actions); + assert_eq!(screen.highlighted_action, ActionMenuOption::Skip); + } + + #[test] + fn numeric_shortcuts_choose_actions() { + let items = sample_items(); + + let mut proceed_screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + proceed_screen.handle_key(KeyEvent::new(KeyCode::Char('1'), KeyModifiers::NONE)); + assert_eq!( + proceed_screen.outcome(), + ExternalAgentConfigMigrationOutcome::Proceed(items.clone()) + ); + + let mut skip_screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + skip_screen.handle_key(KeyEvent::new(KeyCode::Char('2'), KeyModifiers::NONE)); + assert_eq!( + skip_screen.outcome(), + ExternalAgentConfigMigrationOutcome::Skip + ); + + let mut skip_forever_screen = ExternalAgentConfigMigrationScreen::new( + FrameRequester::test_dummy(), + &items, + &items, + /*error*/ None, + ); + skip_forever_screen.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE)); + assert_eq!( + skip_forever_screen.outcome(), + ExternalAgentConfigMigrationOutcome::SkipForever + ); + } +} diff --git a/codex-rs/tui/src/external_agent_config_migration_startup.rs b/codex-rs/tui/src/external_agent_config_migration_startup.rs new file mode 100644 index 0000000000..93f2302535 --- /dev/null +++ b/codex-rs/tui/src/external_agent_config_migration_startup.rs @@ -0,0 +1,567 @@ +use crate::app_server_session::AppServerSession; +use crate::external_agent_config_migration::ExternalAgentConfigMigrationOutcome; +use crate::external_agent_config_migration::run_external_agent_config_migration_prompt; +use crate::legacy_core::config::Config; +use crate::legacy_core::config::ConfigBuilder; +use crate::legacy_core::config::ConfigOverrides; +use crate::legacy_core::config::edit::ConfigEdit; +use crate::legacy_core::config::edit::ConfigEditsBuilder; +use crate::tui; +use codex_app_server_protocol::ExternalAgentConfigDetectParams; +use codex_app_server_protocol::ExternalAgentConfigMigrationItem; +use codex_features::Feature; +use color_eyre::eyre::Result; +use color_eyre::eyre::WrapErr; +use std::collections::BTreeSet; +use std::path::Path; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use toml::Value as TomlValue; + +const EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS: i64 = 5 * 24 * 60 * 60; + +pub(crate) enum ExternalAgentConfigMigrationStartupOutcome { + Continue { success_message: Option }, + ExitRequested, +} + +fn should_show_external_agent_config_migration_prompt( + config: &Config, + entered_trust_nux: bool, +) -> bool { + entered_trust_nux && config.features.enabled(Feature::ExternalMigration) +} + +fn external_config_migration_project_key(path: &Path) -> String { + path.display().to_string() +} + +fn is_external_config_migration_scope_hidden(config: &Config, cwd: Option<&Path>) -> bool { + match cwd { + Some(cwd) => config + .notices + .external_config_migration_prompts + .projects + .get(&external_config_migration_project_key(cwd)) + .copied() + .unwrap_or(false), + None => config + .notices + .external_config_migration_prompts + .home + .unwrap_or(false), + } +} + +fn external_config_migration_last_prompted_at(config: &Config, cwd: Option<&Path>) -> Option { + match cwd { + Some(cwd) => config + .notices + .external_config_migration_prompts + .project_last_prompted_at + .get(&external_config_migration_project_key(cwd)) + .copied(), + None => { + config + .notices + .external_config_migration_prompts + .home_last_prompted_at + } + } +} + +fn is_external_config_migration_scope_cooling_down( + config: &Config, + cwd: Option<&Path>, + now_unix_seconds: i64, +) -> bool { + external_config_migration_last_prompted_at(config, cwd).is_some_and(|last_prompted_at| { + last_prompted_at.saturating_add(EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS) + > now_unix_seconds + }) +} + +fn visible_external_agent_config_migration_items( + config: &Config, + items: Vec, + now_unix_seconds: i64, +) -> Vec { + items + .into_iter() + .filter(|item| { + !is_external_config_migration_scope_hidden(config, item.cwd.as_deref()) + && !is_external_config_migration_scope_cooling_down( + config, + item.cwd.as_deref(), + now_unix_seconds, + ) + }) + .collect() +} + +fn external_agent_config_migration_success_message( + items: &[ExternalAgentConfigMigrationItem], +) -> String { + if items.iter().any(|item| { + item.item_type == codex_app_server_protocol::ExternalAgentConfigMigrationItemType::Plugins + }) { + "External config migration completed. Plugin migration is still in progress and may take a few minutes." + .to_string() + } else { + "External config migration completed successfully.".to_string() + } +} + +fn unix_seconds_now() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 +} + +async fn persist_external_agent_config_migration_prompt_shown( + config: &mut Config, + items: &[ExternalAgentConfigMigrationItem], + now_unix_seconds: i64, +) -> Result<()> { + let mut edits = Vec::new(); + if items.iter().any(|item| item.cwd.is_none()) { + edits.push( + ConfigEdit::SetNoticeExternalConfigMigrationPromptHomeLastPromptedAt(now_unix_seconds), + ); + } + + for project in items + .iter() + .filter_map(|item| item.cwd.as_deref()) + .map(external_config_migration_project_key) + { + edits.push( + ConfigEdit::SetNoticeExternalConfigMigrationPromptProjectLastPromptedAt( + project, + now_unix_seconds, + ), + ); + } + + if edits.is_empty() { + return Ok(()); + } + + ConfigEditsBuilder::new(&config.codex_home) + .with_edits(edits) + .apply() + .await + .map_err(|err| color_eyre::eyre::eyre!("{err}")) + .wrap_err("Failed to save external config migration prompt timestamp")?; + + if items.iter().any(|item| item.cwd.is_none()) { + config + .notices + .external_config_migration_prompts + .home_last_prompted_at = Some(now_unix_seconds); + } + for project in items + .iter() + .filter_map(|item| item.cwd.as_deref()) + .map(external_config_migration_project_key) + { + config + .notices + .external_config_migration_prompts + .project_last_prompted_at + .insert(project, now_unix_seconds); + } + + Ok(()) +} + +async fn persist_external_agent_config_migration_prompt_dismissal( + config: &mut Config, + items: &[ExternalAgentConfigMigrationItem], +) -> Result<()> { + let hide_home = items.iter().any(|item| item.cwd.is_none()); + let projects = items + .iter() + .filter_map(|item| item.cwd.as_deref()) + .map(external_config_migration_project_key) + .collect::>(); + + let mut edits = Vec::new(); + if hide_home + && !config + .notices + .external_config_migration_prompts + .home + .unwrap_or(false) + { + edits.push(ConfigEdit::SetNoticeHideExternalConfigMigrationPromptHome( + true, + )); + } + for project in &projects { + if !config + .notices + .external_config_migration_prompts + .projects + .get(project) + .copied() + .unwrap_or(false) + { + edits.push( + ConfigEdit::SetNoticeHideExternalConfigMigrationPromptProject( + project.clone(), + true, + ), + ); + } + } + + if edits.is_empty() { + return Ok(()); + } + + ConfigEditsBuilder::new(&config.codex_home) + .with_edits(edits) + .apply() + .await + .map_err(|err| color_eyre::eyre::eyre!("{err}")) + .wrap_err("Failed to save external config migration prompt preference")?; + + if hide_home { + config.notices.external_config_migration_prompts.home = Some(true); + } + for project in projects { + config + .notices + .external_config_migration_prompts + .projects + .insert(project, true); + } + + Ok(()) +} + +pub(crate) async fn handle_external_agent_config_migration_prompt_if_needed( + tui: &mut tui::Tui, + app_server: &mut AppServerSession, + config: &mut Config, + cli_kv_overrides: &[(String, TomlValue)], + harness_overrides: &ConfigOverrides, + entered_trust_nux: bool, +) -> Result { + if !should_show_external_agent_config_migration_prompt(config, entered_trust_nux) { + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: None, + }); + } + + let now_unix_seconds = unix_seconds_now(); + let detected_items = match app_server + .external_agent_config_detect(ExternalAgentConfigDetectParams { + include_home: true, + cwds: Some(vec![config.cwd.to_path_buf()]), + }) + .await + { + Ok(response) => { + visible_external_agent_config_migration_items(config, response.items, now_unix_seconds) + } + Err(err) => { + tracing::warn!( + error = %err, + cwd = %config.cwd.display(), + "failed to detect external agent config migrations; continuing startup" + ); + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: None, + }); + } + }; + + if detected_items.is_empty() { + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: None, + }); + } + + if let Err(err) = persist_external_agent_config_migration_prompt_shown( + config, + &detected_items, + now_unix_seconds, + ) + .await + { + tracing::warn!( + error = %err, + cwd = %config.cwd.display(), + "failed to persist external config migration prompt timestamp" + ); + } + + let mut selected_items = detected_items.clone(); + let mut error: Option = None; + + loop { + match run_external_agent_config_migration_prompt( + tui, + &detected_items, + &selected_items, + error.as_deref(), + ) + .await + { + ExternalAgentConfigMigrationOutcome::Proceed(items) => { + selected_items = items.clone(); + match app_server.external_agent_config_import(items).await { + Ok(_) => { + let success_message = + external_agent_config_migration_success_message(&selected_items); + *config = ConfigBuilder::default() + .codex_home(config.codex_home.to_path_buf()) + .cli_overrides(cli_kv_overrides.to_vec()) + .harness_overrides(harness_overrides.clone()) + .build() + .await + .wrap_err("Failed to reload config after external agent migration")?; + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: Some(success_message), + }); + } + Err(err) => { + tracing::warn!( + error = %err, + cwd = %config.cwd.display(), + "failed to import external agent config migration items" + ); + error = Some(format!("Migration failed: {err}")); + } + } + } + ExternalAgentConfigMigrationOutcome::Skip => { + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: None, + }); + } + ExternalAgentConfigMigrationOutcome::SkipForever => { + match persist_external_agent_config_migration_prompt_dismissal( + config, + &detected_items, + ) + .await + { + Ok(()) => { + return Ok(ExternalAgentConfigMigrationStartupOutcome::Continue { + success_message: None, + }); + } + Err(err) => { + tracing::warn!( + error = %err, + cwd = %config.cwd.display(), + "failed to persist external config migration prompt dismissal" + ); + error = Some(format!("Failed to save preference: {err}")); + } + } + } + ExternalAgentConfigMigrationOutcome::Exit => { + return Ok(ExternalAgentConfigMigrationStartupOutcome::ExitRequested); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use codex_app_server_protocol::ExternalAgentConfigMigrationItemType; + use pretty_assertions::assert_eq; + use std::path::PathBuf; + use tempfile::tempdir; + + #[tokio::test] + async fn visible_external_agent_config_migration_items_omits_hidden_scopes() { + let codex_home = tempdir().expect("temp codex home"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config"); + config.notices.external_config_migration_prompts.home = Some(true); + config + .notices + .external_config_migration_prompts + .projects + .insert("/tmp/project".to_string(), true); + + let visible = visible_external_agent_config_migration_items( + &config, + vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: "home".to_string(), + cwd: None, + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: "project".to_string(), + cwd: Some(PathBuf::from("/tmp/project")), + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: "other project".to_string(), + cwd: Some(PathBuf::from("/tmp/other")), + details: None, + }, + ], + /*now_unix_seconds*/ 1_760_000_000, + ); + + assert_eq!( + visible, + vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: "other project".to_string(), + cwd: Some(PathBuf::from("/tmp/other")), + details: None, + }] + ); + } + + #[tokio::test] + async fn visible_external_agent_config_migration_items_omits_recently_prompted_scopes() { + let codex_home = tempdir().expect("temp codex home"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config"); + config + .notices + .external_config_migration_prompts + .home_last_prompted_at = Some(1_760_000_000); + config + .notices + .external_config_migration_prompts + .project_last_prompted_at + .insert("/tmp/project".to_string(), 1_760_000_000); + + let visible = visible_external_agent_config_migration_items( + &config, + vec![ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: "home".to_string(), + cwd: None, + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: "project".to_string(), + cwd: Some(PathBuf::from("/tmp/project")), + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: "other project".to_string(), + cwd: Some(PathBuf::from("/tmp/other")), + details: None, + }, + ], + /*now_unix_seconds*/ + 1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS - 1, + ); + + assert_eq!( + visible, + vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Skills, + description: "other project".to_string(), + cwd: Some(PathBuf::from("/tmp/other")), + details: None, + }] + ); + } + + #[tokio::test] + async fn external_config_migration_scope_cooldown_expires_after_five_days() { + let codex_home = tempdir().expect("temp codex home"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config"); + config + .notices + .external_config_migration_prompts + .home_last_prompted_at = Some(1_760_000_000); + + assert!(is_external_config_migration_scope_cooling_down( + &config, + /*cwd*/ None, + 1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS - 1, + )); + assert!(!is_external_config_migration_scope_cooling_down( + &config, + /*cwd*/ None, + 1_760_000_000 + EXTERNAL_CONFIG_MIGRATION_PROMPT_COOLDOWN_SECS, + )); + } + + #[test] + fn external_agent_config_migration_success_message_mentions_plugins_when_present() { + let message = external_agent_config_migration_success_message(&[ + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Config, + description: String::new(), + cwd: None, + details: None, + }, + ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: String::new(), + cwd: None, + details: None, + }, + ]); + + assert_eq!( + message, + "External config migration completed. Plugin migration is still in progress and may take a few minutes." + ); + } + + #[test] + fn external_agent_config_migration_success_message_omits_plugins_copy_when_absent() { + let message = + external_agent_config_migration_success_message(&[ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::AgentsMd, + description: String::new(), + cwd: None, + details: None, + }]); + + assert_eq!(message, "External config migration completed successfully."); + } + + #[tokio::test] + async fn external_agent_config_migration_prompt_requires_trust_nux_entry() { + let codex_home = tempdir().expect("temp codex home"); + let mut config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .build() + .await + .expect("config"); + let _ = config.features.enable(Feature::ExternalMigration); + + assert!(!should_show_external_agent_config_migration_prompt( + &config, /*entered_trust_nux*/ false, + )); + assert!(should_show_external_agent_config_migration_prompt( + &config, /*entered_trust_nux*/ true, + )); + } +} diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b67dd419f9..c48e49b56d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -118,6 +118,8 @@ mod debug_config; mod diff_render; mod exec_cell; mod exec_command; +mod external_agent_config_migration; +mod external_agent_config_migration_startup; mod external_editor; mod file_search; mod frames; @@ -1452,6 +1454,7 @@ async fn run_ratatui_app( session_selection, feedback, should_show_trust_screen, // Proxy to: is it a first run in this directory? + should_show_trust_screen_flag, // Preserve the startup-time trust NUX signal before onboarding should_prompt_windows_sandbox_nux_at_startup, remote_url, remote_auth_token, diff --git a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__trust_directory__tests__renders_snapshot_for_git_repo.snap b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__trust_directory__tests__renders_snapshot_for_git_repo.snap index 3c8c248c36..0ffd8d35ca 100644 --- a/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__trust_directory__tests__renders_snapshot_for_git_repo.snap +++ b/codex-rs/tui/src/onboarding/snapshots/codex_tui__onboarding__trust_directory__tests__renders_snapshot_for_git_repo.snap @@ -5,7 +5,9 @@ expression: terminal.backend() > You are in /workspace/project Do you trust the contents of this directory? Working with untrusted - contents comes with higher risk of prompt injection. + contents comes with higher risk of prompt injection. Trusting the + directory allows project-local config, hooks, and exec policies to + load. › 1. Yes, continue 2. No, quit diff --git a/codex-rs/tui/src/onboarding/trust_directory.rs b/codex-rs/tui/src/onboarding/trust_directory.rs index e9f2cd58bd..f04e9f9af9 100644 --- a/codex-rs/tui/src/onboarding/trust_directory.rs +++ b/codex-rs/tui/src/onboarding/trust_directory.rs @@ -53,10 +53,15 @@ impl WidgetRef for &TrustDirectoryWidget { column.push( Paragraph::new( - "Do you trust the contents of this directory? Working with untrusted contents comes with higher risk of prompt injection.".to_string(), + "Do you trust the contents of this directory? Working with untrusted \ + contents comes with higher risk of prompt injection. Trusting the \ + directory allows project-local config, hooks, and exec policies to load." + .to_string(), ) - .wrap(Wrap { trim: true }) - .inset(Insets::tlbr(/*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0)), + .wrap(Wrap { trim: true }) + .inset(Insets::tlbr( + /*top*/ 0, /*left*/ 2, /*bottom*/ 0, /*right*/ 0, + )), ); column.push(""); diff --git a/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap new file mode 100644 index 0000000000..3f518141c8 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__external_agent_config_migration__tests__external_agent_config_migration_prompt.snap @@ -0,0 +1,25 @@ +--- +source: tui/src/external_agent_config_migration.rs +assertion_line: 824 +expression: rendered +--- + + > External agent config detected + We found settings from another agent that you can add to this project. + Select what to import + Home + [x] Migrate /Users/alex/.claude/settings.json into /Users/alex/.codex/con… + + Project: /workspace/project + [x] Migrate enabled plugins from /workspace/project/.claude/settings.json… + • acme-tools: deployer, formatter, +1 more + • team-marketplace: asana + • debug: sample + • +1 more marketplaces + [x] Migrate /workspace/project/CLAUDE.md to /workspace/project/AGENTS.md + + Selected 3 of 3 item(s). + 1. Proceed with selected + 2. Skip for now + 3. Don't ask again + Use ↑/↓ to move, space to toggle, 1/2/3 to choose, a/n for all/none diff --git a/codex-rs/utils/absolute-path/src/absolutize.rs b/codex-rs/utils/absolute-path/src/absolutize.rs index 7965c81f7b..4c1842ada7 100644 --- a/codex-rs/utils/absolute-path/src/absolutize.rs +++ b/codex-rs/utils/absolute-path/src/absolutize.rs @@ -12,6 +12,10 @@ use std::path::Path; use std::path::PathBuf; pub(super) fn absolutize(path: &Path) -> std::io::Result { + if path.is_absolute() { + return Ok(normalize_path(path)); + } + Ok(absolutize_from(path, &std::env::current_dir()?)) } diff --git a/codex-rs/utils/absolute-path/src/lib.rs b/codex-rs/utils/absolute-path/src/lib.rs index 200dfd3705..86161d23f0 100644 --- a/codex-rs/utils/absolute-path/src/lib.rs +++ b/codex-rs/utils/absolute-path/src/lib.rs @@ -328,6 +328,8 @@ mod tests { use crate::test_support::test_path_buf; use pretty_assertions::assert_eq; use std::fs; + #[cfg(unix)] + use std::process::Command; use tempfile::tempdir; #[test] @@ -341,6 +343,46 @@ mod tests { assert_eq!(abs_path_buf.as_path(), absolute_path.as_path()); } + #[cfg(unix)] + #[test] + fn from_absolute_path_does_not_read_current_dir_when_path_is_absolute() { + let status = Command::new(std::env::current_exe().expect("current test binary")) + .arg("from_absolute_path_with_removed_current_dir_child") + .arg("--ignored") + .env("CODEX_ABSOLUTE_PATH_REMOVED_CWD_CHILD", "1") + .status() + .expect("run child test"); + + assert!(status.success()); + } + + #[cfg(unix)] + #[test] + #[ignore] + fn from_absolute_path_with_removed_current_dir_child() { + if std::env::var_os("CODEX_ABSOLUTE_PATH_REMOVED_CWD_CHILD").is_none() { + return; + } + + let original_cwd = std::env::current_dir().expect("original cwd"); + let temp_dir = tempdir().expect("temp dir"); + let removed_cwd = temp_dir.path().to_path_buf(); + std::env::set_current_dir(&removed_cwd).expect("enter temp dir"); + std::fs::remove_dir(&removed_cwd).expect("remove current dir"); + std::env::current_dir().expect_err("current dir should be unavailable"); + + let path = AbsolutePathBuf::from_absolute_path(test_path_buf( + "/tmp/codex/../codex-home/plugins/cache", + )) + .expect("absolute path should not require current dir"); + + std::env::set_current_dir(original_cwd).expect("restore cwd"); + assert_eq!( + path.as_path(), + test_path_buf("/tmp/codex-home/plugins/cache") + ); + } + #[test] fn from_absolute_path_checked_rejects_relative_path() { let err = AbsolutePathBuf::from_absolute_path_checked("relative/path") diff --git a/codex-rs/utils/cli/src/format_env_display.rs b/codex-rs/utils/cli/src/format_env_display.rs index dda0cad099..cee63bb32b 100644 --- a/codex-rs/utils/cli/src/format_env_display.rs +++ b/codex-rs/utils/cli/src/format_env_display.rs @@ -1,6 +1,9 @@ use std::collections::HashMap; -pub fn format_env_display(env: Option<&HashMap>, env_vars: &[String]) -> String { +pub fn format_env_display>( + env: Option<&HashMap>, + env_vars: &[S], +) -> String { let mut parts: Vec = Vec::new(); if let Some(map) = env { @@ -10,7 +13,7 @@ pub fn format_env_display(env: Option<&HashMap>, env_vars: &[Str } if !env_vars.is_empty() { - parts.extend(env_vars.iter().map(|var| format!("{var}=*****"))); + parts.extend(env_vars.iter().map(|var| format!("{}=*****", var.as_ref()))); } if parts.is_empty() { @@ -26,10 +29,11 @@ mod tests { #[test] fn returns_dash_when_empty() { - assert_eq!(format_env_display(/*env*/ None, &[]), "-"); + let empty_vars: &[String] = &[]; + assert_eq!(format_env_display(/*env*/ None, empty_vars), "-"); let empty_map = HashMap::new(); - assert_eq!(format_env_display(Some(&empty_map), &[]), "-"); + assert_eq!(format_env_display(Some(&empty_map), empty_vars), "-"); } #[test] @@ -38,7 +42,10 @@ mod tests { env.insert("B".to_string(), "two".to_string()); env.insert("A".to_string(), "one".to_string()); - assert_eq!(format_env_display(Some(&env), &[]), "A=*****, B=*****"); + assert_eq!( + format_env_display(Some(&env), &[] as &[String]), + "A=*****, B=*****" + ); } #[test] diff --git a/codex-rs/utils/image/src/lib.rs b/codex-rs/utils/image/src/lib.rs index 0c0a026e41..a3f595a183 100644 --- a/codex-rs/utils/image/src/lib.rs +++ b/codex-rs/utils/image/src/lib.rs @@ -15,10 +15,8 @@ use image::codecs::jpeg::JpegEncoder; use image::codecs::png::PngEncoder; use image::codecs::webp::WebPEncoder; use image::imageops::FilterType; -/// Maximum width used when resizing images before uploading. -pub const MAX_WIDTH: u32 = 2048; -/// Maximum height used when resizing images before uploading. -pub const MAX_HEIGHT: u32 = 768; +/// Maximum width or height used when resizing images before uploading. +pub const MAX_DIMENSION: u32 = 2048; pub mod error; @@ -80,40 +78,41 @@ pub fn load_for_prompt_bytes( let (width, height) = dynamic.dimensions(); - let encoded = - if mode == PromptImageMode::Original || (width <= MAX_WIDTH && height <= MAX_HEIGHT) { - if let Some(format) = format.filter(|format| can_preserve_source_bytes(*format)) { - let mime = format_to_mime(format); - EncodedImage { - bytes: file_bytes, - mime, - width, - height, - } - } else { - let (bytes, output_format) = encode_image(&dynamic, ImageFormat::Png)?; - let mime = format_to_mime(output_format); - EncodedImage { - bytes, - mime, - width, - height, - } + let encoded = if mode == PromptImageMode::Original + || (width <= MAX_DIMENSION && height <= MAX_DIMENSION) + { + if let Some(format) = format.filter(|format| can_preserve_source_bytes(*format)) { + let mime = format_to_mime(format); + EncodedImage { + bytes: file_bytes, + mime, + width, + height, } } else { - let resized = dynamic.resize(MAX_WIDTH, MAX_HEIGHT, FilterType::Triangle); - let target_format = format - .filter(|format| can_preserve_source_bytes(*format)) - .unwrap_or(ImageFormat::Png); - let (bytes, output_format) = encode_image(&resized, target_format)?; + let (bytes, output_format) = encode_image(&dynamic, ImageFormat::Png)?; let mime = format_to_mime(output_format); EncodedImage { bytes, mime, - width: resized.width(), - height: resized.height(), + width, + height, } - }; + } + } else { + let resized = dynamic.resize(MAX_DIMENSION, MAX_DIMENSION, FilterType::Triangle); + let target_format = format + .filter(|format| can_preserve_source_bytes(*format)) + .unwrap_or(ImageFormat::Png); + let (bytes, output_format) = encode_image(&resized, target_format)?; + let mime = format_to_mime(output_format); + EncodedImage { + bytes, + mime, + width: resized.width(), + height: resized.height(), + } + }; Ok(encoded) }) @@ -251,8 +250,8 @@ mod tests { ) .expect("process image"); - assert!(processed.width <= MAX_WIDTH); - assert!(processed.height <= MAX_HEIGHT); + assert!(processed.width <= MAX_DIMENSION); + assert!(processed.height <= MAX_DIMENSION); assert_eq!(processed.mime, mime); let detected_format = @@ -265,6 +264,23 @@ mod tests { } } + #[tokio::test(flavor = "multi_thread")] + async fn downscales_tall_image_to_fit_square_bounds() { + let image = ImageBuffer::from_pixel(1024, 4096, Rgba([200u8, 10, 10, 255])); + let original_bytes = image_bytes(&image, ImageFormat::Png); + + let processed = load_for_prompt_bytes( + Path::new("in-memory-image"), + original_bytes, + PromptImageMode::ResizeToFit, + ) + .expect("process image"); + + assert_eq!(processed.width, 512); + assert_eq!(processed.height, MAX_DIMENSION); + assert_eq!(processed.mime, "image/png"); + } + #[tokio::test(flavor = "multi_thread")] async fn preserves_large_image_in_original_mode() { let image = ImageBuffer::from_pixel(4096, 2048, Rgba([180u8, 30, 30, 255])); diff --git a/codex-rs/utils/output-truncation/src/truncate_tests.rs b/codex-rs/utils/output-truncation/src/truncate_tests.rs index f159a6b62f..74acb15ca3 100644 --- a/codex-rs/utils/output-truncation/src/truncate_tests.rs +++ b/codex-rs/utils/output-truncation/src/truncate_tests.rs @@ -5,6 +5,7 @@ use crate::formatted_truncate_text; use crate::formatted_truncate_text_content_items_with_policy; use crate::truncate_function_output_items_with_policy; use crate::truncate_text; +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputContentItem; use pretty_assertions::assert_eq; @@ -114,7 +115,7 @@ fn truncates_across_multiple_under_limit_texts_and_reports_omitted() { FunctionCallOutputContentItem::InputText { text: t2.clone() }, FunctionCallOutputContentItem::InputImage { image_url: "img:mid".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: t3 }, FunctionCallOutputContentItem::InputText { text: t4 }, @@ -142,7 +143,7 @@ fn truncates_across_multiple_under_limit_texts_and_reports_omitted() { output[2], FunctionCallOutputContentItem::InputImage { image_url: "img:mid".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), } ); @@ -214,7 +215,7 @@ fn formatted_truncate_text_content_items_with_policy_merges_text_and_appends_ima }, FunctionCallOutputContentItem::InputImage { image_url: "img:one".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputText { text: "efgh".to_string(), @@ -224,7 +225,7 @@ fn formatted_truncate_text_content_items_with_policy_merges_text_and_appends_ima }, FunctionCallOutputContentItem::InputImage { image_url: "img:two".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ]; @@ -239,11 +240,11 @@ fn formatted_truncate_text_content_items_with_policy_merges_text_and_appends_ima }, FunctionCallOutputContentItem::InputImage { image_url: "img:one".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, FunctionCallOutputContentItem::InputImage { image_url: "img:two".to_string(), - detail: None, + detail: Some(DEFAULT_IMAGE_DETAIL), }, ] ); diff --git a/defs.bzl b/defs.bzl index 6972b65a65..53114a5773 100644 --- a/defs.bzl +++ b/defs.bzl @@ -140,6 +140,7 @@ def codex_rust_crate( integration_test_args = [], integration_test_timeout = None, test_data_extra = [], + test_shard_counts = {}, test_tags = [], unit_test_timeout = None, extra_binaries = []): @@ -174,6 +175,11 @@ def codex_rust_crate( integration_test_timeout: Optional Bazel timeout for integration test targets generated from `tests/*.rs`. test_data_extra: Extra runtime data for tests. + test_shard_counts: Mapping from generated test target name to Bazel + shard count. Matching tests use native Bazel sharding on the + original test label, while rules_rust assigns each Rust test case + to a stable bucket by hashing the test name. Matching tests are + also marked flaky, which gives them Bazel's default three attempts. test_tags: Tags applied to unit + integration test targets. Typically used to disable the sandbox, but see https://bazel.build/reference/be/common-definitions#common.tags unit_test_timeout: Optional Bazel timeout for the unit-test target @@ -246,7 +252,13 @@ def codex_rust_crate( visibility = ["//visibility:public"], ) + unit_test_name = name + "-unit-tests" unit_test_binary = name + "-unit-tests-bin" + unit_test_shard_count = _test_shard_count(test_shard_counts, unit_test_name) + unit_test_binary_kwargs = {} + if unit_test_shard_count: + unit_test_binary_kwargs["experimental_enable_sharding"] = True + rust_test( name = unit_test_binary, crate = name, @@ -265,14 +277,18 @@ def codex_rust_crate( rustc_env = rustc_env, data = test_data_extra, tags = test_tags + ["manual"], + **unit_test_binary_kwargs ) unit_test_kwargs = {} if unit_test_timeout: unit_test_kwargs["timeout"] = unit_test_timeout + if unit_test_shard_count: + unit_test_kwargs["shard_count"] = unit_test_shard_count + unit_test_kwargs["flaky"] = True workspace_root_test( - name = name + "-unit-tests", + name = unit_test_name, env = test_env, test_bin = ":" + unit_test_binary, workspace_root_marker = "//codex-rs/utils/cargo-bin:repo_root.marker", @@ -318,6 +334,14 @@ def codex_rust_crate( if not test_name.endswith("-test"): test_name += "-test" + test_kwargs = {} + test_kwargs.update(integration_test_kwargs) + test_shard_count = _test_shard_count(test_shard_counts, test_name) + if test_shard_count: + test_kwargs["experimental_enable_sharding"] = True + test_kwargs["shard_count"] = test_shard_count + test_kwargs["flaky"] = True + rust_test( name = test_name, crate_name = test_crate_name, @@ -339,5 +363,15 @@ def codex_rust_crate( # execute from the repo root and can misplace integration snapshots. env = cargo_env, tags = test_tags, - **integration_test_kwargs + **test_kwargs ) + +def _test_shard_count(test_shard_counts, test_name): + shard_count = test_shard_counts.get(test_name) + if shard_count == None: + return None + + if shard_count < 1: + fail("test_shard_counts[{}] must be a positive integer".format(test_name)) + + return shard_count diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel index 0924842e48..b58292c727 100644 --- a/patches/BUILD.bazel +++ b/patches/BUILD.bazel @@ -10,7 +10,6 @@ exports_files([ "rules_rust_windows_exec_bin_target.patch", "rules_rust_windows_exec_std.patch", "rules_rust_windows_process_wrapper_skip_temp_outputs.patch", - "rules_rust_repository_set_exec_constraints.patch", "rules_rust_windows_msvc_direct_link_args.patch", "rules_rust_windows_gnullvm_build_script.patch", "rules_rs_windows_gnullvm_exec.patch", diff --git a/patches/rules_rs_delete_git_worktree_pointer.patch b/patches/rules_rs_delete_git_worktree_pointer.patch deleted file mode 100644 index fb453dbe79..0000000000 --- a/patches/rules_rs_delete_git_worktree_pointer.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/rs/private/crate_git_repository.bzl b/rs/private/crate_git_repository.bzl ---- a/rs/private/crate_git_repository.bzl -+++ b/rs/private/crate_git_repository.bzl -@@ -35,6 +35,14 @@ - "HEAD" - ]) - if result.return_code != 0: - fail(result.stderr) - -+ # Remove .git worktree pointer file. It contains an absolute path to -+ # the bazel output base which is machine-specific and non-deterministic. -+ # Leaving it in pollutes compile_data globs and causes AC misses. -+ # -+ # Note that bazelbuild/rules_rust ignores .git (among other paths) during splicing: -+ # https://github.com/bazelbuild/rules_rust/blob/ca4915c0210bcd240152a5333ecb24d266bda144/crate_universe/src/splicing/splicer.rs#L42 -+ rctx.delete(root.get_child(".git")) -+ - if strip_prefix: - dest_link = dest_dir.get_child(strip_prefix) - if not dest_link.exists: diff --git a/patches/rules_rs_windows_exec_linker.patch b/patches/rules_rs_windows_exec_linker.patch index 25892de836..f7110735d0 100644 --- a/patches/rules_rs_windows_exec_linker.patch +++ b/patches/rules_rs_windows_exec_linker.patch @@ -3,10 +3,9 @@ # Scope: Windows-only linker metadata for the generated `rules_rs` toolchains. diff --git a/rs/experimental/toolchains/declare_rustc_toolchains.bzl b/rs/experimental/toolchains/declare_rustc_toolchains.bzl -index 67e491c..3f1cff5 100644 --- a/rs/experimental/toolchains/declare_rustc_toolchains.bzl +++ b/rs/experimental/toolchains/declare_rustc_toolchains.bzl -@@ -50,6 +50,8 @@ def declare_rustc_toolchains( +@@ -58,6 +58,8 @@ def declare_rustc_toolchains( rust_toolchain( name = rust_toolchain_name, rust_doc = "{}rustdoc".format(rustc_repo_label), @@ -15,10 +14,10 @@ index 67e491c..3f1cff5 100644 rust_std = select(rust_std_select), rustc = "{}rustc".format(rustc_repo_label), cargo = "{}cargo".format(cargo_repo_label), -@@ -82,7 +84,20 @@ def declare_rustc_toolchains( - stdlib_linkflags = select({ - "@platforms//os:freebsd": ["-lexecinfo", "-lpthread"], - "@platforms//os:macos": ["-lSystem", "-lresolv"], +@@ -104,7 +106,20 @@ def declare_rustc_toolchains( + "@platforms//os:nixos": ["-ldl", "-lpthread"], + "@platforms//os:openbsd": ["-lpthread"], + "@platforms//os:ios": ["-lSystem", "-lobjc", "-Wl,-framework,Security", "-Wl,-framework,Foundation", "-lresolv"], - # TODO: windows + "@rules_rs//rs/experimental/platforms/constraints:windows_gnullvm": [ + "advapi32.lib", @@ -38,17 +37,12 @@ index 67e491c..3f1cff5 100644 }), default_edition = edition, diff --git a/rs/private/rustc_repository.bzl b/rs/private/rustc_repository.bzl -index f4f0286..6558bb2 100644 --- a/rs/private/rustc_repository.bzl +++ b/rs/private/rustc_repository.bzl -@@ -1,13 +1,28 @@ - load("@rules_rust//rust/platform:triple.bzl", "triple") - load( - "@rules_rust//rust/private:repository_utils.bzl", - "BUILD_for_compiler", +@@ -7,10 +7,24 @@ load( ) - load(":rust_repository_utils.bzl", "download_and_extract", "RUST_REPOSITORY_COMMON_ATTR") - + load(":rust_repository_utils.bzl", "RUST_REPOSITORY_COMMON_ATTR", "download_and_extract") ++ +_WINDOWS_EXEC_LINKER_BUILD = """ +filegroup( + name = "rust-lld", @@ -56,18 +50,17 @@ index f4f0286..6558bb2 100644 + visibility = ["//visibility:public"], +) +""" -+ + def _rustc_repository_impl(rctx): exec_triple = triple(rctx.attr.triple) download_and_extract(rctx, "rustc", "rustc", exec_triple) -- rctx.file("BUILD.bazel", BUILD_for_compiler(exec_triple)) -+ build_file = BUILD_for_compiler(exec_triple) + build_content = [BUILD_for_compiler(exec_triple)] + if exec_triple.system == "windows": + lld_link = rctx.which("lld-link.exe") + if lld_link == None: + fail("lld-link.exe not found on PATH") + rctx.symlink(lld_link, "bin/lld-link.exe") -+ build_file += _WINDOWS_EXEC_LINKER_BUILD -+ rctx.file("BUILD.bazel", build_file) - - return rctx.repo_metadata(reproducible = True) ++ build_content.append(_WINDOWS_EXEC_LINKER_BUILD) + if includes_rust_analyzer_proc_macro_srv(rctx.attr.version, rctx.attr.iso_date): + build_content.append(BUILD_for_rust_analyzer_proc_macro_srv(exec_triple)) + rctx.file("BUILD.bazel", "\n".join(build_content)) diff --git a/patches/rules_rs_windows_gnullvm_exec.patch b/patches/rules_rs_windows_gnullvm_exec.patch index 9a49523b78..f806113e56 100644 --- a/patches/rules_rs_windows_gnullvm_exec.patch +++ b/patches/rules_rs_windows_gnullvm_exec.patch @@ -2,7 +2,6 @@ # Scope: experimental platform/toolchain naming only; no Cargo target changes. diff --git a/rs/experimental/platforms/triples.bzl b/rs/experimental/platforms/triples.bzl -index 3ca3bb1..dd15656 100644 --- a/rs/experimental/platforms/triples.bzl +++ b/rs/experimental/platforms/triples.bzl @@ -30,7 +30,9 @@ SUPPORTED_EXEC_TRIPLES = [ @@ -16,22 +15,21 @@ index 3ca3bb1..dd15656 100644 "aarch64-apple-darwin", ] diff --git a/rs/experimental/toolchains/declare_rustc_toolchains.bzl b/rs/experimental/toolchains/declare_rustc_toolchains.bzl -index b9a0ce1..67e491c 100644 --- a/rs/experimental/toolchains/declare_rustc_toolchains.bzl +++ b/rs/experimental/toolchains/declare_rustc_toolchains.bzl -@@ -10,6 +10,11 @@ def _channel(version): +@@ -9,6 +9,11 @@ def _channel(version): + if version.startswith("beta"): return "beta" return "stable" - ++ +def _exec_triple_suffix(exec_triple): + if exec_triple.system == "windows": + return "{}_{}_{}".format(exec_triple.system, exec_triple.arch, exec_triple.abi) + return "{}_{}".format(exec_triple.system, exec_triple.arch) -+ - def declare_rustc_toolchains( - *, - version, -@@ -23,15 +28,14 @@ def declare_rustc_toolchains( + + def _rustc_flags_to_select(rustc_flags_by_triple): + return select( +@@ -31,15 +36,14 @@ def declare_rustc_toolchains( for triple in execs: exec_triple = _parse_triple(triple) @@ -50,7 +48,7 @@ index b9a0ce1..67e491c 100644 version_key, ) -@@ -90,11 +94,8 @@ def declare_rustc_toolchains( +@@ -116,11 +120,8 @@ def declare_rustc_toolchains( target_key = sanitize_triple(target_triple) native.toolchain( @@ -65,24 +63,22 @@ index b9a0ce1..67e491c 100644 target_settings = [ "@rules_rust//rust/toolchain/channel:" + channel, diff --git a/rs/experimental/toolchains/declare_rustfmt_toolchains.bzl b/rs/experimental/toolchains/declare_rustfmt_toolchains.bzl -index a219209..ecb6b05 100644 --- a/rs/experimental/toolchains/declare_rustfmt_toolchains.bzl +++ b/rs/experimental/toolchains/declare_rustfmt_toolchains.bzl -@@ -1,8 +1,13 @@ +@@ -1,7 +1,12 @@ load("@rules_rust//rust:toolchain.bzl", "rustfmt_toolchain") load("@rules_rust//rust/platform:triple.bzl", _parse_triple = "triple") -load("//rs/experimental/platforms:triples.bzl", "SUPPORTED_EXEC_TRIPLES") +load("//rs/experimental/platforms:triples.bzl", "SUPPORTED_EXEC_TRIPLES", "triple_to_constraint_set") load("//rs/experimental/toolchains:toolchain_utils.bzl", "sanitize_version") - ++ +def _exec_triple_suffix(exec_triple): + if exec_triple.system == "windows": + return "{}_{}_{}".format(exec_triple.system, exec_triple.arch, exec_triple.abi) + return "{}_{}".format(exec_triple.system, exec_triple.arch) -+ + def _channel(version): if version.startswith("nightly"): - return "nightly" @@ -22,14 +27,13 @@ def declare_rustfmt_toolchains( for triple in execs: @@ -111,15 +107,71 @@ index a219209..ecb6b05 100644 - "@platforms//cpu:" + exec_triple.arch, - ], + name = "{}_rustfmt_{}".format(triple_suffix, version_key), ++ exec_compatible_with = triple_to_constraint_set(triple), + target_compatible_with = [], + target_settings = [ + "@rules_rust//rust/toolchain/channel:" + channel, +diff --git a/rs/experimental/toolchains/declare_rust_analyzer_toolchains.bzl b/rs/experimental/toolchains/declare_rust_analyzer_toolchains.bzl +--- a/rs/experimental/toolchains/declare_rust_analyzer_toolchains.bzl ++++ b/rs/experimental/toolchains/declare_rust_analyzer_toolchains.bzl +@@ -4,7 +4,7 @@ load( + "@rules_rust//rust/private:repository_utils.bzl", + "includes_rust_analyzer_proc_macro_srv", + ) +-load("//rs/experimental/platforms:triples.bzl", "SUPPORTED_EXEC_TRIPLES") ++load("//rs/experimental/platforms:triples.bzl", "SUPPORTED_EXEC_TRIPLES", "triple_to_constraint_set") + load("//rs/experimental/toolchains:toolchain_utils.bzl", "sanitize_version") + + def _channel(version): +@@ -13,6 +13,11 @@ def _channel(version): + if version.startswith("beta"): + return "beta" + return "stable" ++ ++def _exec_triple_suffix(exec_triple): ++ if exec_triple.system == "windows": ++ return "{}_{}_{}".format(exec_triple.system, exec_triple.arch, exec_triple.abi) ++ return "{}_{}".format(exec_triple.system, exec_triple.arch) + + def _parse_version(version): + if "/" in version: +@@ -31,15 +36,14 @@ def declare_rust_analyzer_toolchains( + + for triple in execs: + exec_triple = _parse_triple(triple) +- triple_suffix = exec_triple.system + "_" + exec_triple.arch ++ triple_suffix = _exec_triple_suffix(exec_triple) + + rustc_repo_label = "@rustc_{}_{}//:".format(triple_suffix, rust_analyzer_version_key) + rust_analyzer_repo_label = "@rust_analyzer_{}_{}//:".format(triple_suffix, rust_analyzer_version_key) + rust_src_repo_label = "@rust_src_{}//lib/rustlib/src:rustc_srcs".format(rust_analyzer_version_key) + +- rust_analyzer_toolchain_name = "{}_{}_{}_rust_analyzer_toolchain".format( +- exec_triple.system, +- exec_triple.arch, ++ rust_analyzer_toolchain_name = "{}_{}_rust_analyzer_toolchain".format( ++ triple_suffix, + version_key, + ) + +@@ -57,11 +61,8 @@ def declare_rust_analyzer_toolchains( + rust_analyzer_toolchain(**rust_analyzer_toolchain_kwargs) + + native.toolchain( +- name = "{}_{}_rust_analyzer_{}".format(exec_triple.system, exec_triple.arch, version_key), +- exec_compatible_with = [ +- "@platforms//os:" + exec_triple.system, +- "@platforms//cpu:" + exec_triple.arch, +- ], ++ name = "{}_rust_analyzer_{}".format(triple_suffix, version_key), + exec_compatible_with = triple_to_constraint_set(triple), target_compatible_with = [], target_settings = [ "@rules_rust//rust/toolchain/channel:" + channel, diff --git a/rs/experimental/toolchains/module_extension.bzl b/rs/experimental/toolchains/module_extension.bzl -index 7bb0205..ace556b 100644 --- a/rs/experimental/toolchains/module_extension.bzl +++ b/rs/experimental/toolchains/module_extension.bzl -@@ -37,6 +37,11 @@ def _normalize_arch_name(arch): +@@ -39,6 +39,11 @@ def _normalize_arch_name(arch): return "aarch64" return arch @@ -131,7 +183,7 @@ index 7bb0205..ace556b 100644 def _sanitize_path_fragment(path): return path.replace("/", "_").replace(":", "_") -@@ -181,7 +186,7 @@ def _toolchains_impl(mctx): +@@ -209,7 +214,7 @@ def _toolchains_impl(mctx): for triple in SUPPORTED_EXEC_TRIPLES: exec_triple = _parse_triple(triple) @@ -140,7 +192,7 @@ index 7bb0205..ace556b 100644 rustc_name = "rustc_{}_{}".format(triple_suffix, version_key) rustc_repository( -@@ -230,7 +235,7 @@ def _toolchains_impl(mctx): +@@ -258,7 +263,7 @@ def _toolchains_impl(mctx): for triple in SUPPORTED_EXEC_TRIPLES: exec_triple = _parse_triple(triple) @@ -149,3 +201,12 @@ index 7bb0205..ace556b 100644 rustfmt_repository( name = "rustfmt_{}_{}".format(triple_suffix, version_key), +@@ -282,7 +287,7 @@ def _toolchains_impl(mctx): + + for triple in SUPPORTED_EXEC_TRIPLES: + exec_triple = _parse_triple(triple) +- triple_suffix = exec_triple.system + "_" + exec_triple.arch ++ triple_suffix = _exec_triple_suffix(exec_triple) + + rust_analyzer_repository( + name = "rust_analyzer_{}_{}".format(triple_suffix, version_key), diff --git a/patches/rules_rust_repository_set_exec_constraints.patch b/patches/rules_rust_repository_set_exec_constraints.patch deleted file mode 100644 index 31afae4f7a..0000000000 --- a/patches/rules_rust_repository_set_exec_constraints.patch +++ /dev/null @@ -1,26 +0,0 @@ -# What: let `rules_rust` repository_set entries specify an explicit exec-platform -# constraint set. -# Why: codex needs Windows nightly lint toolchains to run helper binaries on an -# MSVC exec platform while still targeting `windows-gnullvm` crates. - -diff --git a/rust/extensions.bzl b/rust/extensions.bzl ---- a/rust/extensions.bzl -+++ b/rust/extensions.bzl -@@ -52,6 +52,7 @@ def _rust_impl(module_ctx): - "allocator_library": repository_set.allocator_library, - "dev_components": repository_set.dev_components, - "edition": repository_set.edition, -+ "exec_compatible_with": [str(v) for v in repository_set.exec_compatible_with] if repository_set.exec_compatible_with else None, - "exec_triple": repository_set.exec_triple, - "extra_target_triples": {repository_set.target_triple: [str(v) for v in repository_set.target_compatible_with]}, - "name": repository_set.name, -@@ -166,6 +167,9 @@ _COMMON_TAG_KWARGS = { - - _RUST_REPOSITORY_SET_TAG_ATTRS = { -+ "exec_compatible_with": attr.label_list( -+ doc = "Execution platform constraints for this repository_set.", -+ ), - "exec_triple": attr.string( - doc = "Exec triple for this repository_set.", - ), - "name": attr.string( diff --git a/patches/rules_rust_windows_exec_std.patch b/patches/rules_rust_windows_exec_std.patch index aa20ee0e51..e8e4c4e55c 100644 --- a/patches/rules_rust_windows_exec_std.patch +++ b/patches/rules_rust_windows_exec_std.patch @@ -7,7 +7,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl --- a/rust/toolchain.bzl +++ b/rust/toolchain.bzl -@@ -209,6 +209,7 @@ def _generate_sysroot( +@@ -211,6 +211,7 @@ clippy = None, cargo_clippy = None, llvm_tools = None, @@ -15,23 +15,22 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl rust_std = None, rustfmt = None, linker = None): -@@ -312,7 +313,15 @@ def _generate_sysroot( - +@@ -315,6 +316,14 @@ # Made available to support $(location) expansion in stdlib_linkflags and extra_rustc_flags. transitive_file_sets.append(depset(ctx.files.rust_std)) -+ + + sysroot_exec_rust_std = None + if exec_rust_std: + sysroot_exec_rust_std = _symlink_sysroot_tree(ctx, name, exec_rust_std) + transitive_file_sets.extend([sysroot_exec_rust_std]) - ++ + # Made available to support $(location) expansion in extra_exec_rustc_flags. + transitive_file_sets.append(depset(ctx.files.exec_rust_std)) + # Declare a file in the root of the sysroot to make locating the sysroot easy sysroot_anchor = ctx.actions.declare_file("{}/rust.sysroot".format(name)) ctx.actions.write( -@@ -323,6 +332,7 @@ def _generate_sysroot( +@@ -325,6 +334,7 @@ "cargo-clippy: {}".format(cargo_clippy), "linker: {}".format(linker), "llvm_tools: {}".format(llvm_tools), @@ -39,7 +38,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl "rust_std: {}".format(rust_std), "rustc_lib: {}".format(rustc_lib), "rustc: {}".format(rustc), -@@ -340,6 +350,7 @@ def _generate_sysroot( +@@ -342,6 +352,7 @@ cargo_clippy = sysroot_cargo_clippy, clippy = sysroot_clippy, linker = sysroot_linker, @@ -47,7 +46,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl rust_std = sysroot_rust_std, rustc = sysroot_rustc, rustc_lib = sysroot_rustc_lib, -@@ -410,12 +421,14 @@ def _rust_toolchain_impl(ctx): +@@ -412,12 +423,14 @@ ) rust_std = ctx.attr.rust_std @@ -62,7 +61,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl rust_std = rust_std, rustfmt = ctx.file.rustfmt, clippy = ctx.file.clippy_driver, -@@ -452,7 +465,7 @@ def _rust_toolchain_impl(ctx): +@@ -454,7 +467,7 @@ expanded_stdlib_linkflags = _expand_flags(ctx, "stdlib_linkflags", rust_std[rust_common.stdlib_info].srcs, make_variables) expanded_extra_rustc_flags = _expand_flags(ctx, "extra_rustc_flags", rust_std[rust_common.stdlib_info].srcs, make_variables) @@ -71,7 +70,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl linking_context = cc_common.create_linking_context( linker_inputs = depset([ -@@ -793,6 +806,10 @@ rust_toolchain = rule( +@@ -807,6 +820,10 @@ doc = "The Rust standard library.", mandatory = True, ), @@ -85,7 +84,7 @@ diff --git a/rust/toolchain.bzl b/rust/toolchain.bzl diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.bzl --- a/rust/private/repository_utils.bzl +++ b/rust/private/repository_utils.bzl -@@ -341,6 +341,7 @@ rust_toolchain( +@@ -342,6 +342,7 @@ name = "{toolchain_name}", rust_doc = "//:rustdoc", rust_std = "//:rust_std-{target_triple}", @@ -93,15 +92,15 @@ diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.b rustc = "//:rustc", linker = {linker_label}, linker_type = {linker_type}, -@@ -384,6 +385,7 @@ def BUILD_for_rust_toolchain( - include_llvm_tools, - include_linker, +@@ -389,6 +390,7 @@ + include_llvm_tools = False, + include_linker = False, include_objcopy = False, + exec_rust_std_label = None, stdlib_linkflags = None, extra_rustc_flags = None, extra_exec_rustc_flags = None, -@@ -405,6 +407,7 @@ def BUILD_for_rust_toolchain( +@@ -412,6 +414,7 @@ include_llvm_tools (bool): Whether llvm-tools are present in the toolchain. include_linker (bool): Whether a linker is available in the toolchain. include_objcopy (bool): Whether rust-objcopy is available in the toolchain. @@ -109,7 +108,7 @@ diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.b stdlib_linkflags (list, optional): Overridden flags needed for linking to rust stdlib, akin to BAZEL_LINKLIBS. Defaults to None. -@@ -453,6 +456,7 @@ def BUILD_for_rust_toolchain( +@@ -465,6 +468,7 @@ staticlib_ext = system_to_staticlib_ext(target_triple.system), dylib_ext = system_to_dylib_ext(target_triple.system), allocator_library = repr(allocator_library_label), @@ -120,7 +119,7 @@ diff --git a/rust/private/repository_utils.bzl b/rust/private/repository_utils.b diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl -@@ -1011,7 +1011,10 @@ def construct_arguments( +@@ -1010,7 +1010,10 @@ if build_metadata and not use_json_output: fail("build_metadata requires parse_json_output") @@ -135,7 +134,7 @@ diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl diff --git a/rust/repositories.bzl b/rust/repositories.bzl --- a/rust/repositories.bzl +++ b/rust/repositories.bzl -@@ -536,6 +536,18 @@ def _rust_toolchain_tools_repository_impl(ctx): +@@ -574,6 +574,18 @@ build_components.append(rust_stdlib_content) sha256s.update(rust_stdlib_sha256) @@ -154,7 +153,7 @@ diff --git a/rust/repositories.bzl b/rust/repositories.bzl stdlib_linkflags = None if "BAZEL_RUST_STDLIB_LINKFLAGS" in ctx.os.environ: stdlib_linkflags = ctx.os.environ["BAZEL_RUST_STDLIB_LINKFLAGS"].split(":") -@@ -552,6 +564,7 @@ def _rust_toolchain_tools_repository_impl(ctx): +@@ -590,6 +602,7 @@ include_llvm_tools = include_llvm_tools, include_linker = include_linker, include_objcopy = include_objcopy, @@ -162,12 +161,7 @@ diff --git a/rust/repositories.bzl b/rust/repositories.bzl extra_rustc_flags = ctx.attr.extra_rustc_flags, extra_exec_rustc_flags = ctx.attr.extra_exec_rustc_flags, opt_level = ctx.attr.opt_level if ctx.attr.opt_level else None, -@@ -575,8 +588,16 @@ def _rust_toolchain_tools_repository_impl(ctx): - if ctx.attr.dev_components: - rustc_dev_sha256 = load_rustc_dev_nightly( - ctx = ctx, - target_triple = target_triple, - version = version, +@@ -608,6 +621,14 @@ iso_date = iso_date, ) sha256s.update(rustc_dev_sha256) @@ -179,3 +173,6 @@ diff --git a/rust/repositories.bzl b/rust/repositories.bzl + iso_date = iso_date, + ) + sha256s.update(exec_rustc_dev_sha256) + + ctx.file("WORKSPACE.bazel", """workspace(name = "{}")""".format( + ctx.name, diff --git a/patches/rules_rust_windows_msvc_direct_link_args.patch b/patches/rules_rust_windows_msvc_direct_link_args.patch index 05aef785df..fcf8a487be 100644 --- a/patches/rules_rust_windows_msvc_direct_link_args.patch +++ b/patches/rules_rust_windows_msvc_direct_link_args.patch @@ -26,47 +26,44 @@ filtered_args.append(processed_arg) if processed_arg == "--sysroot" and i + 1 < len(link_args): # Two-part argument, keep the next arg too -@@ -2305,7 +2335,7 @@ - return crate.metadata.dirname - return crate.output.dirname +@@ -2256,8 +2256,10 @@ + use_pic, + ambiguous_libs, + get_lib_name, ++ for_windows = False, + for_darwin = False, +- flavor_msvc = False): ++ flavor_msvc = False, ++ use_direct_driver = False): + """_summary_ --def _portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, for_windows = False, for_darwin = False, flavor_msvc = False): -+def _portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, for_windows = False, for_darwin = False, flavor_msvc = False, use_direct_driver = False): - artifact = get_preferred_artifact(lib, use_pic) - if ambiguous_libs and artifact.path in ambiguous_libs: - artifact = ambiguous_libs[artifact.path] -@@ -2344,6 +2344,11 @@ - artifact.basename.startswith("test-") or artifact.basename.startswith("std-") + Args: +@@ -2310,6 +2312,11 @@ ): return [] if for_darwin else ["-lstatic=%s" % get_lib_name(artifact)] -+ + + if for_windows and use_direct_driver and not artifact.basename.endswith(".lib"): + return [ + "-Clink-arg={}".format(artifact.path), + ] - ++ if flavor_msvc: return [ -@@ -2381,7 +2386,7 @@ + "-lstatic=%s" % get_lib_name(artifact), +@@ -2346,7 +2353,7 @@ ]) elif include_link_flags: get_lib_name = get_lib_name_for_windows if flavor_msvc else get_lib_name_default -- ret.extend(_portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, flavor_msvc = flavor_msvc)) -+ ret.extend(_portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, flavor_msvc = flavor_msvc, use_direct_driver = use_direct_driver)) +- ret.extend(portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, flavor_msvc = flavor_msvc)) ++ ret.extend(portable_link_flags(lib, use_pic, ambiguous_libs, get_lib_name, for_windows = True, flavor_msvc = flavor_msvc, use_direct_driver = use_direct_driver)) # Windows toolchains can inherit POSIX defaults like -pthread from C deps, # which fails to link with the MinGW/LLD toolchain. Drop them here. -@@ -2453,14 +2483,21 @@ +@@ -2522,17 +2529,25 @@ else: # For all other crate types we want to link C++ runtime library statically # (for example libstdc++.a or libc++.a). + runtime_libs = cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration) -+ if toolchain.target_os == "windows" and use_direct_link_driver: -+ runtime_libs = depset([ -+ runtime_lib -+ for runtime_lib in runtime_libs.to_list() -+ if runtime_lib.basename.endswith(".lib") -+ ]) args.add_all( - cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration), + runtime_libs, @@ -79,11 +76,18 @@ - map_each = get_lib_name, - format_each = "-lstatic=%s", - ) -+ args.add_all( -+ runtime_libs, -+ map_each = get_lib_name, -+ format_each = "-lstatic=%s", -+ ) ++ if toolchain.target_os == "windows" and use_direct_link_driver: ++ for runtime_lib in runtime_libs.to_list(): ++ if runtime_lib.basename.endswith(".lib"): ++ args.add(get_lib_name(runtime_lib), format = "-lstatic=%s") ++ else: ++ args.add(runtime_lib.path, format = "--codegen=link-arg=%s") ++ else: ++ args.add_all( ++ runtime_libs, ++ map_each = get_lib_name, ++ format_each = "-lstatic=%s", ++ ) def _get_dirname(file): """A helper function for `_add_native_link_flags`.