From 1d17ca1fa3f485603c4a2e34ff89d489dceeb8e3 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Fri, 3 Oct 2025 10:43:12 -0700 Subject: [PATCH] [MCP] Add support for MCP Oauth credentials (#4517) This PR adds oauth login support to streamable http servers when `experimental_use_rmcp_client` is enabled. This PR is large but represents the minimal amount of work required for this to work. To keep this PR smaller, login can only be done with `codex mcp login` and `codex mcp logout` but it doesn't appear in `/mcp` or `codex mcp list` yet. Fingers crossed that this is the last large MCP PR and that subsequent PRs can be smaller. Under the hood, credentials are stored using platform credential managers using the [keyring crate](https://crates.io/crates/keyring). When the keyring isn't available, it falls back to storing credentials in `CODEX_HOME/.credentials.json` which is consistent with how other coding agents handle authentication. I tested this on macOS, Windows, WSL (ubuntu), and Linux. I wasn't able to test the dbus store on linux but did verify that the fallback works. One quirk is that if you have credentials, during development, every build will have its own ad-hoc binary so the keyring won't recognize the reader as being the same as the write so it may ask for the user's password. I may add an override to disable this or allow users/enterprises to opt-out of the keyring storage if it causes issues. CleanShot 2025-09-30 at 19 31 40 image --- AGENTS.md | 4 + codex-rs/Cargo.lock | 1405 ++++++++++++----- codex-rs/Cargo.toml | 6 +- codex-rs/cli/Cargo.toml | 1 + codex-rs/cli/src/mcp_cmd.rs | 81 + codex-rs/core/Cargo.toml | 3 +- codex-rs/core/src/mcp_connection_manager.rs | 14 +- codex-rs/core/tests/common/test_codex.rs | 4 +- codex-rs/core/tests/suite/rmcp_client.rs | 247 +++ codex-rs/protocol/src/protocol.rs | 1 - codex-rs/rmcp-client/Cargo.toml | 33 +- .../src/bin/test_streamable_http_server.rs | 31 + codex-rs/rmcp-client/src/find_codex_home.rs | 33 + codex-rs/rmcp-client/src/lib.rs | 9 + codex-rs/rmcp-client/src/oauth.rs | 822 ++++++++++ .../rmcp-client/src/perform_oauth_login.rs | 141 ++ codex-rs/rmcp-client/src/rmcp_client.rs | 175 +- docs/config.md | 8 + 18 files changed, 2584 insertions(+), 434 deletions(-) create mode 100644 codex-rs/rmcp-client/src/find_codex_home.rs create mode 100644 codex-rs/rmcp-client/src/oauth.rs create mode 100644 codex-rs/rmcp-client/src/perform_oauth_login.rs diff --git a/AGENTS.md b/AGENTS.md index 28871944..336ad1e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,10 @@ In the codex-rs folder where the rust code lives: - Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`. - You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations. - Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate. +- Always collapse if statements per https://rust-lang.github.io/rust-clippy/master/index.html#collapsible_if +- Always inline format! args when possible per https://rust-lang.github.io/rust-clippy/master/index.html#uninlined_format_args +- Use method references over closures when possible per https://rust-lang.github.io/rust-clippy/master/index.html#redundant_closure_for_method_calls +- When writing tests, prefer comparing the equality of entire objects over fields one by one. Run `just fmt` (in `codex-rs` directory) automatically after making Rust code changes; do not ask for approval to run it. Before finalizing a change to `codex-rs`, run `just fix -p ` (in `codex-rs` directory) to fix any linter issues in the code. Prefer scoping with `-p` to avoid slow workspace‑wide Clippy builds; only run `just fix` without `-p` if you changed shared crates. Additionally, run the tests: 1. Run the test for the specific project that was changed. For example, if changes were made in `codex-rs/tui`, run `cargo test -p codex-tui`. diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index aa9736d2..c9228a0c 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -14,9 +14,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.25.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -27,6 +27,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -69,7 +80,7 @@ checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -111,9 +122,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -141,29 +152,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "app_test_support" @@ -180,9 +191,9 @@ dependencies = [ [[package]] name = "arboard" -version = "3.6.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227" dependencies = [ "clipboard-win", "image", @@ -194,7 +205,7 @@ dependencies = [ "objc2-foundation", "parking_lot", "percent-encoding", - "windows-sys 0.60.2", + "windows-sys 0.59.0", "x11rb", ] @@ -245,7 +256,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -289,6 +300,18 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -301,6 +324,107 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.8", + "slab", + "windows-sys 0.61.1", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.8", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.8", + "signal-hook-registry", + "slab", + "windows-sys 0.61.1", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -320,9 +444,15 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -331,7 +461,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -348,9 +478,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98e529aee37b5c8206bb4bf4c44797127566d72f76952c970bd3d1e85de8f4e2" +checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core", "bytes", @@ -366,7 +496,8 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "serde_core", + "rustversion", + "serde", "sync_wrapper", "tokio", "tower", @@ -376,9 +507,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ac7a6beb1182c7e30253ee75c3e918080bfb83f5a3023bcdf7209d85fd147e6" +checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" dependencies = [ "bytes", "futures-core", @@ -387,6 +518,7 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -394,9 +526,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -404,7 +536,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link 0.2.0", + "windows-targets 0.52.6", ] [[package]] @@ -451,9 +583,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -464,6 +596,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bstr" version = "1.12.0" @@ -483,9 +637,9 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -521,12 +675,20 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.39" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" dependencies = [ - "find-msvc-tools", "shlex", ] @@ -538,9 +700,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -575,10 +737,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" [[package]] -name = "clap" -version = "4.5.48" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931" dependencies = [ "clap_builder", "clap_derive", @@ -586,9 +758,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6" dependencies = [ "anstream", "anstyle", @@ -599,9 +771,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.58" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75bf0b32ad2e152de789bb635ea4d3078f6b838ad7974143e99b99f45a04af4a" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" dependencies = [ "clap", ] @@ -615,7 +787,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -777,6 +949,7 @@ dependencies = [ "codex-protocol", "codex-protocol-ts", "codex-responses-api-proxy", + "codex-rmcp-client", "codex-tui", "ctor 0.5.0", "owo-colors", @@ -869,7 +1042,7 @@ dependencies = [ "escargot", "eventsource-stream", "futures", - "indexmap 2.11.4", + "indexmap 2.10.0", "landlock", "libc", "maplit", @@ -885,6 +1058,7 @@ dependencies = [ "seccompiler", "serde", "serde_json", + "serial_test", "sha1", "shlex", "similar", @@ -1167,15 +1341,23 @@ version = "0.0.0" dependencies = [ "anyhow", "axum", + "dirs", "futures", + "keyring", "mcp-types", + "oauth2", "pretty_assertions", "reqwest", "rmcp", "serde", "serde_json", + "sha2", + "tempfile", + "tiny_http", "tokio", "tracing", + "urlencoding", + "webbrowser", ] [[package]] @@ -1443,7 +1625,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "crossterm_winapi", "futures-core", "mio", @@ -1536,7 +1718,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1550,7 +1732,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1561,7 +1743,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1572,7 +1754,36 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.106", + "syn 2.0.104", +] + +[[package]] +name = "dbus" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "fastrand", + "hkdf", + "num", + "once_cell", + "sha2", + "zeroize", ] [[package]] @@ -1652,7 +1863,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -1664,7 +1875,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "unicode-xid", ] @@ -1697,6 +1908,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1726,7 +1938,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users 0.5.2", + "redox_users 0.5.0", "windows-sys 0.61.1", ] @@ -1747,7 +1959,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", ] @@ -1769,7 +1981,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1828,14 +2040,14 @@ checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "dyn-clone" -version = "1.0.20" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "either" @@ -1867,6 +2079,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "endian-type" version = "0.1.2" @@ -1880,6 +2098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", + "serde", ] [[package]] @@ -1890,7 +2109,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -1939,12 +2158,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.14" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -1966,9 +2185,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -2029,7 +2248,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2039,7 +2258,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix 1.1.2", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -2063,12 +2282,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "find-msvc-tools" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" - [[package]] name = "fixed_decimal" version = "0.7.0" @@ -2134,9 +2347,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2189,6 +2402,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -2197,7 +2423,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2251,19 +2477,19 @@ dependencies = [ [[package]] name = "gethostname" -version = "1.0.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc257fdb4038301ce4b9cd1b3b51704509692bb3ff716a410cbd07925d9dae55" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" dependencies = [ - "rustix 1.1.2", - "windows-targets 0.52.6", + "libc", + "windows-targets 0.48.5", ] [[package]] name = "getopts" -version = "0.2.24" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ "unicode-width 0.2.1", ] @@ -2291,15 +2517,15 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.32.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "globset" @@ -2311,14 +2537,14 @@ dependencies = [ "bstr", "log", "regex-automata", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" dependencies = [ "atomic-waker", "bytes", @@ -2326,7 +2552,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.10.0", "slab", "tokio", "tokio-util", @@ -2361,21 +2587,15 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.5" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - [[package]] name = "heck" version = "0.5.0" @@ -2394,6 +2614,24 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.11" @@ -2530,9 +2768,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -2556,9 +2794,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2566,7 +2804,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.1", + "windows-core", ] [[package]] @@ -2717,9 +2955,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.1.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ "idna_adapter", "smallvec", @@ -2770,9 +3008,9 @@ dependencies = [ [[package]] name = "indenter" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" @@ -2787,14 +3025,13 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.4", "serde", - "serde_core", ] [[package]] @@ -2803,6 +3040,16 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "insta" version = "1.43.2" @@ -2824,25 +3071,25 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "inventory" -version = "0.3.21" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +checksum = "ab08d7cd2c5897f2c949e5383ea7c7db03fb19130ffcfbf7eda795137ae3cb83" dependencies = [ "rustversion", ] [[package]] name = "io-uring" -version = "0.7.10" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -2940,7 +3187,7 @@ checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -2967,14 +3214,32 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "linux-keyutils", + "log", + "secret-service", + "security-framework 2.11.1", + "security-framework 3.5.1", + "windows-sys 0.60.2", + "zbus", + "zeroize", +] + [[package]] name = "lalrpop" version = "0.19.12" @@ -3008,9 +3273,9 @@ dependencies = [ [[package]] name = "landlock" -version = "0.4.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affe8b77dce5b172f8e290bd801b12832a77cd1942d1ea98259916e89d5829d6" +checksum = "b3d2ef408b88e913bfc6594f5e693d57676f6463ded7d8bf994175364320c706" dependencies = [ "enumflags2", "libc", @@ -3025,9 +3290,18 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libdbus-sys" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +dependencies = [ + "pkg-config", +] [[package]] name = "libm" @@ -3037,11 +3311,21 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags 2.9.1", "libc", ] @@ -3053,9 +3337,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" @@ -3108,7 +3392,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.15.4", ] [[package]] @@ -3179,9 +3463,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -3192,6 +3476,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -3299,19 +3592,32 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "cfg_aliases 0.1.1", "libc", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset 0.9.1", +] + [[package]] name = "nix" version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "cfg_aliases 0.2.1", "libc", @@ -3352,6 +3658,20 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3362,6 +3682,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3377,6 +3706,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3405,6 +3756,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.16", + "http", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + [[package]] name = "objc2" version = "0.6.2" @@ -3420,7 +3791,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-graphics", "objc2-foundation", @@ -3432,7 +3803,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "dispatch2", "objc2", ] @@ -3443,7 +3814,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "dispatch2", "objc2", "objc2-core-foundation", @@ -3462,7 +3833,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-foundation", ] @@ -3473,16 +3844,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "objc2", "objc2-core-foundation", ] [[package]] name = "object" -version = "0.37.3" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -3505,7 +3876,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -3522,7 +3893,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3533,9 +3904,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-src" -version = "300.5.2+3.5.2" +version = "300.5.1+3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a" dependencies = [ "cc", ] @@ -3657,6 +4028,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_info" version = "3.12.0" @@ -3742,9 +4123,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -3753,7 +4134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.10.0", ] [[package]] @@ -3782,7 +4163,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3797,6 +4178,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3805,12 +4197,12 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plist" -version = "1.8.0" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64", - "indexmap 2.11.4", + "indexmap 2.10.0", "quick-xml", "serde", "time", @@ -3822,13 +4214,27 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.8", + "windows-sys 0.61.1", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -3867,9 +4273,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" dependencies = [ "serde", "zerovec", @@ -3937,10 +4343,19 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.101" +name = "proc-macro-crate" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -3952,7 +4367,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" dependencies = [ "futures", - "indexmap 2.11.4", + "indexmap 2.10.0", "nix 0.30.1", "tokio", "tracing", @@ -3979,7 +4394,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -3988,7 +4403,7 @@ version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "getopts", "memchr", "pulldown-cmark-escape", @@ -4003,9 +4418,9 @@ checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" dependencies = [ "num-traits", ] @@ -4018,18 +4433,18 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "8927b0664f5c5a98265138b7e3f90aa19a6b21353182469ace36d4ac527b7b1b" dependencies = [ "memchr", ] [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases 0.2.1", @@ -4038,7 +4453,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.16", "tokio", "tracing", @@ -4047,9 +4462,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -4068,16 +4483,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4169,7 +4584,7 @@ name = "ratatui" version = "0.29.0" source = "git+https://github.com/nornagon/ratatui?branch=nornagon-v0.29.0-patch#9b2ad1298408c45918ee9f8241a6f95498cdbed2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cassowary", "compact_str", "crossterm", @@ -4186,11 +4601,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", ] [[package]] @@ -4206,9 +4621,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.5.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" dependencies = [ "getrandom 0.2.16", "libredox", @@ -4232,30 +4647,30 @@ checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", ] [[package]] @@ -4272,9 +4687,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -4352,6 +4767,7 @@ dependencies = [ "http", "http-body", "http-body-util", + "oauth2", "paste", "pin-project-lite", "process-wrap", @@ -4368,6 +4784,7 @@ dependencies = [ "tokio-util", "tower-service", "tracing", + "url", "uuid", ] @@ -4381,14 +4798,14 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "rustc-demangle" -version = "0.1.26" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -4402,7 +4819,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4411,22 +4828,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys 0.11.0", - "windows-sys 0.61.1", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "once_cell", "ring", @@ -4445,7 +4862,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.5.1", ] [[package]] @@ -4460,9 +4877,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -4471,9 +4888,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "rustyline" @@ -4481,7 +4898,7 @@ version = "14.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "cfg-if", "clipboard-win", "fd-lock", @@ -4512,6 +4929,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.28" @@ -4610,7 +5036,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4622,7 +5048,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4631,6 +5057,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + [[package]] name = "seccompiler" version = "0.5.0" @@ -4640,13 +5072,32 @@ dependencies = [ "libc", ] +[[package]] +name = "secret-service" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4" +dependencies = [ + "aes", + "cbc", + "futures-util", + "generic-array", + "hkdf", + "num", + "once_cell", + "rand 0.8.5", + "serde", + "sha2", + "zbus", +] + [[package]] name = "security-framework" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4655,11 +5106,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4678,9 +5129,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.227" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80ece43fc6fbed4eb5392ab50c07334d3e577cbf40997ee896fe7af40bba4245" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" dependencies = [ "serde_core", "serde_derive", @@ -4688,22 +5139,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.227" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a576275b607a2c86ea29e410193df32bc680303c82f31e275bbfcafe8b33be5" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.227" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e694923b8824cf0e9b382adf0f60d4e05f348f357b38833a3fa5ed7c2ede04" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4714,7 +5165,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -4723,7 +5174,7 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.10.0", "itoa", "memchr", "ryu", @@ -4731,6 +5182,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -4739,16 +5201,16 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "serde_spanned" -version = "1.0.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ - "serde_core", + "serde", ] [[package]] @@ -4765,15 +5227,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" dependencies = [ "base64", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.10.0", "schemars 0.9.0", "schemars 1.0.4", "serde", @@ -4785,27 +5247,52 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" dependencies = [ - "darling 0.21.3", + "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "serial2" -version = "0.2.33" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" +checksum = "26e1e5956803a69ddd72ce2de337b577898801528749565def03515f82bad5bb" dependencies = [ "cfg-if", "libc", "winapi", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "sha1" version = "0.10.6" @@ -4882,9 +5369,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" dependencies = [ "libc", ] @@ -4991,7 +5478,7 @@ dependencies = [ "inventory", "itertools 0.13.0", "maplit", - "memoffset", + "memoffset 0.6.5", "num-bigint", "num-traits", "once_cell", @@ -5019,7 +5506,7 @@ dependencies = [ "dupe", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5121,7 +5608,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5133,7 +5620,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5164,9 +5651,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -5190,7 +5677,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5208,7 +5695,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5232,7 +5719,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.1.2", + "rustix 1.0.8", "windows-sys 0.61.1", ] @@ -5258,12 +5745,12 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.4.3" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 1.1.2", - "windows-sys 0.60.2", + "rustix 1.0.8", + "windows-sys 0.59.0", ] [[package]] @@ -5318,7 +5805,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5329,7 +5816,7 @@ checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5472,7 +5959,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5487,9 +5974,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.4" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -5534,12 +6021,12 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.7" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ - "indexmap 2.11.4", - "serde_core", + "indexmap 2.10.0", + "serde", "serde_spanned", "toml_datetime", "toml_parser", @@ -5549,20 +6036,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ - "serde_core", + "serde", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "7211ff1b8f0d3adae1663b7da9ffe396eabe1ca25f0b0bee42b0da29a9ddce93" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.10.0", "toml_datetime", "toml_parser", "toml_writer", @@ -5571,18 +6058,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" dependencies = [ "winnow", ] [[package]] name = "toml_writer" -version = "1.0.3" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d163a63c116ce562a22cda521fcc4d79152e7aba014456fb5eb442f6d6a10109" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tonic" @@ -5621,7 +6108,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.11.4", + "indexmap 2.10.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5638,7 +6125,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.9.1", "bytes", "futures-util", "http", @@ -5694,7 +6181,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -5764,18 +6251,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "tree-sitter" -version = "0.25.10" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" +checksum = "ccd2a058a86cfece0bf96f7cce1021efef9c8ed0e892ab74639173e5ed7a34fa" dependencies = [ "cc", "regex", - "regex-syntax 0.8.6", + "regex-syntax 0.8.5", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -5823,7 +6310,7 @@ checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "termcolor", ] @@ -5833,6 +6320,17 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset 0.9.1", + "tempfile", + "winapi", +] + [[package]] name = "unicase" version = "2.8.1" @@ -5841,9 +6339,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -5894,9 +6392,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -6009,54 +6507,44 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.7+wasi-0.2.4" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", + "wit-bindgen-rt", ] [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", - "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -6067,9 +6555,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6077,22 +6565,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" dependencies = [ "unicode-ident", ] @@ -6112,9 +6600,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -6197,11 +6685,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.11" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.59.0", ] [[package]] @@ -6217,7 +6705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -6229,7 +6717,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", + "windows-core", ] [[package]] @@ -6241,21 +6729,8 @@ dependencies = [ "windows-implement", "windows-interface", "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-result", + "windows-strings", ] [[package]] @@ -6264,31 +6739,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", "windows-threading", ] [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6309,7 +6784,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core", "windows-link 0.1.3", ] @@ -6320,8 +6795,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-result", + "windows-strings", ] [[package]] @@ -6333,15 +6808,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -6351,15 +6817,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = [ - "windows-link 0.2.0", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -6393,7 +6850,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.4", + "windows-targets 0.53.2", ] [[package]] @@ -6420,6 +6877,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6438,11 +6910,10 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.4" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ - "windows-link 0.2.0", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -6468,6 +6939,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6486,6 +6963,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6504,6 +6987,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6534,6 +7023,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6552,6 +7047,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6570,6 +7071,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6588,6 +7095,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -6602,9 +7115,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -6648,10 +7161,13 @@ dependencies = [ ] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] [[package]] name = "writeable" @@ -6661,20 +7177,30 @@ checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "x11rb" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", - "rustix 1.1.2", + "rustix 0.38.44", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" -version = "0.13.2" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] [[package]] name = "yansi" @@ -6702,28 +7228,90 @@ checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] [[package]] -name = "zerocopy" -version = "0.8.27" +name = "zbus" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.29.0", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6743,7 +7331,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", "synstructure", ] @@ -6752,6 +7340,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "zerotrie" @@ -6766,9 +7368,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -6783,7 +7385,7 @@ checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.104", ] [[package]] @@ -6794,9 +7396,46 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "2c9e525af0a6a658e031e95f14b7f889976b74a11ba0eca5a5fc9ac8a1c43a6a" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f8e88797..683e1f6a 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -87,6 +87,7 @@ assert_cmd = "2" async-channel = "2.3.1" async-stream = "0.3.6" async-trait = "0.1.89" +axum = { version = "0.8", default-features = false } base64 = "0.22.1" bytes = "1.10.1" chrono = "0.4.42" @@ -104,7 +105,7 @@ env-flags = "0.1.1" env_logger = "0.11.5" escargot = "0.5" eventsource-stream = "0.2.3" -futures = "0.3" +futures = { version = "0.3", default-features = false } icu_decimal = "2.0.0" icu_locale_core = "2.0.0" ignore = "0.4.23" @@ -112,6 +113,7 @@ image = { version = "^0.25.8", default-features = false } indexmap = "2.6.0" insta = "1.43.2" itertools = "0.14.0" +keyring = "3.6" landlock = "0.4.1" lazy_static = "1" libc = "0.2.175" @@ -140,11 +142,13 @@ rand = "0.9" ratatui = "0.29.0" regex-lite = "0.1.7" reqwest = "0.12" +rmcp = { version = "0.7.0", default-features = false } schemars = "0.8.22" seccompiler = "0.5.0" serde = "1" serde_json = "1" serde_with = "3.14" +serial_test = "3.2.0" sha1 = "0.10.6" sha2 = "0.10" shlex = "1.3.0" diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index c4dac80e..bcece872 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -32,6 +32,7 @@ codex-app-server-protocol = { workspace = true } codex-protocol-ts = { workspace = true } codex-responses-api-proxy = { workspace = true } codex-tui = { workspace = true } +codex-rmcp-client = { workspace = true } codex-cloud-tasks = { path = "../cloud-tasks" } ctor = { workspace = true } owo-colors = { workspace = true } diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index 85243a64..b0c601b4 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -12,6 +12,8 @@ use codex_core::config::load_global_mcp_servers; use codex_core::config::write_global_mcp_servers; use codex_core::config_types::McpServerConfig; use codex_core::config_types::McpServerTransportConfig; +use codex_rmcp_client::delete_oauth_tokens; +use codex_rmcp_client::perform_oauth_login; /// [experimental] Launch Codex as an MCP server or manage configured MCP servers. /// @@ -43,6 +45,14 @@ pub enum McpSubcommand { /// [experimental] Remove a global MCP server entry. Remove(RemoveArgs), + + /// [experimental] Authenticate with a configured MCP server via OAuth. + /// Requires experimental_use_rmcp_client = true in config.toml. + Login(LoginArgs), + + /// [experimental] Remove stored OAuth credentials for a server. + /// Requires experimental_use_rmcp_client = true in config.toml. + Logout(LogoutArgs), } #[derive(Debug, clap::Parser)] @@ -82,6 +92,18 @@ pub struct RemoveArgs { pub name: String, } +#[derive(Debug, clap::Parser)] +pub struct LoginArgs { + /// Name of the MCP server to authenticate with oauth. + pub name: String, +} + +#[derive(Debug, clap::Parser)] +pub struct LogoutArgs { + /// Name of the MCP server to deauthenticate. + pub name: String, +} + impl McpCli { pub async fn run(self) -> Result<()> { let McpCli { @@ -102,6 +124,12 @@ impl McpCli { McpSubcommand::Remove(args) => { run_remove(&config_overrides, args)?; } + McpSubcommand::Login(args) => { + run_login(&config_overrides, args).await?; + } + McpSubcommand::Logout(args) => { + run_logout(&config_overrides, args)?; + } } Ok(()) @@ -183,6 +211,59 @@ fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveArgs) -> Ok(()) } +async fn run_login(config_overrides: &CliConfigOverrides, login_args: LoginArgs) -> Result<()> { + let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) + .context("failed to load configuration")?; + + if !config.use_experimental_use_rmcp_client { + bail!( + "OAuth login is only supported when experimental_use_rmcp_client is true in config.toml." + ); + } + + let LoginArgs { name } = login_args; + + let Some(server) = config.mcp_servers.get(&name) else { + bail!("No MCP server named '{name}' found."); + }; + + let url = match &server.transport { + McpServerTransportConfig::StreamableHttp { url, .. } => url.clone(), + _ => bail!("OAuth login is only supported for streamable HTTP servers."), + }; + + perform_oauth_login(&name, &url).await?; + println!("Successfully logged in to MCP server '{name}'."); + Ok(()) +} + +fn run_logout(config_overrides: &CliConfigOverrides, logout_args: LogoutArgs) -> Result<()> { + let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; + let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) + .context("failed to load configuration")?; + + let LogoutArgs { name } = logout_args; + + let server = config + .mcp_servers + .get(&name) + .ok_or_else(|| anyhow!("No MCP server named '{name}' found in configuration."))?; + + let url = match &server.transport { + McpServerTransportConfig::StreamableHttp { url, .. } => url.clone(), + _ => bail!("OAuth logout is only supported for streamable_http transports."), + }; + + match delete_oauth_tokens(&name, &url) { + Ok(true) => println!("Removed OAuth credentials for '{name}'."), + Ok(false) => println!("No OAuth credentials stored for '{name}'."), + Err(err) => return Err(anyhow!("failed to delete OAuth credentials: {err}")), + } + + Ok(()) +} + fn run_list(config_overrides: &CliConfigOverrides, list_args: ListArgs) -> Result<()> { let overrides = config_overrides.parse_overrides().map_err(|e| anyhow!(e))?; let config = Config::load_with_cli_overrides(overrides, ConfigOverrides::default()) diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 5054ce98..9c55ef9a 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -91,11 +91,12 @@ escargot = { workspace = true } maplit = { workspace = true } predicates = { workspace = true } pretty_assertions = { workspace = true } +serial_test = { workspace = true } tempfile = { workspace = true } tokio-test = { workspace = true } +tracing-test = { workspace = true, features = ["no-env-filter"] } walkdir = { workspace = true } wiremock = { workspace = true } -tracing-test = { workspace = true, features = ["no-env-filter"] } [package.metadata.cargo-shear] ignored = ["openssl-sys"] diff --git a/codex-rs/core/src/mcp_connection_manager.rs b/codex-rs/core/src/mcp_connection_manager.rs index 9be7d956..2f06f370 100644 --- a/codex-rs/core/src/mcp_connection_manager.rs +++ b/codex-rs/core/src/mcp_connection_manager.rs @@ -123,12 +123,15 @@ impl McpClientAdapter { } async fn new_streamable_http_client( + server_name: String, url: String, bearer_token: Option, params: mcp_types::InitializeRequestParams, startup_timeout: Duration, ) -> Result { - let client = Arc::new(RmcpClient::new_streamable_http_client(url, bearer_token)?); + let client = Arc::new( + RmcpClient::new_streamable_http_client(&server_name, &url, bearer_token).await?, + ); client.initialize(params, Some(startup_timeout)).await?; Ok(McpClientAdapter::Rmcp(client)) } @@ -208,8 +211,7 @@ impl McpConnectionManager { ) && !use_rmcp_client { info!( - "skipping MCP server `{}` configured with url because rmcp client is disabled", - server_name + "skipping MCP server `{server_name}` because the legacy MCP client only supports stdio servers", ); continue; } @@ -217,7 +219,6 @@ impl McpConnectionManager { let startup_timeout = cfg.startup_timeout_sec.unwrap_or(DEFAULT_STARTUP_TIMEOUT); let tool_timeout = cfg.tool_timeout_sec.unwrap_or(DEFAULT_TOOL_TIMEOUT); - let use_rmcp_client_flag = use_rmcp_client; join_set.spawn(async move { let McpServerConfig { transport, .. } = cfg; let params = mcp_types::InitializeRequestParams { @@ -246,17 +247,18 @@ impl McpConnectionManager { let command_os: OsString = command.into(); let args_os: Vec = args.into_iter().map(Into::into).collect(); McpClientAdapter::new_stdio_client( - use_rmcp_client_flag, + use_rmcp_client, command_os, args_os, env, - params.clone(), + params, startup_timeout, ) .await } McpServerTransportConfig::StreamableHttp { url, bearer_token } => { McpClientAdapter::new_streamable_http_client( + server_name.clone(), url, bearer_token, params, diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 65b25b13..3957b052 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -13,7 +13,7 @@ use tempfile::TempDir; use crate::load_default_config_for_test; -type ConfigMutator = dyn FnOnce(&mut Config); +type ConfigMutator = dyn FnOnce(&mut Config) + Send; pub struct TestCodexBuilder { config_mutators: Vec>, @@ -22,7 +22,7 @@ pub struct TestCodexBuilder { impl TestCodexBuilder { pub fn with_config(mut self, mutator: T) -> Self where - T: FnOnce(&mut Config) + 'static, + T: FnOnce(&mut Config) + Send + 'static, { self.config_mutators.push(Box::new(mutator)); self diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index bd168240..413efe79 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -1,6 +1,11 @@ use std::collections::HashMap; +use std::ffi::OsString; +use std::fs; use std::net::TcpListener; +use std::path::Path; use std::time::Duration; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; use codex_core::config_types::McpServerConfig; use codex_core::config_types::McpServerTransportConfig; @@ -19,6 +24,8 @@ use core_test_support::wait_for_event; use core_test_support::wait_for_event_with_timeout; use escargot::CargoBuild; use serde_json::Value; +use serial_test::serial; +use tempfile::tempdir; use tokio::net::TcpStream; use tokio::process::Child; use tokio::process::Command; @@ -328,6 +335,189 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { Ok(()) } +/// This test writes to a fallback credentials file in CODEX_HOME. +/// Ideally, we wouldn't need to serialize the test but it's much more cumbersome to wire CODEX_HOME through the code. +#[serial(codex_home)] +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + + let call_id = "call-789"; + let server_name = "rmcp_http_oauth"; + let tool_name = format!("{server_name}__echo"); + + mount_sse_once_match( + &server, + any(), + responses::sse(vec![ + serde_json::json!({ + "type": "response.created", + "response": {"id": "resp-1"} + }), + responses::ev_function_call(call_id, &tool_name, "{\"message\":\"ping\"}"), + responses::ev_completed("resp-1"), + ]), + ) + .await; + mount_sse_once_match( + &server, + any(), + responses::sse(vec![ + responses::ev_assistant_message( + "msg-1", + "rmcp streamable http oauth echo tool completed successfully.", + ), + responses::ev_completed("resp-2"), + ]), + ) + .await; + + let expected_env_value = "propagated-env-http-oauth"; + let expected_token = "initial-access-token"; + let client_id = "test-client-id"; + let refresh_token = "initial-refresh-token"; + let rmcp_http_server_bin = CargoBuild::new() + .package("codex-rmcp-client") + .bin("test_streamable_http_server") + .run()? + .path() + .to_string_lossy() + .into_owned(); + + let listener = TcpListener::bind("127.0.0.1:0")?; + let port = listener.local_addr()?.port(); + drop(listener); + let bind_addr = format!("127.0.0.1:{port}"); + let server_url = format!("http://{bind_addr}/mcp"); + + let mut http_server_child = Command::new(&rmcp_http_server_bin) + .kill_on_drop(true) + .env("MCP_STREAMABLE_HTTP_BIND_ADDR", &bind_addr) + .env("MCP_EXPECT_BEARER", expected_token) + .env("MCP_TEST_VALUE", expected_env_value) + .spawn()?; + + wait_for_streamable_http_server(&mut http_server_child, &bind_addr, Duration::from_secs(5)) + .await?; + + let temp_home = tempdir()?; + let _guard = EnvVarGuard::set("CODEX_HOME", temp_home.path().as_os_str()); + write_fallback_oauth_tokens( + temp_home.path(), + server_name, + &server_url, + client_id, + expected_token, + refresh_token, + )?; + + let fixture = test_codex() + .with_config(move |config| { + config.use_experimental_use_rmcp_client = true; + config.mcp_servers.insert( + server_name.to_string(), + McpServerConfig { + transport: McpServerTransportConfig::StreamableHttp { + url: server_url, + bearer_token: None, + }, + startup_timeout_sec: Some(Duration::from_secs(10)), + tool_timeout_sec: None, + }, + ); + }) + .build(&server) + .await?; + let session_model = fixture.session_configured.model.clone(); + + fixture + .codex + .submit(Op::UserTurn { + items: vec![InputItem::Text { + text: "call the rmcp streamable http oauth echo tool".into(), + }], + final_output_json_schema: None, + cwd: fixture.cwd.path().to_path_buf(), + approval_policy: AskForApproval::Never, + sandbox_policy: SandboxPolicy::DangerFullAccess, + model: session_model, + effort: None, + summary: ReasoningSummary::Auto, + }) + .await?; + + let begin_event = wait_for_event_with_timeout( + &fixture.codex, + |ev| matches!(ev, EventMsg::McpToolCallBegin(_)), + Duration::from_secs(10), + ) + .await; + + let EventMsg::McpToolCallBegin(begin) = begin_event else { + unreachable!("event guard guarantees McpToolCallBegin"); + }; + assert_eq!(begin.invocation.server, server_name); + assert_eq!(begin.invocation.tool, "echo"); + + 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 result = end + .result + .as_ref() + .expect("rmcp echo tool should return success"); + assert_eq!(result.is_error, Some(false)); + assert!( + result.content.is_empty(), + "content should default to an empty array" + ); + + let structured = result + .structured_content + .as_ref() + .expect("structured content"); + let Value::Object(map) = structured else { + panic!("structured content should be an object: {structured:?}"); + }; + let echo_value = map + .get("echo") + .and_then(Value::as_str) + .expect("echo payload present"); + assert_eq!(echo_value, "ECHOING: ping"); + let env_value = map + .get("env") + .and_then(Value::as_str) + .expect("env snapshot inserted"); + assert_eq!(env_value, expected_env_value); + + wait_for_event(&fixture.codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + + server.verify().await; + + match http_server_child.try_wait() { + Ok(Some(_)) => {} + Ok(None) => { + let _ = http_server_child.kill().await; + } + Err(error) => { + eprintln!("failed to check streamable http oauth server status: {error}"); + let _ = http_server_child.kill().await; + } + } + if let Err(error) = http_server_child.wait().await { + eprintln!("failed to await streamable http oauth server shutdown: {error}"); + } + + Ok(()) +} + async fn wait_for_streamable_http_server( server_child: &mut Child, address: &str, @@ -369,3 +559,60 @@ async fn wait_for_streamable_http_server( sleep(Duration::from_millis(50)).await; } } + +fn write_fallback_oauth_tokens( + home: &Path, + server_name: &str, + server_url: &str, + client_id: &str, + access_token: &str, + refresh_token: &str, +) -> anyhow::Result<()> { + let expires_at = SystemTime::now() + .checked_add(Duration::from_secs(3600)) + .ok_or_else(|| anyhow::anyhow!("failed to compute expiry time"))? + .duration_since(UNIX_EPOCH)? + .as_millis() as u64; + + let store = serde_json::json!({ + "stub": { + "server_name": server_name, + "server_url": server_url, + "client_id": client_id, + "access_token": access_token, + "expires_at": expires_at, + "refresh_token": refresh_token, + "scopes": ["profile"], + } + }); + + let file_path = home.join(".credentials.json"); + fs::write(&file_path, serde_json::to_vec(&store)?)?; + Ok(()) +} + +struct EnvVarGuard { + key: &'static str, + original: Option, +} + +impl EnvVarGuard { + fn set(key: &'static str, value: &std::ffi::OsStr) -> Self { + let original = std::env::var_os(key); + unsafe { + std::env::set_var(key, value); + } + Self { key, original } + } +} + +impl Drop for EnvVarGuard { + fn drop(&mut self) { + unsafe { + match &self.original { + Some(value) => std::env::set_var(self.key, value), + None => std::env::remove_var(self.key), + } + } + } +} diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2c6d3b33..b6b279e0 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1206,7 +1206,6 @@ pub struct GetHistoryEntryResponseEvent { pub entry: Option, } -/// Response payload for `Op::ListMcpTools`. #[derive(Debug, Clone, Deserialize, Serialize, TS)] pub struct McpListToolsResponseEvent { /// Fully qualified tool name -> tool definition. diff --git a/codex-rs/rmcp-client/Cargo.toml b/codex-rs/rmcp-client/Cargo.toml index a377b94f..ddbc7056 100644 --- a/codex-rs/rmcp-client/Cargo.toml +++ b/codex-rs/rmcp-client/Cargo.toml @@ -8,8 +8,19 @@ workspace = true [dependencies] anyhow = "1" +axum = { workspace = true, default-features = false, features = [ + "http1", + "tokio", +] } +keyring = { workspace = true, features = [ + "apple-native", + "crypto-rust", + "linux-native-async-persistent", + "windows-native", +] } mcp-types = { path = "../mcp-types" } -rmcp = { version = "0.7.0", default-features = false, features = [ +rmcp = { workspace = true, default-features = false, features = [ + "auth", "base64", "client", "macros", @@ -19,16 +30,19 @@ rmcp = { version = "0.7.0", default-features = false, features = [ "transport-streamable-http-client-reqwest", "transport-streamable-http-server", ] } -axum = { version = "0.8", default-features = false, features = ["http1", "tokio"] } -futures = { version = "0.3", default-features = false, features = ["std"] } +futures = { workspace = true, default-features = false, features = ["std"] } reqwest = { version = "0.12", default-features = false, features = [ "json", "stream", "rustls-tls", ] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = [ +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +dirs = { workspace = true } +oauth2 = "5" +tiny_http = { workspace = true } +tokio = { workspace = true, features = [ "io-util", "macros", "process", @@ -37,7 +51,10 @@ tokio = { version = "1", features = [ "io-std", "time", ] } -tracing = { version = "0.1.41", features = ["log"] } +tracing = { workspace = true, features = ["log"] } +urlencoding = { workspace = true } +webbrowser = { workspace = true } [dev-dependencies] -pretty_assertions = "1.4.1" +pretty_assertions = { workspace = true } +tempfile = { workspace = true } diff --git a/codex-rs/rmcp-client/src/bin/test_streamable_http_server.rs b/codex-rs/rmcp-client/src/bin/test_streamable_http_server.rs index eedd1cb1..81a60404 100644 --- a/codex-rs/rmcp-client/src/bin/test_streamable_http_server.rs +++ b/codex-rs/rmcp-client/src/bin/test_streamable_http_server.rs @@ -5,6 +5,14 @@ use std::net::SocketAddr; use std::sync::Arc; use axum::Router; +use axum::body::Body; +use axum::extract::State; +use axum::http::Request; +use axum::http::StatusCode; +use axum::http::header::AUTHORIZATION; +use axum::middleware; +use axum::middleware::Next; +use axum::response::Response; use rmcp::ErrorData as McpError; use rmcp::handler::server::ServerHandler; use rmcp::model::CallToolRequestParam; @@ -161,7 +169,30 @@ async fn main() -> Result<(), Box> { ), ); + let router = if let Ok(token) = std::env::var("MCP_EXPECT_BEARER") { + let expected = Arc::new(format!("Bearer {token}")); + router.layer(middleware::from_fn_with_state(expected, require_bearer)) + } else { + router + }; + axum::serve(listener, router).await?; task::yield_now().await; Ok(()) } + +async fn require_bearer( + State(expected): State>, + request: Request, + next: Next, +) -> Result { + if request + .headers() + .get(AUTHORIZATION) + .is_some_and(|value| value.as_bytes() == expected.as_bytes()) + { + Ok(next.run(request).await) + } else { + Err(StatusCode::UNAUTHORIZED) + } +} diff --git a/codex-rs/rmcp-client/src/find_codex_home.rs b/codex-rs/rmcp-client/src/find_codex_home.rs new file mode 100644 index 00000000..d683ba9d --- /dev/null +++ b/codex-rs/rmcp-client/src/find_codex_home.rs @@ -0,0 +1,33 @@ +use dirs::home_dir; +use std::path::PathBuf; + +/// This was copied from codex-core but codex-core depends on this crate. +/// TODO: move this to a shared crate lower in the dependency tree. +/// +/// +/// Returns the path to the Codex configuration directory, which can be +/// specified by the `CODEX_HOME` environment variable. If not set, defaults to +/// `~/.codex`. +/// +/// - If `CODEX_HOME` is set, the value will be canonicalized and this +/// function will Err if the path does not exist. +/// - If `CODEX_HOME` is not set, this function does not verify that the +/// directory exists. +pub(crate) fn find_codex_home() -> std::io::Result { + // Honor the `CODEX_HOME` environment variable when it is set to allow users + // (and tests) to override the default location. + if let Ok(val) = std::env::var("CODEX_HOME") + && !val.is_empty() + { + return PathBuf::from(val).canonicalize(); + } + + let mut p = home_dir().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::NotFound, + "Could not find home directory", + ) + })?; + p.push(".codex"); + Ok(p) +} diff --git a/codex-rs/rmcp-client/src/lib.rs b/codex-rs/rmcp-client/src/lib.rs index ef508840..ac69a100 100644 --- a/codex-rs/rmcp-client/src/lib.rs +++ b/codex-rs/rmcp-client/src/lib.rs @@ -1,5 +1,14 @@ +mod find_codex_home; mod logging_client_handler; +mod oauth; +mod perform_oauth_login; mod rmcp_client; mod utils; +pub use oauth::StoredOAuthTokens; +pub use oauth::WrappedOAuthTokenResponse; +pub use oauth::delete_oauth_tokens; +pub(crate) use oauth::load_oauth_tokens; +pub use oauth::save_oauth_tokens; +pub use perform_oauth_login::perform_oauth_login; pub use rmcp_client::RmcpClient; diff --git a/codex-rs/rmcp-client/src/oauth.rs b/codex-rs/rmcp-client/src/oauth.rs new file mode 100644 index 00000000..bb13b718 --- /dev/null +++ b/codex-rs/rmcp-client/src/oauth.rs @@ -0,0 +1,822 @@ +//! This file handles all logic related to managing MCP OAuth credentials. +//! All credentials are stored using the keyring crate which uses os-specific keyring services. +//! https://crates.io/crates/keyring +//! macOS: macOS keychain. +//! Windows: Windows Credential Manager +//! Linux: DBus-based Secret Service, the kernel keyutils, and a combo of the two +//! FreeBSD, OpenBSD: DBus-based Secret Service +//! +//! For Linux, we use linux-native-async-persistent which uses both keyutils and async-secret-service (see below) for storage. +//! See the docs for the keyutils_persistent module for a full explanation of why both are used. Because this store uses the +//! async-secret-service, you must specify the additional features required by that store +//! +//! async-secret-service provides access to the DBus-based Secret Service storage on Linux, FreeBSD, and OpenBSD. This is an asynchronous +//! keystore that always encrypts secrets when they are transferred across the bus. If DBus isn't installed the keystore will fall back to the json +//! file because we don't use the "vendored" feature. +//! +//! If the keyring is not available or fails, we fall back to CODEX_HOME/.credentials.json which is consistent with other coding CLI agents. + +use anyhow::Context; +use anyhow::Result; +use keyring::Entry; +use oauth2::AccessToken; +use oauth2::EmptyExtraTokenFields; +use oauth2::RefreshToken; +use oauth2::Scope; +use oauth2::TokenResponse; +use oauth2::basic::BasicTokenType; +use rmcp::transport::auth::OAuthTokenResponse; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value; +use serde_json::map::Map as JsonMap; +use sha2::Digest; +use sha2::Sha256; +use std::collections::BTreeMap; +use std::fmt; +use std::fs; +use std::io::ErrorKind; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::Duration; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use tracing::warn; + +use rmcp::transport::auth::AuthorizationManager; +use tokio::sync::Mutex; + +use crate::find_codex_home::find_codex_home; + +const KEYRING_SERVICE: &str = "Codex MCP Credentials"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StoredOAuthTokens { + pub server_name: String, + pub url: String, + pub client_id: String, + pub token_response: WrappedOAuthTokenResponse, +} + +#[derive(Debug)] +struct CredentialStoreError(anyhow::Error); + +impl CredentialStoreError { + fn new(error: impl Into) -> Self { + Self(error.into()) + } + + fn message(&self) -> String { + self.0.to_string() + } + + fn into_error(self) -> anyhow::Error { + self.0 + } +} + +impl fmt::Display for CredentialStoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for CredentialStoreError {} + +trait CredentialStore { + fn load(&self, service: &str, account: &str) -> Result, CredentialStoreError>; + fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError>; + fn delete(&self, service: &str, account: &str) -> Result; +} + +struct KeyringCredentialStore; + +impl CredentialStore for KeyringCredentialStore { + fn load(&self, service: &str, account: &str) -> Result, CredentialStoreError> { + let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; + match entry.get_password() { + Ok(password) => Ok(Some(password)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(error) => Err(CredentialStoreError::new(error)), + } + } + + fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError> { + let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; + entry.set_password(value).map_err(CredentialStoreError::new) + } + + fn delete(&self, service: &str, account: &str) -> Result { + let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?; + match entry.delete_credential() { + Ok(()) => Ok(true), + Err(keyring::Error::NoEntry) => Ok(false), + Err(error) => Err(CredentialStoreError::new(error)), + } + } +} + +/// Wrap OAuthTokenResponse to allow for partial equality comparison. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WrappedOAuthTokenResponse(pub OAuthTokenResponse); + +impl PartialEq for WrappedOAuthTokenResponse { + fn eq(&self, other: &Self) -> bool { + match (serde_json::to_string(self), serde_json::to_string(other)) { + (Ok(s1), Ok(s2)) => s1 == s2, + _ => false, + } + } +} + +pub(crate) fn load_oauth_tokens(server_name: &str, url: &str) -> Result> { + let store = KeyringCredentialStore; + load_oauth_tokens_with_store(&store, server_name, url) +} + +fn load_oauth_tokens_with_store( + store: &C, + server_name: &str, + url: &str, +) -> Result> { + let key = compute_store_key(server_name, url)?; + match store.load(KEYRING_SERVICE, &key) { + Ok(Some(serialized)) => { + let tokens: StoredOAuthTokens = serde_json::from_str(&serialized) + .context("failed to deserialize OAuth tokens from keyring")?; + Ok(Some(tokens)) + } + Ok(None) => load_oauth_tokens_from_file(server_name, url), + Err(error) => { + let message = error.message(); + warn!("failed to read OAuth tokens from keyring: {message}"); + load_oauth_tokens_from_file(server_name, url) + .with_context(|| format!("failed to read OAuth tokens from keyring: {message}")) + } + } +} + +pub fn save_oauth_tokens(server_name: &str, tokens: &StoredOAuthTokens) -> Result<()> { + let store = KeyringCredentialStore; + save_oauth_tokens_with_store(&store, server_name, tokens) +} + +fn save_oauth_tokens_with_store( + store: &C, + server_name: &str, + tokens: &StoredOAuthTokens, +) -> Result<()> { + let serialized = serde_json::to_string(tokens).context("failed to serialize OAuth tokens")?; + + let key = compute_store_key(server_name, &tokens.url)?; + match store.save(KEYRING_SERVICE, &key, &serialized) { + Ok(()) => { + if let Err(error) = delete_oauth_tokens_from_file(&key) { + warn!("failed to remove OAuth tokens from fallback storage: {error:?}"); + } + Ok(()) + } + Err(error) => { + let message = error.message(); + warn!("failed to write OAuth tokens to keyring: {message}"); + save_oauth_tokens_to_file(tokens) + .with_context(|| format!("failed to write OAuth tokens to keyring: {message}")) + } + } +} + +pub fn delete_oauth_tokens(server_name: &str, url: &str) -> Result { + let store = KeyringCredentialStore; + delete_oauth_tokens_with_store(&store, server_name, url) +} + +fn delete_oauth_tokens_with_store( + store: &C, + server_name: &str, + url: &str, +) -> Result { + let key = compute_store_key(server_name, url)?; + let keyring_removed = match store.delete(KEYRING_SERVICE, &key) { + Ok(removed) => removed, + Err(error) => { + let message = error.message(); + warn!("failed to delete OAuth tokens from keyring: {message}"); + return Err(error.into_error()).context("failed to delete OAuth tokens from keyring"); + } + }; + + let file_removed = delete_oauth_tokens_from_file(&key)?; + Ok(keyring_removed || file_removed) +} + +#[derive(Clone)] +pub(crate) struct OAuthPersistor { + inner: Arc, +} + +struct OAuthPersistorInner { + server_name: String, + url: String, + authorization_manager: Arc>, + last_credentials: Mutex>, +} + +impl OAuthPersistor { + pub(crate) fn new( + server_name: String, + url: String, + manager: Arc>, + initial_credentials: Option, + ) -> Self { + Self { + inner: Arc::new(OAuthPersistorInner { + server_name, + url, + authorization_manager: manager, + last_credentials: Mutex::new(initial_credentials), + }), + } + } + + /// Persists the latest stored credentials if they have changed. + /// Deletes the credentials if they are no longer present. + pub(crate) async fn persist_if_needed(&self) -> Result<()> { + let (client_id, maybe_credentials) = { + let manager = self.inner.authorization_manager.clone(); + let guard = manager.lock().await; + guard.get_credentials().await + }?; + + match maybe_credentials { + Some(credentials) => { + let stored = StoredOAuthTokens { + server_name: self.inner.server_name.clone(), + url: self.inner.url.clone(), + client_id, + token_response: WrappedOAuthTokenResponse(credentials.clone()), + }; + let mut last_credentials = self.inner.last_credentials.lock().await; + if last_credentials.as_ref() != Some(&stored) { + save_oauth_tokens(&self.inner.server_name, &stored)?; + *last_credentials = Some(stored); + } + } + None => { + let mut last_serialized = self.inner.last_credentials.lock().await; + if last_serialized.take().is_some() + && let Err(error) = + delete_oauth_tokens(&self.inner.server_name, &self.inner.url) + { + warn!( + "failed to remove OAuth tokens for server {}: {error}", + self.inner.server_name + ); + } + } + } + + Ok(()) + } +} + +const FALLBACK_FILENAME: &str = ".credentials.json"; +const MCP_SERVER_TYPE: &str = "http"; + +type FallbackFile = BTreeMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct FallbackTokenEntry { + server_name: String, + server_url: String, + client_id: String, + access_token: String, + #[serde(default)] + expires_at: Option, + #[serde(default)] + refresh_token: Option, + #[serde(default)] + scopes: Vec, +} + +fn load_oauth_tokens_from_file(server_name: &str, url: &str) -> Result> { + let Some(store) = read_fallback_file()? else { + return Ok(None); + }; + + let key = compute_store_key(server_name, url)?; + + for entry in store.values() { + let entry_key = compute_store_key(&entry.server_name, &entry.server_url)?; + if entry_key != key { + continue; + } + + let mut token_response = OAuthTokenResponse::new( + AccessToken::new(entry.access_token.clone()), + BasicTokenType::Bearer, + EmptyExtraTokenFields {}, + ); + + if let Some(refresh) = entry.refresh_token.clone() { + token_response.set_refresh_token(Some(RefreshToken::new(refresh))); + } + + let scopes = entry.scopes.clone(); + if !scopes.is_empty() { + token_response.set_scopes(Some(scopes.into_iter().map(Scope::new).collect())); + } + + if let Some(expires_at) = entry.expires_at + && let Some(seconds) = expires_in_from_timestamp(expires_at) + { + let duration = Duration::from_secs(seconds); + token_response.set_expires_in(Some(&duration)); + } + + let stored = StoredOAuthTokens { + server_name: entry.server_name.clone(), + url: entry.server_url.clone(), + client_id: entry.client_id.clone(), + token_response: WrappedOAuthTokenResponse(token_response), + }; + + return Ok(Some(stored)); + } + + Ok(None) +} + +fn save_oauth_tokens_to_file(tokens: &StoredOAuthTokens) -> Result<()> { + let key = compute_store_key(&tokens.server_name, &tokens.url)?; + let mut store = read_fallback_file()?.unwrap_or_default(); + + let token_response = &tokens.token_response.0; + let refresh_token = token_response + .refresh_token() + .map(|token| token.secret().to_string()); + let scopes = token_response + .scopes() + .map(|s| s.iter().map(|s| s.to_string()).collect()) + .unwrap_or_default(); + let entry = FallbackTokenEntry { + server_name: tokens.server_name.clone(), + server_url: tokens.url.clone(), + client_id: tokens.client_id.clone(), + access_token: token_response.access_token().secret().to_string(), + expires_at: compute_expires_at_millis(token_response), + refresh_token, + scopes, + }; + + store.insert(key, entry); + write_fallback_file(&store) +} + +fn delete_oauth_tokens_from_file(key: &str) -> Result { + let mut store = match read_fallback_file()? { + Some(store) => store, + None => return Ok(false), + }; + + let removed = store.remove(key).is_some(); + + if removed { + write_fallback_file(&store)?; + } + + Ok(removed) +} + +fn compute_expires_at_millis(response: &OAuthTokenResponse) -> Option { + let expires_in = response.expires_in()?; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)); + let expiry = now.checked_add(expires_in)?; + let millis = expiry.as_millis(); + if millis > u128::from(u64::MAX) { + Some(u64::MAX) + } else { + Some(millis as u64) + } +} + +fn expires_in_from_timestamp(expires_at: u64) -> Option { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)); + let now_ms = now.as_millis() as u64; + + if expires_at <= now_ms { + None + } else { + Some((expires_at - now_ms) / 1000) + } +} + +fn compute_store_key(server_name: &str, server_url: &str) -> Result { + let mut payload = JsonMap::new(); + payload.insert( + "type".to_string(), + Value::String(MCP_SERVER_TYPE.to_string()), + ); + payload.insert("url".to_string(), Value::String(server_url.to_string())); + payload.insert("headers".to_string(), Value::Object(JsonMap::new())); + + let truncated = sha_256_prefix(&Value::Object(payload))?; + Ok(format!("{server_name}|{truncated}")) +} + +fn fallback_file_path() -> Result { + let mut path = find_codex_home()?; + path.push(FALLBACK_FILENAME); + Ok(path) +} + +fn read_fallback_file() -> Result> { + let path = fallback_file_path()?; + let contents = match fs::read_to_string(&path) { + Ok(contents) => contents, + Err(err) if err.kind() == ErrorKind::NotFound => return Ok(None), + Err(err) => { + return Err(err).context(format!( + "failed to read credentials file at {}", + path.display() + )); + } + }; + + match serde_json::from_str::(&contents) { + Ok(store) => Ok(Some(store)), + Err(e) => Err(e).context(format!( + "failed to parse credentials file at {}", + path.display() + )), + } +} + +fn write_fallback_file(store: &FallbackFile) -> Result<()> { + let path = fallback_file_path()?; + + if store.is_empty() { + if path.exists() { + fs::remove_file(path)?; + } + return Ok(()); + } + + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + + let serialized = serde_json::to_string(store)?; + fs::write(&path, serialized)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = fs::Permissions::from_mode(0o600); + fs::set_permissions(&path, perms)?; + } + + Ok(()) +} + +fn sha_256_prefix(value: &Value) -> Result { + let serialized = + serde_json::to_string(&value).context("failed to serialize MCP OAuth key payload")?; + let mut hasher = Sha256::new(); + hasher.update(serialized.as_bytes()); + let digest = hasher.finalize(); + let hex = format!("{digest:x}"); + let truncated = &hex[..16]; + Ok(truncated.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + use keyring::Error as KeyringError; + use keyring::credential::CredentialApi as _; + use keyring::mock::MockCredential; + use pretty_assertions::assert_eq; + use std::collections::HashMap; + use std::sync::Arc; + use std::sync::Mutex; + use std::sync::MutexGuard; + use std::sync::OnceLock; + use std::sync::PoisonError; + use tempfile::tempdir; + + #[derive(Default, Clone)] + struct MockCredentialStore { + credentials: Arc>>>, + } + + impl MockCredentialStore { + fn credential(&self, account: &str) -> Arc { + let mut guard = self.credentials.lock().unwrap(); + guard + .entry(account.to_string()) + .or_insert_with(|| Arc::new(MockCredential::default())) + .clone() + } + + fn saved_value(&self, account: &str) -> Option { + let credential = { + let guard = self.credentials.lock().unwrap(); + guard.get(account).cloned() + }?; + credential.get_password().ok() + } + + fn set_error(&self, account: &str, error: KeyringError) { + let credential = self.credential(account); + credential.set_error(error); + } + + fn contains(&self, account: &str) -> bool { + let guard = self.credentials.lock().unwrap(); + guard.contains_key(account) + } + } + + impl CredentialStore for MockCredentialStore { + fn load( + &self, + _service: &str, + account: &str, + ) -> Result, CredentialStoreError> { + let credential = { + let guard = self.credentials.lock().unwrap(); + guard.get(account).cloned() + }; + + let Some(credential) = credential else { + return Ok(None); + }; + + match credential.get_password() { + Ok(password) => Ok(Some(password)), + Err(KeyringError::NoEntry) => Ok(None), + Err(error) => Err(CredentialStoreError::new(error)), + } + } + + fn save( + &self, + _service: &str, + account: &str, + value: &str, + ) -> Result<(), CredentialStoreError> { + let credential = self.credential(account); + credential + .set_password(value) + .map_err(CredentialStoreError::new) + } + + fn delete(&self, _service: &str, account: &str) -> Result { + let credential = { + let guard = self.credentials.lock().unwrap(); + guard.get(account).cloned() + }; + + let Some(credential) = credential else { + return Ok(false); + }; + + match credential.delete_credential() { + Ok(()) => { + let mut guard = self.credentials.lock().unwrap(); + guard.remove(account); + Ok(true) + } + Err(KeyringError::NoEntry) => { + let mut guard = self.credentials.lock().unwrap(); + guard.remove(account); + Ok(false) + } + Err(error) => Err(CredentialStoreError::new(error)), + } + } + } + + struct TempCodexHome { + _guard: MutexGuard<'static, ()>, + _dir: tempfile::TempDir, + } + + impl TempCodexHome { + fn new() -> Self { + static LOCK: OnceLock> = OnceLock::new(); + let guard = LOCK + .get_or_init(Mutex::default) + .lock() + .unwrap_or_else(PoisonError::into_inner); + let dir = tempdir().expect("create CODEX_HOME temp dir"); + unsafe { + std::env::set_var("CODEX_HOME", dir.path()); + } + Self { + _guard: guard, + _dir: dir, + } + } + } + + impl Drop for TempCodexHome { + fn drop(&mut self) { + unsafe { + std::env::remove_var("CODEX_HOME"); + } + } + } + + #[test] + fn load_oauth_tokens_reads_from_keyring_when_available() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let expected = tokens.clone(); + let serialized = serde_json::to_string(&tokens)?; + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + store.save(KEYRING_SERVICE, &key, &serialized)?; + + let loaded = super::load_oauth_tokens_with_store(&store, &tokens.server_name, &tokens.url)?; + assert_eq!(loaded, Some(expected)); + Ok(()) + } + + #[test] + fn load_oauth_tokens_falls_back_when_missing_in_keyring() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let expected = tokens.clone(); + + super::save_oauth_tokens_to_file(&tokens)?; + + let loaded = super::load_oauth_tokens_with_store(&store, &tokens.server_name, &tokens.url)? + .expect("tokens should load from fallback"); + assert_tokens_match_without_expiry(&loaded, &expected); + Ok(()) + } + + #[test] + fn load_oauth_tokens_falls_back_when_keyring_errors() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let expected = tokens.clone(); + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + store.set_error(&key, KeyringError::Invalid("error".into(), "load".into())); + + super::save_oauth_tokens_to_file(&tokens)?; + + let loaded = super::load_oauth_tokens_with_store(&store, &tokens.server_name, &tokens.url)? + .expect("tokens should load from fallback"); + assert_tokens_match_without_expiry(&loaded, &expected); + Ok(()) + } + + #[test] + fn save_oauth_tokens_prefers_keyring_when_available() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + + super::save_oauth_tokens_to_file(&tokens)?; + + super::save_oauth_tokens_with_store(&store, &tokens.server_name, &tokens)?; + + let fallback_path = super::fallback_file_path()?; + assert!(!fallback_path.exists(), "fallback file should be removed"); + let stored = store.saved_value(&key).expect("value saved to keyring"); + assert_eq!(serde_json::from_str::(&stored)?, tokens); + Ok(()) + } + + #[test] + fn save_oauth_tokens_writes_fallback_when_keyring_fails() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + store.set_error(&key, KeyringError::Invalid("error".into(), "save".into())); + + super::save_oauth_tokens_with_store(&store, &tokens.server_name, &tokens)?; + + let fallback_path = super::fallback_file_path()?; + assert!(fallback_path.exists(), "fallback file should be created"); + let saved = super::read_fallback_file()?.expect("fallback file should load"); + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + let entry = saved.get(&key).expect("entry for key"); + assert_eq!(entry.server_name, tokens.server_name); + assert_eq!(entry.server_url, tokens.url); + assert_eq!(entry.client_id, tokens.client_id); + assert_eq!( + entry.access_token, + tokens.token_response.0.access_token().secret().as_str() + ); + assert!(store.saved_value(&key).is_none()); + Ok(()) + } + + #[test] + fn delete_oauth_tokens_removes_all_storage() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let serialized = serde_json::to_string(&tokens)?; + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + store.save(KEYRING_SERVICE, &key, &serialized)?; + super::save_oauth_tokens_to_file(&tokens)?; + + let removed = + super::delete_oauth_tokens_with_store(&store, &tokens.server_name, &tokens.url)?; + assert!(removed); + assert!(!store.contains(&key)); + assert!(!super::fallback_file_path()?.exists()); + Ok(()) + } + + #[test] + fn delete_oauth_tokens_propagates_keyring_errors() -> Result<()> { + let _env = TempCodexHome::new(); + let store = MockCredentialStore::default(); + let tokens = sample_tokens(); + let key = super::compute_store_key(&tokens.server_name, &tokens.url)?; + store.set_error(&key, KeyringError::Invalid("error".into(), "delete".into())); + super::save_oauth_tokens_to_file(&tokens).unwrap(); + + let result = + super::delete_oauth_tokens_with_store(&store, &tokens.server_name, &tokens.url); + assert!(result.is_err()); + assert!(super::fallback_file_path().unwrap().exists()); + Ok(()) + } + + fn assert_tokens_match_without_expiry( + actual: &StoredOAuthTokens, + expected: &StoredOAuthTokens, + ) { + assert_eq!(actual.server_name, expected.server_name); + assert_eq!(actual.url, expected.url); + assert_eq!(actual.client_id, expected.client_id); + assert_token_response_match_without_expiry( + &actual.token_response, + &expected.token_response, + ); + } + + fn assert_token_response_match_without_expiry( + actual: &WrappedOAuthTokenResponse, + expected: &WrappedOAuthTokenResponse, + ) { + let actual_response = &actual.0; + let expected_response = &expected.0; + + assert_eq!( + actual_response.access_token().secret(), + expected_response.access_token().secret() + ); + assert_eq!(actual_response.token_type(), expected_response.token_type()); + assert_eq!( + actual_response.refresh_token().map(RefreshToken::secret), + expected_response.refresh_token().map(RefreshToken::secret), + ); + assert_eq!(actual_response.scopes(), expected_response.scopes()); + assert_eq!( + actual_response.extra_fields(), + expected_response.extra_fields() + ); + assert_eq!( + actual_response.expires_in().is_some(), + expected_response.expires_in().is_some() + ); + } + + fn sample_tokens() -> StoredOAuthTokens { + let mut response = OAuthTokenResponse::new( + AccessToken::new("access-token".to_string()), + BasicTokenType::Bearer, + EmptyExtraTokenFields {}, + ); + response.set_refresh_token(Some(RefreshToken::new("refresh-token".to_string()))); + response.set_scopes(Some(vec![ + Scope::new("scope-a".to_string()), + Scope::new("scope-b".to_string()), + ])); + let expires_in = Duration::from_secs(3600); + response.set_expires_in(Some(&expires_in)); + + StoredOAuthTokens { + server_name: "test-server".to_string(), + url: "https://example.test".to_string(), + client_id: "client-id".to_string(), + token_response: WrappedOAuthTokenResponse(response), + } + } +} diff --git a/codex-rs/rmcp-client/src/perform_oauth_login.rs b/codex-rs/rmcp-client/src/perform_oauth_login.rs new file mode 100644 index 00000000..0fcdb2f6 --- /dev/null +++ b/codex-rs/rmcp-client/src/perform_oauth_login.rs @@ -0,0 +1,141 @@ +use std::string::String; +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use anyhow::anyhow; +use rmcp::transport::auth::OAuthState; +use tiny_http::Response; +use tiny_http::Server; +use tokio::sync::oneshot; +use tokio::time::timeout; +use urlencoding::decode; + +use crate::StoredOAuthTokens; +use crate::WrappedOAuthTokenResponse; +use crate::save_oauth_tokens; + +struct CallbackServerGuard { + server: Arc, +} + +impl Drop for CallbackServerGuard { + fn drop(&mut self) { + self.server.unblock(); + } +} + +pub async fn perform_oauth_login(server_name: &str, server_url: &str) -> Result<()> { + let server = Arc::new(Server::http("127.0.0.1:0").map_err(|err| anyhow!(err))?); + let guard = CallbackServerGuard { + server: Arc::clone(&server), + }; + + let redirect_uri = match server.server_addr() { + tiny_http::ListenAddr::IP(std::net::SocketAddr::V4(addr)) => { + format!("http://{}:{}/callback", addr.ip(), addr.port()) + } + tiny_http::ListenAddr::IP(std::net::SocketAddr::V6(addr)) => { + format!("http://[{}]:{}/callback", addr.ip(), addr.port()) + } + #[cfg(not(target_os = "windows"))] + _ => return Err(anyhow!("unable to determine callback address")), + }; + + let (tx, rx) = oneshot::channel(); + spawn_callback_server(server, tx); + + let mut oauth_state = OAuthState::new(server_url, None).await?; + oauth_state.start_authorization(&[], &redirect_uri).await?; + let auth_url = oauth_state.get_authorization_url().await?; + + println!("Authorize `{server_name}` by opening this URL in your browser:\n{auth_url}\n"); + + if webbrowser::open(&auth_url).is_err() { + println!("(Browser launch failed; please copy the URL above manually.)"); + } + + let (code, csrf_state) = timeout(Duration::from_secs(300), rx) + .await + .context("timed out waiting for OAuth callback")? + .context("OAuth callback was cancelled")?; + + oauth_state + .handle_callback(&code, &csrf_state) + .await + .context("failed to handle OAuth callback")?; + + let (client_id, credentials_opt) = oauth_state + .get_credentials() + .await + .context("failed to retrieve OAuth credentials")?; + let credentials = + credentials_opt.ok_or_else(|| anyhow!("OAuth provider did not return credentials"))?; + + let stored = StoredOAuthTokens { + server_name: server_name.to_string(), + url: server_url.to_string(), + client_id, + token_response: WrappedOAuthTokenResponse(credentials), + }; + save_oauth_tokens(server_name, &stored)?; + + drop(guard); + Ok(()) +} + +fn spawn_callback_server(server: Arc, tx: oneshot::Sender<(String, String)>) { + tokio::task::spawn_blocking(move || { + while let Ok(request) = server.recv() { + let path = request.url().to_string(); + if let Some(OauthCallbackResult { code, state }) = parse_oauth_callback(&path) { + let response = + Response::from_string("Authentication complete. You may close this window."); + if let Err(err) = request.respond(response) { + eprintln!("Failed to respond to OAuth callback: {err}"); + } + if let Err(err) = tx.send((code, state)) { + eprintln!("Failed to send OAuth callback: {err:?}"); + } + break; + } else { + let response = + Response::from_string("Invalid OAuth callback").with_status_code(400); + if let Err(err) = request.respond(response) { + eprintln!("Failed to respond to OAuth callback: {err}"); + } + } + } + }); +} + +struct OauthCallbackResult { + code: String, + state: String, +} + +fn parse_oauth_callback(path: &str) -> Option { + let (route, query) = path.split_once('?')?; + if route != "/callback" { + return None; + } + + let mut code = None; + let mut state = None; + + for pair in query.split('&') { + let (key, value) = pair.split_once('=')?; + let decoded = decode(value).ok()?.into_owned(); + match key { + "code" => code = Some(decoded), + "state" => state = Some(decoded), + _ => {} + } + } + + Some(OauthCallbackResult { + code: code?, + state: state?, + }) +} diff --git a/codex-rs/rmcp-client/src/rmcp_client.rs b/codex-rs/rmcp-client/src/rmcp_client.rs index e1270294..d19bf8fe 100644 --- a/codex-rs/rmcp-client/src/rmcp_client.rs +++ b/codex-rs/rmcp-client/src/rmcp_client.rs @@ -21,6 +21,8 @@ use rmcp::service::RoleClient; use rmcp::service::RunningService; use rmcp::service::{self}; use rmcp::transport::StreamableHttpClientTransport; +use rmcp::transport::auth::AuthClient; +use rmcp::transport::auth::OAuthState; use rmcp::transport::child_process::TokioChildProcess; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; use tokio::io::AsyncBufReadExt; @@ -31,7 +33,10 @@ use tokio::time; use tracing::info; use tracing::warn; +use crate::load_oauth_tokens; use crate::logging_client_handler::LoggingClientHandler; +use crate::oauth::OAuthPersistor; +use crate::oauth::StoredOAuthTokens; use crate::utils::convert_call_tool_result; use crate::utils::convert_to_mcp; use crate::utils::convert_to_rmcp; @@ -40,7 +45,13 @@ use crate::utils::run_with_timeout; enum PendingTransport { ChildProcess(TokioChildProcess), - StreamableHttp(StreamableHttpClientTransport), + StreamableHttp { + transport: StreamableHttpClientTransport, + }, + StreamableHttpWithOAuth { + transport: StreamableHttpClientTransport>, + oauth_persistor: OAuthPersistor, + }, } enum ClientState { @@ -49,6 +60,7 @@ enum ClientState { }, Ready { service: Arc>, + oauth: Option, }, } @@ -103,17 +115,37 @@ impl RmcpClient { }) } - pub fn new_streamable_http_client(url: String, bearer_token: Option) -> Result { - let mut config = StreamableHttpClientTransportConfig::with_uri(url); - if let Some(token) = bearer_token { - config = config.auth_header(format!("Bearer {token}")); - } - - let transport = StreamableHttpClientTransport::from_config(config); + pub async fn new_streamable_http_client( + server_name: &str, + url: &str, + bearer_token: Option, + ) -> Result { + let initial_tokens = match load_oauth_tokens(server_name, url) { + Ok(tokens) => tokens, + Err(err) => { + warn!("failed to read tokens for server `{server_name}`: {err}"); + None + } + }; + let transport = if let Some(initial_tokens) = initial_tokens.clone() { + let (transport, oauth_persistor) = + create_oauth_transport_and_runtime(server_name, url, initial_tokens).await?; + PendingTransport::StreamableHttpWithOAuth { + transport, + oauth_persistor, + } + } else { + let mut http_config = StreamableHttpClientTransportConfig::with_uri(url.to_string()); + if let Some(bearer_token) = bearer_token { + http_config = http_config.auth_header(format!("Bearer {bearer_token}")); + } + let transport = StreamableHttpClientTransport::from_config(http_config); + PendingTransport::StreamableHttp { transport } + }; Ok(Self { state: Mutex::new(ClientState::Connecting { - transport: Some(PendingTransport::StreamableHttp(transport)), + transport: Some(transport), }), }) } @@ -125,35 +157,40 @@ impl RmcpClient { params: InitializeRequestParams, timeout: Option, ) -> Result { - let transport = { + let rmcp_params: InitializeRequestParam = convert_to_rmcp(params.clone())?; + let client_handler = LoggingClientHandler::new(rmcp_params); + + let (transport, oauth_persistor) = { let mut guard = self.state.lock().await; match &mut *guard { - ClientState::Connecting { transport } => transport - .take() - .ok_or_else(|| anyhow!("client already initializing"))?, - ClientState::Ready { .. } => { - return Err(anyhow!("client already initialized")); - } - } - }; - - let client_info = convert_to_rmcp::<_, InitializeRequestParam>(params.clone())?; - let client_handler = LoggingClientHandler::new(client_info); - let service_future = match transport { - PendingTransport::ChildProcess(transport) => { - service::serve_client(client_handler.clone(), transport).boxed() - } - PendingTransport::StreamableHttp(transport) => { - service::serve_client(client_handler, transport).boxed() + ClientState::Connecting { transport } => match transport.take() { + Some(PendingTransport::ChildProcess(transport)) => ( + service::serve_client(client_handler.clone(), transport).boxed(), + None, + ), + Some(PendingTransport::StreamableHttp { transport }) => ( + service::serve_client(client_handler.clone(), transport).boxed(), + None, + ), + Some(PendingTransport::StreamableHttpWithOAuth { + transport, + oauth_persistor, + }) => ( + service::serve_client(client_handler.clone(), transport).boxed(), + Some(oauth_persistor), + ), + None => return Err(anyhow!("client already initializing")), + }, + ClientState::Ready { .. } => return Err(anyhow!("client already initialized")), } }; let service = match timeout { - Some(duration) => time::timeout(duration, service_future) + Some(duration) => time::timeout(duration, transport) .await .map_err(|_| anyhow!("timed out handshaking with MCP server after {duration:?}"))? .map_err(|err| anyhow!("handshaking with MCP server failed: {err}"))?, - None => service_future + None => transport .await .map_err(|err| anyhow!("handshaking with MCP server failed: {err}"))?, }; @@ -168,9 +205,16 @@ impl RmcpClient { let mut guard = self.state.lock().await; *guard = ClientState::Ready { service: Arc::new(service), + oauth: oauth_persistor.clone(), }; } + if let Some(runtime) = oauth_persistor + && let Err(error) = runtime.persist_if_needed().await + { + warn!("failed to persist OAuth tokens after initialize: {error}"); + } + Ok(initialize_result) } @@ -186,7 +230,9 @@ impl RmcpClient { let fut = service.list_tools(rmcp_params); let result = run_with_timeout(fut, timeout, "tools/list").await?; - convert_to_mcp(result) + let converted = convert_to_mcp(result)?; + self.persist_oauth_tokens().await; + Ok(converted) } pub async fn call_tool( @@ -200,14 +246,79 @@ impl RmcpClient { let rmcp_params: CallToolRequestParam = convert_to_rmcp(params)?; let fut = service.call_tool(rmcp_params); let rmcp_result = run_with_timeout(fut, timeout, "tools/call").await?; - convert_call_tool_result(rmcp_result) + let converted = convert_call_tool_result(rmcp_result)?; + self.persist_oauth_tokens().await; + Ok(converted) } async fn service(&self) -> Result>> { let guard = self.state.lock().await; match &*guard { - ClientState::Ready { service } => Ok(Arc::clone(service)), + ClientState::Ready { service, .. } => Ok(Arc::clone(service)), ClientState::Connecting { .. } => Err(anyhow!("MCP client not initialized")), } } + + async fn oauth_persistor(&self) -> Option { + let guard = self.state.lock().await; + match &*guard { + ClientState::Ready { + oauth: Some(runtime), + service: _, + } => Some(runtime.clone()), + _ => None, + } + } + + async fn persist_oauth_tokens(&self) { + if let Some(runtime) = self.oauth_persistor().await + && let Err(error) = runtime.persist_if_needed().await + { + warn!("failed to persist OAuth tokens: {error}"); + } + } +} + +async fn create_oauth_transport_and_runtime( + server_name: &str, + url: &str, + initial_tokens: StoredOAuthTokens, +) -> Result<( + StreamableHttpClientTransport>, + OAuthPersistor, +)> { + let http_client = reqwest::Client::builder().build()?; + let mut oauth_state = OAuthState::new(url.to_string(), Some(http_client.clone())).await?; + + oauth_state + .set_credentials( + &initial_tokens.client_id, + initial_tokens.token_response.0.clone(), + ) + .await?; + + let manager = match oauth_state { + OAuthState::Authorized(manager) => manager, + OAuthState::Unauthorized(manager) => manager, + OAuthState::Session(_) | OAuthState::AuthorizedHttpClient(_) => { + return Err(anyhow!("unexpected OAuth state during client setup")); + } + }; + + let auth_client = AuthClient::new(http_client, manager); + let auth_manager = auth_client.auth_manager.clone(); + + let transport = StreamableHttpClientTransport::with_client( + auth_client, + StreamableHttpClientTransportConfig::with_uri(url.to_string()), + ); + + let runtime = OAuthPersistor::new( + server_name.to_string(), + url.to_string(), + auth_manager, + Some(initial_tokens), + ); + + Ok((transport, runtime)) } diff --git a/docs/config.md b/docs/config.md index 9c4e6575..f359c8ca 100644 --- a/docs/config.md +++ b/docs/config.md @@ -369,6 +369,8 @@ url = "http://127.0.0.1:3845/mcp" bearer_token = "" ``` +Refer to the MCP CLI commands for oauth login + ### Other configuration options ```toml @@ -404,6 +406,12 @@ codex mcp get docs --json # Remove a server codex mcp remove docs + +# Log in to a streamable HTTP server that supports oauth +codex mcp login SERVER_NAME + +# Log out from a streamable HTTP server that supports oauth +codex mcp logout SERVER_NAME ``` ## shell_environment_policy