diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index f144c4496..38f015792 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -884,6 +884,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "codex-app-server-test-client" +version = "0.0.0" +dependencies = [ + "anyhow", + "clap", + "codex-app-server-protocol", + "codex-protocol", + "serde", + "serde_json", + "uuid", +] + [[package]] name = "codex-apply-patch" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 80bae6550..3460faf5d 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -5,6 +5,7 @@ members = [ "async-utils", "app-server", "app-server-protocol", + "app-server-test-client", "apply-patch", "arg0", "feedback", diff --git a/codex-rs/app-server-test-client/Cargo.lock b/codex-rs/app-server-test-client/Cargo.lock new file mode 100644 index 000000000..1720850cd --- /dev/null +++ b/codex-rs/app-server-test-client/Cargo.lock @@ -0,0 +1,1298 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "codex-app-server-protocol" +version = "0.45.0" +source = "git+https://github.com/openai/codex.git?tag=rust-v0.45.0#a7c7869c23f88f6c468281e6f438ba4a91b81f26" +dependencies = [ + "codex-protocol", + "paste", + "serde", + "serde_json", + "strum_macros", + "ts-rs", + "uuid", +] + +[[package]] +name = "codex-protocol" +version = "0.45.0" +source = "git+https://github.com/openai/codex.git?tag=rust-v0.45.0#a7c7869c23f88f6c468281e6f438ba4a91b81f26" +dependencies = [ + "base64", + "icu_decimal", + "icu_locale_core", + "mcp-types", + "mime_guess", + "serde", + "serde_json", + "serde_with", + "strum", + "strum_macros", + "sys-locale", + "tracing", + "ts-rs", + "uuid", +] + +[[package]] +name = "codex_toy_app_server" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "codex-app-server-protocol", + "codex-protocol", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + +[[package]] +name = "fixed_decimal" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35943d22b2f19c0cb198ecf915910a8158e94541c89dcc63300d7799d46c2c5e" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_decimal" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec61c43fdc4e368a9f450272833123a8ef0d7083a44597660ce94d791b8a2e2" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_decimal_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_decimal_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b70963bc35f9bdf1bc66a5c1f458f4991c1dc71760e00fa06016b2c76b2738d5" + +[[package]] +name = "icu_locale" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ae5921528335e91da1b6c695dbf1ec37df5ac13faa3f91e5640be93aa2fbefd" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fdef0c124749d06a743c69e938350816554eb63ac979166590e2b4ee4252765" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "mcp-types" +version = "0.45.0" +source = "git+https://github.com/openai/codex.git?tag=rust-v0.45.0#a7c7869c23f88f6c468281e6f438ba4a91b81f26" +dependencies = [ + "serde", + "serde_json", + "ts-rs", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "potential_utf" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +dependencies = [ + "serde", + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6093cd8c01b25262b84927e0f7151692158fab02d961e04c979d3903eba7ecc5" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.11.4", + "schemars 0.9.0", + "schemars 1.0.4", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e6c180db0816026a61afa1cff5344fb7ebded7e4d3062772179f2501481c27" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ts-rs" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" +dependencies = [ + "serde_json", + "thiserror", + "ts-rs-macros", + "uuid", +] + +[[package]] +name = "ts-rs-macros" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "termcolor", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "wasi" +version = "0.14.7+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +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", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/codex-rs/app-server-test-client/Cargo.toml b/codex-rs/app-server-test-client/Cargo.toml new file mode 100644 index 000000000..2fd14fb15 --- /dev/null +++ b/codex-rs/app-server-test-client/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "codex-app-server-test-client" +version = { workspace = true } +edition = "2024" + +[lints] +workspace = true + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive", "env"] } +codex-app-server-protocol = { workspace = true } +codex-protocol = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +uuid = { workspace = true, features = ["v4"] } diff --git a/codex-rs/app-server-test-client/README.md b/codex-rs/app-server-test-client/README.md new file mode 100644 index 000000000..ee75348c9 --- /dev/null +++ b/codex-rs/app-server-test-client/README.md @@ -0,0 +1,2 @@ +# App Server Test Client +Exercises simple `codex app-server` flows end-to-end, logging JSON-RPC messages sent between client and server to stdout. diff --git a/codex-rs/app-server-test-client/src/main.rs b/codex-rs/app-server-test-client/src/main.rs new file mode 100644 index 000000000..a243937b2 --- /dev/null +++ b/codex-rs/app-server-test-client/src/main.rs @@ -0,0 +1,691 @@ +use std::collections::VecDeque; +use std::io::BufRead; +use std::io::BufReader; +use std::io::Write; +use std::process::Child; +use std::process::ChildStdin; +use std::process::ChildStdout; +use std::process::Command; +use std::process::Stdio; +use std::thread; +use std::time::Duration; + +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use clap::Parser; +use clap::Subcommand; +use codex_app_server_protocol::AddConversationListenerParams; +use codex_app_server_protocol::AddConversationSubscriptionResponse; +use codex_app_server_protocol::AskForApproval; +use codex_app_server_protocol::ClientInfo; +use codex_app_server_protocol::ClientRequest; +use codex_app_server_protocol::GetAccountRateLimitsResponse; +use codex_app_server_protocol::InitializeParams; +use codex_app_server_protocol::InitializeResponse; +use codex_app_server_protocol::InputItem; +use codex_app_server_protocol::JSONRPCMessage; +use codex_app_server_protocol::JSONRPCNotification; +use codex_app_server_protocol::JSONRPCResponse; +use codex_app_server_protocol::LoginChatGptCompleteNotification; +use codex_app_server_protocol::LoginChatGptResponse; +use codex_app_server_protocol::NewConversationParams; +use codex_app_server_protocol::NewConversationResponse; +use codex_app_server_protocol::RequestId; +use codex_app_server_protocol::SandboxPolicy; +use codex_app_server_protocol::SendUserMessageParams; +use codex_app_server_protocol::SendUserMessageResponse; +use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::ThreadStartParams; +use codex_app_server_protocol::ThreadStartResponse; +use codex_app_server_protocol::TurnStartParams; +use codex_app_server_protocol::TurnStartResponse; +use codex_app_server_protocol::UserInput as V2UserInput; +use codex_protocol::ConversationId; +use codex_protocol::protocol::Event; +use codex_protocol::protocol::EventMsg; +use serde::de::DeserializeOwned; +use serde_json::Value; +use uuid::Uuid; + +/// Minimal launcher that initializes the Codex app-server and logs the handshake. +#[derive(Parser)] +#[command(author = "Codex", version, about = "Bootstrap Codex app-server", long_about = None)] +struct Cli { + /// Path to the `codex` CLI binary. + #[arg(long, env = "CODEX_BIN", default_value = "codex")] + codex_bin: String, + + #[command(subcommand)] + command: CliCommand, +} + +#[derive(Subcommand)] +enum CliCommand { + /// Send a user message through the Codex app-server. + SendMessage { + /// User message to send to Codex. + #[arg()] + user_message: String, + }, + /// Send a user message through the app-server V2 thread/turn APIs. + SendMessageV2 { + /// User message to send to Codex. + #[arg()] + user_message: String, + }, + /// Start a V2 turn that elicits an ExecCommand approval. + #[command(name = "trigger-cmd-approval")] + TriggerCmdApproval { + /// Optional prompt; defaults to a simple python command. + #[arg()] + user_message: Option, + }, + /// Start a V2 turn that elicits an ApplyPatch approval. + #[command(name = "trigger-patch-approval")] + TriggerPatchApproval { + /// Optional prompt; defaults to creating a file via apply_patch. + #[arg()] + user_message: Option, + }, + /// Start a V2 turn that should not elicit an ExecCommand approval. + #[command(name = "no-trigger-cmd-approval")] + NoTriggerCmdApproval, + /// Trigger the ChatGPT login flow and wait for completion. + TestLogin, + /// Fetch the current account rate limits from the Codex app-server. + GetAccountRateLimits, +} + +fn main() -> Result<()> { + let Cli { codex_bin, command } = Cli::parse(); + + match command { + CliCommand::SendMessage { user_message } => send_message(codex_bin, user_message), + CliCommand::SendMessageV2 { user_message } => send_message_v2(codex_bin, user_message), + CliCommand::TriggerCmdApproval { user_message } => { + trigger_cmd_approval(codex_bin, user_message) + } + CliCommand::TriggerPatchApproval { user_message } => { + trigger_patch_approval(codex_bin, user_message) + } + CliCommand::NoTriggerCmdApproval => no_trigger_cmd_approval(codex_bin), + CliCommand::TestLogin => test_login(codex_bin), + CliCommand::GetAccountRateLimits => get_account_rate_limits(codex_bin), + } +} + +fn send_message(codex_bin: String, user_message: String) -> Result<()> { + let mut client = CodexClient::spawn(codex_bin)?; + + let initialize = client.initialize()?; + println!("< initialize response: {initialize:?}"); + + let conversation = client.new_conversation()?; + println!("< newConversation response: {conversation:?}"); + + let subscription = client.add_conversation_listener(&conversation.conversation_id)?; + println!("< addConversationListener response: {subscription:?}"); + + let send_response = client.send_user_message(&conversation.conversation_id, &user_message)?; + println!("< sendUserMessage response: {send_response:?}"); + + client.stream_conversation(&conversation.conversation_id)?; + + client.remove_conversation_listener(subscription.subscription_id)?; + + Ok(()) +} + +fn send_message_v2(codex_bin: String, user_message: String) -> Result<()> { + send_message_v2_with_policies(codex_bin, user_message, None, None) +} + +fn trigger_cmd_approval(codex_bin: String, user_message: Option) -> Result<()> { + let default_prompt = + "Run `touch /tmp/should-trigger-approval` so I can confirm the file exists."; + let message = user_message.unwrap_or_else(|| default_prompt.to_string()); + send_message_v2_with_policies( + codex_bin, + message, + Some(AskForApproval::OnRequest), + Some(SandboxPolicy::ReadOnly), + ) +} + +fn trigger_patch_approval(codex_bin: String, user_message: Option) -> Result<()> { + let default_prompt = + "Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch."; + let message = user_message.unwrap_or_else(|| default_prompt.to_string()); + send_message_v2_with_policies( + codex_bin, + message, + Some(AskForApproval::OnRequest), + Some(SandboxPolicy::ReadOnly), + ) +} + +fn no_trigger_cmd_approval(codex_bin: String) -> Result<()> { + let prompt = "Run `touch should_not_trigger_approval.txt`"; + send_message_v2_with_policies(codex_bin, prompt.to_string(), None, None) +} + +fn send_message_v2_with_policies( + codex_bin: String, + user_message: String, + approval_policy: Option, + sandbox_policy: Option, +) -> Result<()> { + let mut client = CodexClient::spawn(codex_bin)?; + + let initialize = client.initialize()?; + println!("< initialize response: {initialize:?}"); + + let thread_response = client.thread_start(ThreadStartParams::default())?; + println!("< thread/start response: {thread_response:?}"); + let mut turn_params = TurnStartParams { + thread_id: thread_response.thread.id.clone(), + input: vec![V2UserInput::Text { text: user_message }], + ..Default::default() + }; + turn_params.approval_policy = approval_policy; + turn_params.sandbox_policy = sandbox_policy; + + let turn_response = client.turn_start(turn_params)?; + println!("< turn/start response: {turn_response:?}"); + + client.stream_turn(&thread_response.thread.id, &turn_response.turn.id)?; + + Ok(()) +} + +fn test_login(codex_bin: String) -> Result<()> { + let mut client = CodexClient::spawn(codex_bin)?; + + let initialize = client.initialize()?; + println!("< initialize response: {initialize:?}"); + + let login_response = client.login_chat_gpt()?; + println!("< loginChatGpt response: {login_response:?}"); + println!( + "Open the following URL in your browser to continue:\n{}", + login_response.auth_url + ); + + let completion = client.wait_for_login_completion(&login_response.login_id)?; + println!("< loginChatGptComplete notification: {completion:?}"); + + if completion.success { + println!("Login succeeded."); + Ok(()) + } else { + bail!( + "login failed: {}", + completion + .error + .as_deref() + .unwrap_or("unknown error from loginChatGptComplete") + ); + } +} + +fn get_account_rate_limits(codex_bin: String) -> Result<()> { + let mut client = CodexClient::spawn(codex_bin)?; + + let initialize = client.initialize()?; + println!("< initialize response: {initialize:?}"); + + let response = client.get_account_rate_limits()?; + println!("< account/rateLimits/read response: {response:?}"); + + Ok(()) +} + +struct CodexClient { + child: Child, + stdin: Option, + stdout: BufReader, + pending_notifications: VecDeque, +} + +impl CodexClient { + fn spawn(codex_bin: String) -> Result { + let mut codex_app_server = Command::new(&codex_bin) + .arg("app-server") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .with_context(|| format!("failed to start `{codex_bin}` app-server"))?; + + let stdin = codex_app_server + .stdin + .take() + .context("codex app-server stdin unavailable")?; + let stdout = codex_app_server + .stdout + .take() + .context("codex app-server stdout unavailable")?; + + Ok(Self { + child: codex_app_server, + stdin: Some(stdin), + stdout: BufReader::new(stdout), + pending_notifications: VecDeque::new(), + }) + } + + fn initialize(&mut self) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::Initialize { + request_id: request_id.clone(), + params: InitializeParams { + client_info: ClientInfo { + name: "codex-toy-app-server".to_string(), + title: Some("Codex Toy App Server".to_string()), + version: env!("CARGO_PKG_VERSION").to_string(), + }, + }, + }; + + self.send_request(request, request_id, "initialize") + } + + fn new_conversation(&mut self) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::NewConversation { + request_id: request_id.clone(), + params: NewConversationParams::default(), + }; + + self.send_request(request, request_id, "newConversation") + } + + fn add_conversation_listener( + &mut self, + conversation_id: &ConversationId, + ) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::AddConversationListener { + request_id: request_id.clone(), + params: AddConversationListenerParams { + conversation_id: *conversation_id, + experimental_raw_events: false, + }, + }; + + self.send_request(request, request_id, "addConversationListener") + } + + fn remove_conversation_listener(&mut self, subscription_id: Uuid) -> Result<()> { + let request_id = self.request_id(); + let request = ClientRequest::RemoveConversationListener { + request_id: request_id.clone(), + params: codex_app_server_protocol::RemoveConversationListenerParams { subscription_id }, + }; + + self.send_request::( + request, + request_id, + "removeConversationListener", + )?; + + Ok(()) + } + + fn send_user_message( + &mut self, + conversation_id: &ConversationId, + message: &str, + ) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::SendUserMessage { + request_id: request_id.clone(), + params: SendUserMessageParams { + conversation_id: *conversation_id, + items: vec![InputItem::Text { + text: message.to_string(), + }], + }, + }; + + self.send_request(request, request_id, "sendUserMessage") + } + + fn thread_start(&mut self, params: ThreadStartParams) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::ThreadStart { + request_id: request_id.clone(), + params, + }; + + self.send_request(request, request_id, "thread/start") + } + + fn turn_start(&mut self, params: TurnStartParams) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::TurnStart { + request_id: request_id.clone(), + params, + }; + + self.send_request(request, request_id, "turn/start") + } + + fn login_chat_gpt(&mut self) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::LoginChatGpt { + request_id: request_id.clone(), + params: None, + }; + + self.send_request(request, request_id, "loginChatGpt") + } + + fn get_account_rate_limits(&mut self) -> Result { + let request_id = self.request_id(); + let request = ClientRequest::GetAccountRateLimits { + request_id: request_id.clone(), + params: None, + }; + + self.send_request(request, request_id, "account/rateLimits/read") + } + + fn stream_conversation(&mut self, conversation_id: &ConversationId) -> Result<()> { + loop { + let notification = self.next_notification()?; + + if !notification.method.starts_with("codex/event/") { + continue; + } + + if let Some(event) = self.extract_event(notification, conversation_id)? { + match &event.msg { + EventMsg::AgentMessage(event) => { + println!("{}", event.message); + } + EventMsg::AgentMessageDelta(event) => { + print!("{}", event.delta); + std::io::stdout().flush().ok(); + } + EventMsg::TaskComplete(event) => { + println!("\n[task complete: {event:?}]"); + break; + } + EventMsg::TurnAborted(event) => { + println!("\n[turn aborted: {:?}]", event.reason); + break; + } + EventMsg::Error(event) => { + println!("[error] {event:?}"); + } + _ => { + println!("[UNKNOWN EVENT] {:?}", event.msg); + } + } + } + } + + Ok(()) + } + + fn wait_for_login_completion( + &mut self, + expected_login_id: &Uuid, + ) -> Result { + loop { + let notification = self.next_notification()?; + + if let Ok(server_notification) = ServerNotification::try_from(notification) { + match server_notification { + ServerNotification::LoginChatGptComplete(completion) => { + if &completion.login_id == expected_login_id { + return Ok(completion); + } + + println!( + "[ignoring loginChatGptComplete for unexpected login_id: {}]", + completion.login_id + ); + } + ServerNotification::AuthStatusChange(status) => { + println!("< authStatusChange notification: {status:?}"); + } + ServerNotification::AccountRateLimitsUpdated(snapshot) => { + println!("< accountRateLimitsUpdated notification: {snapshot:?}"); + } + ServerNotification::SessionConfigured(_) => { + // SessionConfigured notifications are unrelated to login; skip. + } + _ => {} + } + } + + // Not a server notification (likely a conversation event); keep waiting. + } + } + + fn stream_turn(&mut self, thread_id: &str, turn_id: &str) -> Result<()> { + loop { + let notification = self.next_notification()?; + + let Ok(server_notification) = ServerNotification::try_from(notification) else { + continue; + }; + + match server_notification { + ServerNotification::ThreadStarted(payload) => { + if payload.thread.id == thread_id { + println!("< thread/started notification: {:?}", payload.thread); + } + } + ServerNotification::TurnStarted(payload) => { + if payload.turn.id == turn_id { + println!("< turn/started notification: {:?}", payload.turn.status); + } + } + ServerNotification::AgentMessageDelta(delta) => { + print!("{}", delta.delta); + std::io::stdout().flush().ok(); + } + ServerNotification::CommandExecutionOutputDelta(delta) => { + print!("{}", delta.delta); + std::io::stdout().flush().ok(); + } + ServerNotification::ItemStarted(payload) => { + println!("\n< item started: {:?}", payload.item); + } + ServerNotification::ItemCompleted(payload) => { + println!("< item completed: {:?}", payload.item); + } + ServerNotification::TurnCompleted(payload) => { + if payload.turn.id == turn_id { + println!("\n< turn/completed notification: {:?}", payload.turn.status); + if let Some(error) = payload.turn.error { + println!("[turn error] {}", error.message); + } + println!("< usage: {:?}", payload.usage); + break; + } + } + ServerNotification::McpToolCallProgress(payload) => { + println!("< MCP tool progress: {}", payload.message); + } + _ => { + println!("[UNKNOWN SERVER NOTIFICATION] {server_notification:?}"); + } + } + } + + Ok(()) + } + + fn extract_event( + &self, + notification: JSONRPCNotification, + conversation_id: &ConversationId, + ) -> Result> { + let params = notification + .params + .context("event notification missing params")?; + + let mut map = match params { + Value::Object(map) => map, + other => bail!("unexpected params shape: {other:?}"), + }; + + let conversation_value = map + .remove("conversationId") + .context("event missing conversationId")?; + let notification_conversation: ConversationId = serde_json::from_value(conversation_value) + .context("conversationId was not a valid UUID")?; + + if ¬ification_conversation != conversation_id { + return Ok(None); + } + + let event_value = Value::Object(map); + let event: Event = + serde_json::from_value(event_value).context("failed to decode event payload")?; + Ok(Some(event)) + } + + fn send_request( + &mut self, + request: ClientRequest, + request_id: RequestId, + method: &str, + ) -> Result + where + T: DeserializeOwned, + { + self.write_request(&request)?; + self.wait_for_response(request_id, method) + } + + fn write_request(&mut self, request: &ClientRequest) -> Result<()> { + let request_json = serde_json::to_string(request)?; + let request_pretty = serde_json::to_string_pretty(request)?; + print_multiline_with_prefix("> ", &request_pretty); + + if let Some(stdin) = self.stdin.as_mut() { + writeln!(stdin, "{request_json}")?; + stdin + .flush() + .context("failed to flush request to codex app-server")?; + } else { + bail!("codex app-server stdin closed"); + } + + Ok(()) + } + + fn wait_for_response(&mut self, request_id: RequestId, method: &str) -> Result + where + T: DeserializeOwned, + { + loop { + let message = self.read_jsonrpc_message()?; + + match message { + JSONRPCMessage::Response(JSONRPCResponse { id, result }) => { + if id == request_id { + return serde_json::from_value(result) + .with_context(|| format!("{method} response missing payload")); + } + } + JSONRPCMessage::Error(err) => { + if err.id == request_id { + bail!("{method} failed: {err:?}"); + } + } + JSONRPCMessage::Notification(notification) => { + self.pending_notifications.push_back(notification); + } + JSONRPCMessage::Request(_) => { + bail!("unexpected request from codex app-server"); + } + } + } + } + + fn next_notification(&mut self) -> Result { + if let Some(notification) = self.pending_notifications.pop_front() { + return Ok(notification); + } + + loop { + let message = self.read_jsonrpc_message()?; + + match message { + JSONRPCMessage::Notification(notification) => return Ok(notification), + JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => { + // No outstanding requests, so ignore stray responses/errors for now. + continue; + } + JSONRPCMessage::Request(_) => { + bail!("unexpected request from codex app-server"); + } + } + } + } + + fn read_jsonrpc_message(&mut self) -> Result { + loop { + let mut response_line = String::new(); + let bytes = self + .stdout + .read_line(&mut response_line) + .context("failed to read from codex app-server")?; + + if bytes == 0 { + bail!("codex app-server closed stdout"); + } + + let trimmed = response_line.trim(); + if trimmed.is_empty() { + continue; + } + + let parsed: Value = + serde_json::from_str(trimmed).context("response was not valid JSON-RPC")?; + let pretty = serde_json::to_string_pretty(&parsed)?; + print_multiline_with_prefix("< ", &pretty); + let message: JSONRPCMessage = serde_json::from_value(parsed) + .context("response was not a valid JSON-RPC message")?; + return Ok(message); + } + } + + fn request_id(&self) -> RequestId { + RequestId::String(Uuid::new_v4().to_string()) + } +} + +fn print_multiline_with_prefix(prefix: &str, payload: &str) { + for line in payload.lines() { + println!("{prefix}{line}"); + } +} + +impl Drop for CodexClient { + fn drop(&mut self) { + let _ = self.stdin.take(); + + if let Ok(Some(status)) = self.child.try_wait() { + println!("[codex app-server exited: {status}]"); + return; + } + + thread::sleep(Duration::from_millis(100)); + + if let Ok(Some(status)) = self.child.try_wait() { + println!("[codex app-server exited: {status}]"); + return; + } + + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} diff --git a/codex-rs/justfile b/codex-rs/justfile index 9ddc4a37a..b1b9d7337 100644 --- a/codex-rs/justfile +++ b/codex-rs/justfile @@ -21,6 +21,11 @@ tui *args: file-search *args: cargo run --bin codex-file-search -- "$@" +# Build the CLI and run the app-server test client +app-server-test-client *args: + cargo build -p codex-cli + cargo run -p codex-app-server-test-client -- --codex-bin ./target/debug/codex "$@" + # format code fmt: cargo fmt -- --config imports_granularity=Item