app
Codex
Public Made by Adomby adom
Codex in Adom Hydrogen: ecosystem setup, dock dashboard and live engineering demos.
← Commit history
Add Rust Codex Desktop bridge with native app tools, auth reuse and plugin lifecycle
16 files changed
+3134−8
README.md+5−1bridge/.gitignore+3bridge/Cargo.lock+872bridge/Cargo.toml+20bridge/bridge.json+86bridge/build.sh+13bridge/src/app_tools.rs+160bridge/src/desktop.rs+256bridge/src/main.rs+639bridge/src/process.rs+112bridge/src/rpc.rs+268bridge/tests/http.rs+75bridge/verbs.json+456docs/DESKTOP-BRIDGE.md+132−6package.json+3−1skills/codex-desktop-bridge/SKILL.md+34README.md+5−1@@ -6,13 +6,17 @@ Install the Adom integration with `adom-wiki pkg install adom/codex`, then open This package configures Codex for Adom. It does not install or replace the OpenAI Codex executable or your credentials. +The repository also contains a Windows x64 Rust desktop bridge in `bridge/`.+Its separately built ZIP runs inside AB; installing this skills/dashboard package+does not install that runtime or the desktop app. See the desktop bridge guide below.+ ## Guides - [Bootstrap: preparing Codex for Hydrogen](docs/BOOTSTRAP.md) - [Dock dashboard, newbie panel and demos](docs/DASHBOARD.md) - [Astra: live KiCad routing and comparison log](docs/ASTRA-KICAD.md) - [Electrical-aware routing and copper-ablation worked example](docs/ELECTRICAL-ROUTING.md)-- [Codex Desktop installation and Bridge roadmap](docs/DESKTOP-BRIDGE.md)+- [Codex Desktop Rust bridge: installation, sign-in, plugins and native tools](docs/DESKTOP-BRIDGE.md) ## Astra drives live KiCad
bridge/.gitignoreadded+3@@ -0,0 +1,3 @@+/target/+/dist/+
bridge/Cargo.lockadded+872@@ -0,0 +1,872 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adler2"+version = "2.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"++[[package]]+name = "ascii"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"++[[package]]+name = "base64"+version = "0.22.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"++[[package]]+name = "bumpalo"+version = "3.20.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"++[[package]]+name = "cc"+version = "1.4.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80"+dependencies = [+ "find-msvc-tools",+ "shlex",+]++[[package]]+name = "cfg-if"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"++[[package]]+name = "chunked_transfer"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"++[[package]]+name = "codex-bridge"+version = "0.1.0"+dependencies = [+ "base64",+ "serde",+ "serde_json",+ "tiny_http",+ "ureq",+ "uuid",+ "windows-sys 0.61.2",+]++[[package]]+name = "crc32fast"+version = "1.5.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"+dependencies = [+ "cfg-if",+]++[[package]]+name = "displaydoc"+version = "0.2.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "find-msvc-tools"+version = "0.1.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d"++[[package]]+name = "flate2"+version = "1.1.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb"+dependencies = [+ "crc32fast",+ "miniz_oxide",+ "zlib-rs",+]++[[package]]+name = "form_urlencoded"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"+dependencies = [+ "percent-encoding",+]++[[package]]+name = "futures-core"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"++[[package]]+name = "futures-task"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"++[[package]]+name = "futures-util"+version = "0.3.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"+dependencies = [+ "futures-core",+ "futures-task",+ "pin-project-lite",+ "slab",+]++[[package]]+name = "getrandom"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"+dependencies = [+ "cfg-if",+ "libc",+ "wasi",+]++[[package]]+name = "getrandom"+version = "0.4.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"+dependencies = [+ "cfg-if",+ "libc",+ "r-efi",+]++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "icu_collections"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"+dependencies = [+ "displaydoc",+ "potential_utf",+ "utf8_iter",+ "yoke",+ "zerofrom",+ "zerovec",+]++[[package]]+name = "icu_locale_core"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"+dependencies = [+ "displaydoc",+ "litemap",+ "tinystr",+ "writeable",+ "zerovec",+]++[[package]]+name = "icu_normalizer"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"+dependencies = [+ "icu_collections",+ "icu_normalizer_data",+ "icu_properties",+ "icu_provider",+ "smallvec",+ "zerovec",+]++[[package]]+name = "icu_normalizer_data"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"++[[package]]+name = "icu_properties"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"+dependencies = [+ "displaydoc",+ "icu_collections",+ "icu_locale_core",+ "icu_properties_data",+ "icu_provider",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "icu_properties_data"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"++[[package]]+name = "icu_provider"+version = "2.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"+dependencies = [+ "displaydoc",+ "icu_locale_core",+ "writeable",+ "yoke",+ "zerofrom",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "idna"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"+dependencies = [+ "idna_adapter",+ "smallvec",+ "utf8_iter",+]++[[package]]+name = "idna_adapter"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"+dependencies = [+ "icu_normalizer",+ "icu_properties",+]++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "js-sys"+version = "0.3.105"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"+dependencies = [+ "cfg-if",+ "futures-util",+ "wasm-bindgen",+]++[[package]]+name = "libc"+version = "0.2.189"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"++[[package]]+name = "litemap"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"++[[package]]+name = "log"+version = "0.4.34"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"++[[package]]+name = "memchr"+version = "2.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"++[[package]]+name = "miniz_oxide"+version = "0.9.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c"+dependencies = [+ "adler2",+ "simd-adler32",+]++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[package]]+name = "percent-encoding"+version = "2.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"++[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]]+name = "potential_utf"+version = "0.1.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"+dependencies = [+ "zerovec",+]++[[package]]+name = "proc-macro2"+version = "1.0.107"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.47"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "r-efi"+version = "6.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"++[[package]]+name = "ring"+version = "0.17.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"+dependencies = [+ "cc",+ "cfg-if",+ "getrandom 0.2.17",+ "libc",+ "untrusted",+ "windows-sys 0.52.0",+]++[[package]]+name = "rustls"+version = "0.23.44"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba"+dependencies = [+ "log",+ "once_cell",+ "ring",+ "rustls-pki-types",+ "rustls-webpki",+ "subtle",+ "zeroize",+]++[[package]]+name = "rustls-pki-types"+version = "1.15.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"+dependencies = [+ "zeroize",+]++[[package]]+name = "rustls-webpki"+version = "0.103.15"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"+dependencies = [+ "ring",+ "rustls-pki-types",+ "untrusted",+]++[[package]]+name = "rustversion"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"++[[package]]+name = "serde"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.229"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "serde_json"+version = "1.0.151"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "shlex"+version = "2.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"++[[package]]+name = "simd-adler32"+version = "0.3.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "smallvec"+version = "1.16.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"++[[package]]+name = "stable_deref_trait"+version = "1.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"++[[package]]+name = "subtle"+version = "2.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"++[[package]]+name = "syn"+version = "2.0.119"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "syn"+version = "3.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"+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 2.0.119",+]++[[package]]+name = "tiny_http"+version = "0.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"+dependencies = [+ "ascii",+ "chunked_transfer",+ "httpdate",+ "log",+]++[[package]]+name = "tinystr"+version = "0.8.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"+dependencies = [+ "displaydoc",+ "zerovec",+]++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "untrusted"+version = "0.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"++[[package]]+name = "ureq"+version = "2.12.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"+dependencies = [+ "base64",+ "flate2",+ "log",+ "once_cell",+ "rustls",+ "rustls-pki-types",+ "serde",+ "serde_json",+ "url",+ "webpki-roots 0.26.11",+]++[[package]]+name = "url"+version = "2.5.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"+dependencies = [+ "form_urlencoded",+ "idna",+ "percent-encoding",+ "serde",+]++[[package]]+name = "utf8_iter"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"++[[package]]+name = "uuid"+version = "1.26.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"+dependencies = [+ "getrandom 0.4.3",+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasi"+version = "0.11.1+wasi-snapshot-preview1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"++[[package]]+name = "wasm-bindgen"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn 3.0.5",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.128"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "webpki-roots"+version = "0.26.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"+dependencies = [+ "webpki-roots 1.0.9",+]++[[package]]+name = "webpki-roots"+version = "1.0.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"+dependencies = [+ "rustls-pki-types",+]++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-sys"+version = "0.52.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"+dependencies = [+ "windows-targets",+]++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "windows-targets"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"+dependencies = [+ "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.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"++[[package]]+name = "windows_aarch64_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"++[[package]]+name = "windows_i686_gnu"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"++[[package]]+name = "windows_i686_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"++[[package]]+name = "windows_i686_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"++[[package]]+name = "windows_x86_64_gnu"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"++[[package]]+name = "windows_x86_64_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"++[[package]]+name = "windows_x86_64_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"++[[package]]+name = "writeable"+version = "0.6.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"++[[package]]+name = "yoke"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"+dependencies = [+ "stable_deref_trait",+ "yoke-derive",+ "zerofrom",+]++[[package]]+name = "yoke-derive"+version = "0.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+ "synstructure",+]++[[package]]+name = "zerofrom"+version = "0.1.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"+dependencies = [+ "zerofrom-derive",+]++[[package]]+name = "zerofrom-derive"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 2.0.119",+ "synstructure",+]++[[package]]+name = "zeroize"+version = "1.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"++[[package]]+name = "zerotrie"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"+dependencies = [+ "displaydoc",+ "yoke",+ "zerofrom",+]++[[package]]+name = "zerovec"+version = "0.11.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"+dependencies = [+ "yoke",+ "zerofrom",+ "zerovec-derive",+]++[[package]]+name = "zerovec-derive"+version = "0.11.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"+dependencies = [+ "proc-macro2",+ "quote",+ "syn 3.0.5",+]++[[package]]+name = "zlib-rs"+version = "0.6.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"++[[package]]+name = "zmij"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
bridge/Cargo.tomladded+20@@ -0,0 +1,20 @@+[package]+name = "codex-bridge"+version = "0.1.0"+edition = "2021"+publish = false++[dependencies]+base64 = "0.22"+serde = { version = "1", features = ["derive"] }+serde_json = "1"+tiny_http = "0.12"+ureq = { version = "2.12", features = ["json"] }+uuid = { version = "1", features = ["v4"] }++[target.'cfg(windows)'.dependencies]+windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Pipes"] }++[profile.release]+strip = true+lto = "thin"
bridge/bridge.jsonadded+86@@ -0,0 +1,86 @@+{+ "manifest_version": 1,+ "name": "codex",+ "displayName": "Codex",+ "version": "0.1.0",+ "description": "Compiled Rust bridge for the OpenAI Codex desktop app",+ "author": "Adom",+ "docs": "https://wiki.adom.inc/adom/codex",+ "homepage": "https://wiki.adom.inc/adom/codex",+ "spawn": {+ "kind": "exe",+ "entrypoint": "codex-bridge.exe",+ "port": 0,+ "persistent": true,+ "healthEndpoint": "/health",+ "stopMethod": "kill",+ "killImageName": "codex-bridge.exe"+ },+ "verbPrefixes": [+ "codex_"+ ],+ "verbs": [+ "codex_describe",+ "codex_status",+ "codex_readiness",+ "codex_state",+ "codex_progress",+ "codex_desktop_install",+ "codex_desktop_uninstall",+ "codex_desktop_open",+ "codex_connect",+ "codex_disconnect",+ "codex_uninstall",+ "codex_auth_import",+ "codex_events",+ "codex_requests",+ "codex_request_reply",+ "codex_account_read",+ "codex_login_start",+ "codex_login_cancel",+ "codex_models",+ "codex_thread_list",+ "codex_thread_read",+ "codex_thread_start",+ "codex_thread_resume",+ "codex_thread_fork",+ "codex_thread_archive",+ "codex_turn_start",+ "codex_turn_steer",+ "codex_turn_interrupt",+ "codex_plugin_list",+ "codex_plugin_read",+ "codex_plugin_install",+ "codex_plugin_uninstall",+ "codex_skills_list",+ "codex_mcp_status",+ "codex_config_read",+ "codex_desktop_tools",+ "codex_desktop_tool_call",+ "codex_thread_unsubscribe",+ "codex_thread_turns",+ "codex_thread_items"+ ],+ "statusVerb": "codex_progress",+ "risk": "exec",+ "uninstall": "codex_uninstall",+ "timeouts": {+ "default": 150,+ "verbs": {+ "desktop_install": 10,+ "desktop_uninstall": 10,+ "progress": 10,+ "status": 30,+ "state": 120+ }+ },+ "detect": {+ "hostApp": "Codex Desktop",+ "paths": {+ "windows": [+ "%ProgramFiles%\\WindowsApps\\OpenAI.Codex_*\\app\\ChatGPT.exe",+ "%ProgramFiles%\\WindowsApps\\OpenAI.Codex_*\\app\\Codex.exe"+ ]+ }+ }+}
bridge/build.shadded+13@@ -0,0 +1,13 @@+#!/usr/bin/env bash+set -euo pipefail+cd "$(dirname "$0")"+cargo fmt --check+cargo test --locked+cargo build --locked --release --target x86_64-pc-windows-gnu+mkdir -p dist/package+cp bridge.json dist/package/bridge.json+cp target/x86_64-pc-windows-gnu/release/codex-bridge.exe dist/package/codex-bridge.exe+cd dist/package+zip -X -j ../codex-bridge-windows-x64.zip bridge.json codex-bridge.exe+cd ..+sha256sum codex-bridge-windows-x64.zip package/codex-bridge.exe > SHA256SUMS
bridge/src/app_tools.rsadded+160@@ -0,0 +1,160 @@+#[cfg(any(windows, test))]+use serde_json::json;+use serde_json::Value;++#[cfg(any(windows, test))]+const MAX_FRAME: usize = 8 * 1024 * 1024;+#[cfg(any(windows, test))]+fn frame(value: &Value) -> Result<Vec<u8>, String> {+ let body = serde_json::to_vec(value).map_err(|e| e.to_string())?;+ if body.len() > MAX_FRAME {+ return Err("app_tools_frame_too_large".into());+ }+ let mut frame = (body.len() as u32).to_le_bytes().to_vec();+ frame.extend(body);+ Ok(frame)+}++#[cfg(not(windows))]+pub fn request(_method: &str, _params: Value, _endpoint: Option<&str>) -> Result<Value, String> {+ Err("desktop_tools_require_windows".into())+}++#[cfg(windows)]+pub fn request(method: &str, params: Value, endpoint: Option<&str>) -> Result<Value, String> {+ use std::{+ fs::OpenOptions,+ io::{Read, Write},+ os::windows::io::AsRawHandle,+ time::{Duration, Instant},+ };+ use windows_sys::Win32::System::Pipes::{GetNamedPipeServerProcessId, PeekNamedPipe};+ let inv = crate::desktop::inventory()?;+ let owners: Vec<u32> = inv["windows"]+ .as_array()+ .unwrap_or(&vec![])+ .iter()+ .filter_map(|w| w["pid"].as_u64().map(|p| p as u32))+ .collect();+ if owners.is_empty() {+ return Err("desktop_window_not_running".into());+ }+ let listing = crate::process::powershell(+ r#"@(Get-ChildItem \\.\pipe\ | Where-Object {$_.Name -like 'codex-browser-use-*'} | ForEach-Object {$_.Name}) | ConvertTo-Json -Compress"#,+ Duration::from_secs(10),+ )?;+ let value: Value = serde_json::from_str(listing["stdout"].as_str().unwrap_or("").trim())+ .map_err(|_| "desktop_tools_pipe_not_found")?;+ let names = if value.is_array() {+ value.as_array().unwrap().clone()+ } else {+ vec![value]+ };+ let mut verified = Vec::new();+ for name in names {+ let Some(name) = name.as_str() else {+ continue;+ };+ if !name.starts_with("codex-browser-use-")+ || name.contains(['\\', '/'])+ || endpoint.is_some_and(|e| e != name)+ {+ continue;+ }+ let path = format!(r"\\.\pipe\{name}");+ if let Ok(pipe) = OpenOptions::new().read(true).write(true).open(&path) {+ let mut pid = 0;+ if unsafe { GetNamedPipeServerProcessId(pipe.as_raw_handle(), &mut pid) } != 0+ && owners.contains(&pid)+ {+ verified.push((pipe, pid, name.to_string()));+ }+ }+ }+ if verified.len() != 1 {+ return Err(format!(+ "desktop_tools_pipe_ambiguous_or_missing: specify desktopPipe from verified endpoints {:?}",+ verified.iter().map(|(_,pid,name)|(pid,name)).collect::<Vec<_>>()+ ));+ }+ let (mut pipe, pid, endpoint) = verified.pop().unwrap();+ let id = uuid::Uuid::new_v4().to_string();+ pipe.write_all(&frame(+ &json!({"jsonrpc":"2.0","id":id,"method":method,"params":params}),+ )?)+ .map_err(|_| "desktop_tools_write_failed")?;+ let deadline = Instant::now() + Duration::from_secs(120);+ fn read_until(+ pipe: &mut std::fs::File,+ buf: &mut [u8],+ deadline: Instant,+ ) -> Result<(), String> {+ let mut offset = 0;+ while offset < buf.len() {+ let mut available = 0;+ if unsafe {+ PeekNamedPipe(+ pipe.as_raw_handle(),+ std::ptr::null_mut(),+ 0,+ std::ptr::null_mut(),+ &mut available,+ std::ptr::null_mut(),+ )+ } == 0+ {+ return Err("desktop_tools_disconnected".into());+ }+ if available == 0 {+ if Instant::now() >= deadline {+ return Err("desktop_tools_timeout: native request may still need a user response; inspect desktop state".into());+ }+ std::thread::sleep(Duration::from_millis(20));+ continue;+ }+ let end = buf.len().min(offset + available as usize);+ let n = pipe+ .read(&mut buf[offset..end])+ .map_err(|_| "desktop_tools_read_failed")?;+ if n == 0 {+ return Err("desktop_tools_disconnected".into());+ }+ offset += n;+ }+ Ok(())+ }+ let mut size = [0; 4];+ read_until(&mut pipe, &mut size, deadline)?;+ let size = u32::from_le_bytes(size) as usize;+ if size > MAX_FRAME {+ return Err("desktop_tools_frame_too_large".into());+ }+ let mut body = vec![0; size];+ read_until(&mut pipe, &mut body, deadline)?;+ let response: Value =+ serde_json::from_slice(&body).map_err(|_| "desktop_tools_invalid_response")?;+ if response["id"] != id {+ return Err("desktop_tools_response_id_mismatch".into());+ }+ if let Some(error) = response.get("error") {+ return Err(format!("desktop_tools_error: {error}"));+ }+ Ok(+ json!({"result":response["result"],"desktopPid":pid,"desktopPipe":endpoint,"transport":"desktop-native-pipe"}),+ )+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn framing_matches_native_little_endian_protocol() {+ let v = json!({"method":"tools/list","params":{"threadStartKind":"all"}});+ let f = frame(&v).unwrap();+ assert_eq!(+ u32::from_le_bytes(f[..4].try_into().unwrap()) as usize,+ f.len() - 4+ );+ assert_eq!(serde_json::from_slice::<Value>(&f[4..]).unwrap(), v);+ }+}
bridge/src/desktop.rsadded+256@@ -0,0 +1,256 @@+use crate::process::{powershell, ps_quote, run};+use serde_json::{json, Value};+use std::{+ env, fs,+ path::{Path, PathBuf},+ process::Command,+ time::Duration,+};++pub const PACKAGE: &str = "OpenAI.Codex";+pub const STORE_ID: &str = "9PLM9XGG6VKS";+pub fn home() -> PathBuf {+ env::var_os("CODEX_HOME")+ .map(PathBuf::from)+ .unwrap_or_else(|| {+ PathBuf::from(+ env::var_os("USERPROFILE")+ .or_else(|| env::var_os("HOME"))+ .unwrap_or_default(),+ )+ .join(".codex")+ })+}+pub fn inventory() -> Result<Value, String> {+ if !cfg!(windows) {+ return Ok(json!({"installed":false,"platform":env::consts::OS,"supported":false}));+ }+ let result = powershell(+ &format!(+ r#"$ErrorActionPreference='Stop'; $p=Get-AppxPackage {PACKAGE};+$processes=@(Get-Process Codex,ChatGPT -ErrorAction SilentlyContinue | Where-Object {{$p -and $_.Path -and $_.Path.StartsWith($p.InstallLocation,[StringComparison]::OrdinalIgnoreCase)}} | ForEach-Object {{@{{pid=$_.Id;hwnd=$_.MainWindowHandle.ToInt64();title=$_.MainWindowTitle;path=$_.Path}}}});+$items=@($processes | Where-Object {{$_.hwnd -ne 0}});+if($p){{ @{{installed=$true;platform='windows';supported=$true;name=$p.Name;version=$p.Version.ToString();path=$p.InstallLocation;family=$p.PackageFamilyName;packageFullName=$p.PackageFullName;windows=$items;processes=$processes}} | ConvertTo-Json -Depth 5 -Compress }}+else {{ @{{installed=$false;platform='windows';supported=$true;windows=$items}} | ConvertTo-Json -Depth 5 -Compress }}"#+ ),+ Duration::from_secs(15),+ )?;+ if result["success"] != true {+ return Err("desktop_inventory_failed".into());+ }+ serde_json::from_str(+ result["stdout"]+ .as_str()+ .unwrap_or("")+ .trim_start_matches('\u{feff}')+ .trim(),+ )+ .map_err(|e| e.to_string())+}+pub fn executable() -> Result<PathBuf, String> {+ if let Some(path) = env::var_os("CODEX_BRIDGE_CLI") {+ let path = PathBuf::from(path);+ if path.is_file() {+ return Ok(path);+ }+ return Err("configured_cli_missing".into());+ }+ let inv = inventory()?;+ let root = PathBuf::from(inv["path"].as_str().ok_or("desktop_not_installed")?);+ if cfg!(windows) {+ let result = powershell(+ r#"$ErrorActionPreference='Stop'; $p=Get-AppxPackage OpenAI.Codex; $all=@(Get-CimInstance Win32_Process); $parents=@($all | Where-Object {$_.ExecutablePath -and $_.ExecutablePath.StartsWith($p.InstallLocation,[StringComparison]::OrdinalIgnoreCase)} | ForEach-Object {$_.ProcessId}); $cli=$all | Where-Object {$_.Name -eq 'codex.exe' -and $parents -contains $_.ParentProcessId} | Select-Object -First 1; if($cli){@{path=$cli.ExecutablePath}|ConvertTo-Json -Compress}"#,+ Duration::from_secs(15),+ )?;+ if let Ok(value) =+ serde_json::from_str::<Value>(result["stdout"].as_str().unwrap_or("").trim())+ {+ if let Some(path) = value["path"].as_str() {+ let path = PathBuf::from(path);+ if path.is_file() {+ return Ok(path);+ }+ }+ }+ return Err("desktop_backend_not_running: open the desktop app first so it activates its bundled CLI".into());+ }+ for relative in [+ "app/resources/codex.exe",+ "app/resources/bin/codex.exe",+ "resources/codex.exe",+ ] {+ let path = root.join(relative);+ if path.is_file() {+ return Ok(path);+ }+ }+ Err("desktop_cli_not_found".into())+}+pub fn install(accept: bool) -> Result<Value, String> {+ if !cfg!(windows) {+ return Err("unsupported_platform".into());+ }+ let before = inventory()?;+ if before["installed"] == true {+ return Ok(json!({"alreadyInstalled":true,"desktop":before}));+ }+ let mut cmd = Command::new("winget.exe");+ cmd.args([+ "install",+ "--id",+ STORE_ID,+ "--exact",+ "--source",+ "msstore",+ "--silent",+ "--disable-interactivity",+ ]);+ if accept {+ cmd.arg("--accept-package-agreements");+ }+ let result = run(cmd, None, Duration::from_secs(900))?;+ let after = inventory()?;+ if after["installed"] != true {+ return Ok(+ json!({"success":false,"errorCode":if accept {"install_failed"} else {"blocked_on_user"},+ "blockedBy":if accept {Value::Null} else {json!("Microsoft Store package agreements")},+ "installer":result,"desktop":after,"_hint":"Inspect installer output. After user agreement acceptance, call codex_desktop_install with acceptAgreements:true."}),+ );+ }+ Ok(json!({"desktop":after,"installer":result}))+}+pub fn uninstall() -> Result<Value, String> {+ let before = inventory()?;+ if before["installed"] != true {+ return Ok(json!({"alreadyAbsent":true}));+ }+ if before["processes"]+ .as_array()+ .is_some_and(|a| !a.is_empty())+ {+ return Err("desktop_running_close_it_first".into());+ }+ let result=powershell(&format!("$ErrorActionPreference='Stop'; Get-AppxPackage {PACKAGE} | Remove-AppxPackage -ErrorAction Stop"),Duration::from_secs(180))?;+ let after = inventory()?;+ Ok(+ json!({"success":result["success"]==true && after["installed"]==false,"desktop":after,+ "removal":result,"preservedCodexHome":home()}),+ )+}+pub fn launch() -> Result<Value, String> {+ let inv = inventory()?;+ if inv["installed"] != true {+ return Err("desktop_not_installed".into());+ }+ if inv["windows"].as_array().is_some_and(|a| !a.is_empty()) {+ return Ok(inv);+ }+ let family = inv["family"].as_str().ok_or("missing_package_family")?;+ let script=format!("$ErrorActionPreference='Stop'; Start-Process explorer.exe -ArgumentList {} -WindowStyle Minimized",ps_quote(&format!("shell:AppsFolder\\{family}!App")));+ let result = powershell(&script, Duration::from_secs(15))?;+ if result["success"] != true {+ return Err("desktop_launch_failed".into());+ }+ for _ in 0..30 {+ let inv = inventory()?;+ if inv["windows"].as_array().is_some_and(|a| !a.is_empty()) {+ return Ok(inv);+ }+ std::thread::sleep(Duration::from_millis(500));+ }+ Err("desktop_window_not_observed".into())+}+pub fn auth_import(path: &Path) -> Result<Value, String> {+ auth_import_to(path, &home())+}+fn auth_import_to(path: &Path, dir: &Path) -> Result<Value, String> {+ use std::io::Write;+ if !path.is_absolute() {+ return Err("auth_source_must_be_absolute".into());+ }+ if fs::metadata(path)+ .map_err(|_| "auth_source_unreadable")?+ .len()+ > 128 * 1024+ {+ return Err("auth_source_too_large".into());+ }+ let bytes = fs::read(path).map_err(|_| "auth_source_unreadable")?;+ if bytes.len() > 128 * 1024 {+ return Err("auth_source_too_large".into());+ }+ let auth: Value = serde_json::from_slice(&bytes).map_err(|_| "invalid_auth_json")?;+ if !auth["tokens"]["access_token"].is_string() && !auth["OPENAI_API_KEY"].is_string() {+ return Err("unsupported_auth_cache".into());+ }+ fs::create_dir_all(dir).map_err(|_| "auth_directory_failed")?;+ let dest = dir.join("auth.json");+ let mut options = fs::OpenOptions::new();+ options.write(true).create_new(true);+ #[cfg(unix)]+ {+ use std::os::unix::fs::OpenOptionsExt;+ options.mode(0o600);+ }+ let mut file = options+ .open(&dest)+ .map_err(|_| "auth_destination_exists_or_unwritable")?;+ if cfg!(windows) {+ let secured=powershell(&format!("$ErrorActionPreference='Stop'; $acl=Get-Acl {}; $acl.SetAccessRuleProtection($true,$false); $rule=New-Object System.Security.AccessControl.FileSystemAccessRule([System.Security.Principal.WindowsIdentity]::GetCurrent().Name,'FullControl','Allow'); $acl.SetAccessRule($rule); Set-Acl -Path {} -AclObject $acl",ps_quote(&dest.to_string_lossy()),ps_quote(&dest.to_string_lossy())),Duration::from_secs(10))?;+ if secured["success"] != true {+ drop(file);+ let _ = fs::remove_file(&dest);+ return Err("auth_permissions_failed".into());+ }+ }+ if file+ .write_all(&bytes)+ .and_then(|_| file.sync_all())+ .is_err()+ {+ drop(file);+ let _ = fs::remove_file(&dest);+ return Err("auth_write_failed".into());+ }+ Ok(+ json!({"imported":true,"credentialValuesReturned":false,"destination":dest,+ "_hint":"Call codex_account_read to verify authenticated identity. Existing destination credentials are never overwritten."}),+ )+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn import_preserves_existing_credentials_and_returns_no_values() {+ let root = env::temp_dir().join(format!("codex-auth-test-{}", uuid::Uuid::new_v4()));+ fs::create_dir_all(&root).unwrap();+ let source = root.join("source.json");+ let dest = root.join("home");+ fs::write(+ &source,+ br#"{"tokens":{"access_token":"test-only-placeholder"}}"#,+ )+ .unwrap();+ let result = auth_import_to(&source, &dest).unwrap();+ assert!(!result.to_string().contains("test-only-placeholder"));+ assert!(auth_import_to(&source, &dest).is_err());+ assert_eq!(+ fs::read(&source).unwrap(),+ fs::read(dest.join("auth.json")).unwrap()+ );+ #[cfg(unix)]+ {+ use std::os::unix::fs::PermissionsExt;+ assert_eq!(+ fs::metadata(dest.join("auth.json"))+ .unwrap()+ .permissions()+ .mode()+ & 0o777,+ 0o600+ );+ }+ fs::remove_dir_all(root).unwrap();+ }+}
bridge/src/main.rsadded+639@@ -0,0 +1,639 @@+mod app_tools;+mod desktop;+mod process;+mod rpc;++use serde::Deserialize;+use serde_json::{json, Value};+use std::{+ collections::HashMap,+ env,+ io::Read,+ path::Path,+ sync::{Arc, Mutex},+ thread,+ time::{Duration, Instant},+};+use tiny_http::{Header, Method, Request, Response, Server};++const VERSION: &str = env!("CARGO_PKG_VERSION");+const CATALOG: &str = include_str!("../verbs.json");+#[derive(Deserialize)]+struct Command {+ command: String,+ #[serde(default)]+ args: Value,+}+#[derive(Clone, Default)]+struct Caller {+ headers: HashMap<String, String>,+ thread: String,+}+struct Job {+ owner: String,+ verb: String,+ started: Instant,+ result: Option<Value>,+}+struct State {+ rpc: Mutex<Option<Arc<rpc::Rpc>>>,+ owner: Mutex<Option<String>>,+ jobs: Mutex<HashMap<String, Job>>,+ mutation: Mutex<()>,+}++fn failure(code: &str) -> Value {+ json!({"success":false,"errorCode":code.split(':').next().unwrap_or(code),"error":code,+ "_hint":"Inspect codex_status and codex_describe. After a timeout, poll the existing job or events before retrying a mutation."})+}+fn finish(mut result: Value) -> Value {+ if !result.is_object() {+ result = json!({"result":result});+ }+ let obj = result.as_object_mut().unwrap();+ obj.entry("success").or_insert(json!(true));+ obj.entry("_hint").or_insert(json!("Use codex_describe for input schemas; codex_events and codex_requests expose progress and requests waiting for a reply."));+ obj.insert("bridge".into(), json!("codex"));+ obj.entry("version").or_insert(json!(VERSION));+ obj.insert("bridgeVersion".into(), json!(VERSION));+ redact(&mut result);+ result+}+fn redact(v: &mut Value) {+ match v {+ Value::Object(map) => {+ for (key, value) in map {+ let normalized = key.replace('_', "").to_ascii_lowercase();+ if [+ "accesstoken",+ "refreshtoken",+ "idtoken",+ "apikey",+ "openaiapikey",+ "authorization",+ "bearertoken",+ "password",+ "clientsecret",+ "httpheaders",+ "env",+ ]+ .contains(&normalized.as_str())+ && !value.is_boolean()+ {+ *value = json!("[REDACTED]");+ } else {+ redact(value);+ }+ }+ }+ Value::Array(a) => a.iter_mut().for_each(redact),+ _ => {}+ }+}+fn ab(command: &str, args: Value, caller: &Caller) -> Result<Value, String> {+ let base = env::var("ADOM_DIRECT_API_URL").map_err(|_| "ab_callback_not_configured")?;+ let url = ureq::AgentBuilder::new()+ .timeout(Duration::from_secs(35))+ .build();+ let mut req = url+ .post(&format!("{}/command", base.trim_end_matches('/')))+ .set("Content-Type", "application/json");+ for (k, v) in &caller.headers {+ req = req.set(k, v);+ }+ req = req.set("X-Adom-Caller-Delegate", "codex");+ if let Ok(token) = env::var("ADOM_BRIDGE_TOKEN") {+ req = req.set("X-Adom-Bridge-Token", &token);+ }+ let raw: Value = req+ .send_json(json!({"command":command,"args":args}))+ .map_err(|_| "ab_callback_failed")?+ .into_json()+ .map_err(|_| "ab_invalid_json")?;+ normalize(raw)+}+fn normalize(mut v: Value) -> Result<Value, String> {+ for _ in 0..4 {+ if v["success"] == false || v["ok"] == false || v["status"] == "error" {+ return Err(format!(+ "ab_error: {}",+ v.get("errorCode")+ .or(v.get("error"))+ .unwrap_or(&json!("unknown"))+ ));+ }+ if let Some(output) = v["output"].as_str() {+ if let Ok(inner) = serde_json::from_str(output) {+ v = inner;+ continue;+ }+ }+ break;+ }+ Ok(v)+}+impl State {+ fn backend(&self) -> Result<Arc<rpc::Rpc>, String> {+ self.rpc+ .lock()+ .unwrap()+ .as_ref()+ .filter(|r| r.alive())+ .cloned()+ .ok_or("not_connected: call codex_connect".into())+ }+ fn check_owner(&self, caller: &Caller) -> Result<(), String> {+ if let Some(owner) = self.owner.lock().unwrap().as_ref() {+ if owner != &caller.thread {+ return Err(format!("session_owned_by_other_thread: {owner}"));+ }+ }+ Ok(())+ }+ fn dispatch(&self, command: &str, mut args: Value, caller: &Caller) -> Result<Value, String> {+ match command {+ "codex_describe" => Ok(serde_json::from_str(CATALOG).unwrap()),+ "codex_status" | "codex_readiness" => {+ let backend = self.rpc.lock().unwrap();+ Ok(+ json!({"desktop":desktop::inventory()?,"connected":backend.as_ref().is_some_and(|r|r.alive()),+ "transportMode":backend.as_ref().map(|r|&r.mode),"ownerThread":*self.owner.lock().unwrap(),+ "authCachePresent":desktop::home().join("auth.json").is_file(),"codexHome":desktop::home()}),+ )+ }+ "codex_progress" => {+ let jobs = self.jobs.lock().unwrap();+ let Some(id) = args["jobId"].as_str() else {+ return Ok(+ json!({"jobs":jobs.iter().filter(|(_,j)|j.owner==caller.thread).map(|(id,j)|json!({"jobId":id,"command":j.verb,"stillRunning":j.result.is_none(),"elapsedSec":j.started.elapsed().as_secs_f64()})).collect::<Vec<_>>(),"_hint":"Pass a jobId to retrieve its result. Jobs are retained for one hour in this bridge process."}),+ );+ };+ let j = jobs.get(id).ok_or("job_not_found")?;+ if j.owner != caller.thread {+ return Err(format!("job_owned_by_other_thread: {}", j.owner));+ }+ Ok(+ json!({"jobId":id,"command":j.verb,"ownerThread":j.owner,"elapsedSec":j.started.elapsed().as_secs_f64(),+ "stillRunning":j.result.is_none(),"state":if j.result.is_none(){"running"}else{"complete"},"result":j.result}),+ )+ }+ "codex_desktop_install" => desktop::install(args["acceptAgreements"] == true),+ "codex_desktop_uninstall" => {+ self.check_owner(caller)?;+ if args["confirm"] != true {+ return Err("confirm_required: pass confirm:true for the explicitly requested desktop removal".into());+ }+ if self.backend().is_ok() {+ return Err("disconnect_before_desktop_uninstall".into());+ }+ desktop::uninstall()+ }+ "codex_desktop_open" => desktop::launch(),+ "codex_state" => {+ let inventory = desktop::inventory()?;+ let mut shots = Vec::new();+ for window in inventory["windows"].as_array().unwrap_or(&vec![]) {+ shots.push(json!({"window":window,"capture":ab("desktop_screenshot_window",json!({"hwnd":window["hwnd"],"format":"base64"}),caller)?}));+ }+ Ok(+ json!({"desktop":inventory,"shots":shots,"focusEvents":[],"windowEtiquette":"background"}),+ )+ }+ "codex_connect" => {+ self.check_owner(caller)?;+ if let Ok(r) = self.backend() {+ return Ok(+ json!({"connected":true,"mode":r.mode,"ownerThread":*self.owner.lock().unwrap()}),+ );+ }+ let exe = desktop::executable()?;+ let proxy = args["mode"].as_str().unwrap_or("standalone") == "desktop";+ if proxy && cfg!(windows) {+ return Err("daemon_proxy_unsupported_on_windows: use standalone for backend RPC; codex_desktop_tools and codex_desktop_tool_call communicate directly with the desktop".into());+ }+ let r = rpc::Rpc::start(&exe, proxy)?;+ *self.rpc.lock().unwrap() = Some(r.clone());+ *self.owner.lock().unwrap() = Some(caller.thread.clone());+ Ok(+ json!({"connected":true,"mode":r.mode,"executable":exe,"ownerThread":caller.thread,+ "_hint":"desktop-daemon-proxy attaches to the native daemon. bridge-owned-app-server is a separate backend using the same local Codex home; it does not prove a desktop window changed."}),+ )+ }+ "codex_disconnect" | "codex_uninstall" => {+ if !(command == "codex_uninstall" && args["cascade"] == true) {+ self.check_owner(caller)?;+ if self.backend().is_ok_and(|r| r.busy()) {+ return Err("active_turn_or_request: finish or interrupt the active turn before disconnecting".into());+ }+ }+ if let Some(r) = self.rpc.lock().unwrap().take() {+ r.stop();+ }+ *self.owner.lock().unwrap() = None;+ Ok(+ json!({"disconnected":true,"preservedDesktop":true,"preservedCodexHome":true,+ "_hint":"Only the bridge connection was stopped. AB removes its bridge cache. Desktop removal is a separate explicit codex_desktop_uninstall operation."}),+ )+ }+ "codex_auth_import" => {+ self.check_owner(caller)?;+ if self.backend().is_ok() {+ return Err("disconnect_before_auth_import".into());+ }+ desktop::auth_import(Path::new(args["path"].as_str().ok_or("path_required")?))+ }+ "codex_events" => {+ self.check_owner(caller)?;+ Ok(self+ .backend()?+ .events(args["sinceSeq"].as_u64().unwrap_or(0)))+ }+ "codex_requests" => {+ self.check_owner(caller)?;+ Ok(json!({"requests":self.backend()?.requests()}))+ }+ "codex_request_reply" => {+ self.check_owner(caller)?;+ self.backend()?+ .reply(args["requestId"].clone(), args["result"].clone())+ }+ "codex_desktop_tools" => app_tools::request(+ "tools/list",+ json!({"threadStartKind":"all"}),+ args["desktopPipe"].as_str(),+ ),+ "codex_desktop_tool_call" => {+ self.check_owner(caller)?;+ let name = args["tool"].as_str().ok_or("tool_required")?;+ let list = app_tools::request(+ "tools/list",+ json!({"threadStartKind":"all"}),+ args["desktopPipe"].as_str(),+ )?;+ let tool = list["result"]["tools"]+ .as_array()+ .ok_or("invalid_desktop_catalog")?+ .iter()+ .find(|t| t["name"] == name)+ .ok_or("unknown_desktop_tool")?;+ let thread_id = args["threadId"]+ .as_str()+ .ok_or("native_threadId_required")?;+ let result = app_tools::request(+ "tools/call",+ json!({"namespace":tool["namespace"],"tool":name,+ "arguments":args.get("arguments").cloned().unwrap_or(json!({})),"threadId":thread_id,+ "turnId":args.get("turnId").cloned().unwrap_or(json!(format!("adom-{}",uuid::Uuid::new_v4()))),+ "callId":format!("adom-{}",uuid::Uuid::new_v4())}),+ list["desktopPipe"].as_str(),+ )?;+ if result["result"]["success"] == false {+ return Ok(+ json!({"success":false,"errorCode":"native_tool_failed","native":result}),+ );+ }+ Ok(result)+ }+ _ => {+ self.check_owner(caller)?;+ if command == "codex_login_start"+ && !matches!(args["type"].as_str(), Some("chatgpt" | "chatgptDeviceCode"))+ {+ return Err("unsupported_login_type".into());+ }+ let method = rpc_method(command).ok_or("unknown_command")?;+ let query = args["query"].as_str().unwrap_or("").to_ascii_lowercase();+ let offset = args["cursor"]+ .as_str()+ .and_then(|v| v.parse::<usize>().ok())+ .unwrap_or(0);+ let limit = args["limit"].as_u64().unwrap_or(30).clamp(1, 100) as usize;+ if command == "codex_plugin_list" {+ if let Some(obj) = args.as_object_mut() {+ obj.remove("query");+ obj.remove("cursor");+ obj.remove("limit");+ }+ }+ let result = self+ .backend()?+ .call(method, args, Duration::from_secs(120))?;+ if command == "codex_plugin_list" {+ return Ok(+ json!({"result":plugin_page(result,&query,offset,limit),"method":method}),+ );+ }+ Ok(json!({"result":result,"method":method}))+ }+ }+ }+}+fn rpc_method(verb: &str) -> Option<&'static str> {+ Some(match verb {+ "codex_account_read" => "account/read",+ "codex_login_start" => "account/login/start",+ "codex_login_cancel" => "account/login/cancel",+ "codex_models" => "model/list",+ "codex_thread_list" => "thread/list",+ "codex_thread_read" => "thread/read",+ "codex_thread_start" => "thread/start",+ "codex_thread_resume" => "thread/resume",+ "codex_thread_fork" => "thread/fork",+ "codex_thread_archive" => "thread/archive",+ "codex_thread_unsubscribe" => "thread/unsubscribe",+ "codex_thread_turns" => "thread/turns/list",+ "codex_thread_items" => "thread/items/list",+ "codex_turn_start" => "turn/start",+ "codex_turn_steer" => "turn/steer",+ "codex_turn_interrupt" => "turn/interrupt",+ "codex_plugin_list" => "plugin/list",+ "codex_plugin_read" => "plugin/read",+ "codex_plugin_install" => "plugin/install",+ "codex_plugin_uninstall" => "plugin/uninstall",+ "codex_skills_list" => "skills/list",+ "codex_mcp_status" => "mcpServerStatus/list",+ "codex_config_read" => "config/read",+ _ => return None,+ })+}+fn plugin_page(result: Value, query: &str, offset: usize, limit: usize) -> Value {+ let mut plugins = Vec::new();+ for marketplace in result["marketplaces"].as_array().unwrap_or(&vec![]) {+ for p in marketplace["plugins"].as_array().unwrap_or(&vec![]) {+ let searchable = format!(+ "{} {} {} {}",+ p["name"],+ p["id"],+ p["interface"]["displayName"],+ p["interface"]["shortDescription"]+ )+ .to_ascii_lowercase();+ if !searchable.contains(query) {+ continue;+ }+ plugins.push(json!({"name":p["name"],"id":p["id"],"installed":p["installed"],"enabled":p["enabled"],+ "availability":p["availability"],"marketplaceName":marketplace["name"],"marketplacePath":marketplace["path"]}));+ }+ }+ let total = plugins.len();+ let next =+ (offset.saturating_add(limit) < total).then(|| offset.saturating_add(limit).to_string());+ json!({"plugins":plugins.into_iter().skip(offset).take(limit).collect::<Vec<_>>(),"total":total,"nextCursor":next,"marketplaceLoadErrors":result["marketplaceLoadErrors"]})+}+fn is_job(verb: &str) -> bool {+ matches!(+ verb,+ "codex_desktop_install"+ | "codex_desktop_uninstall"+ | "codex_desktop_open"+ | "codex_plugin_install"+ | "codex_plugin_uninstall"+ | "codex_login_start"+ | "codex_connect"+ | "codex_desktop_tool_call"+ )+}+fn main() {+ let args: Vec<String> = env::args().collect();+ if args.iter().any(|a| a == "--version") {+ println!("codex-bridge {VERSION}");+ return;+ }+ let port = args+ .windows(2)+ .find(|a| a[0] == "--port")+ .map(|a| a[1].parse::<u16>().expect("valid port"))+ .unwrap_or(0);+ let host = env::var("ADOM_BIND_HOST").unwrap_or("127.0.0.1".into());+ let ip: std::net::IpAddr = host.parse().expect("ADOM_BIND_HOST must be an IP address");+ assert!(ip.is_loopback(), "bridge must bind loopback");+ let server = Server::http((host.as_str(), port)).expect("bind bridge");+ eprintln!("codex-bridge {VERSION} listening {}", server.server_addr());+ let state = Arc::new(State {+ rpc: Mutex::new(None),+ owner: Mutex::new(None),+ jobs: Mutex::new(HashMap::new()),+ mutation: Mutex::new(()),+ });+ for request in server.incoming_requests() {+ let state = state.clone();+ thread::spawn(move || serve(request, state));+ }+}+fn serve(mut req: Request, state: Arc<State>) {+ let send = |req: Request, status: u16, v: Value| {+ let response = Response::from_string(finish(v).to_string())+ .with_status_code(status)+ .with_header(Header::from_bytes("Content-Type", "application/json").unwrap());+ let _ = req.respond(response);+ };+ if req.method() == &Method::Get && matches!(req.url(), "/health" | "/status") {+ send(+ req,+ 200,+ json!({"led":"green","summary":"Rust bridge ready"}),+ );+ return;+ }+ if req.method() != &Method::Post || req.url() != "/command" {+ send(req, 404, failure("not_found"));+ return;+ }+ if req.headers().iter().any(|h| h.field.equiv("Origin")) {+ send(req, 403, failure("browser_origin_refused"));+ return;+ }+ let mut caller = Caller::default();+ for header in req.headers() {+ let name = header.field.as_str().as_str().to_ascii_lowercase();+ if name.starts_with("x-adom-caller-") {+ caller.headers.insert(name, header.value.as_str().into());+ }+ }+ caller.thread = caller+ .headers+ .get("x-adom-caller-thread")+ .cloned()+ .unwrap_or_default();+ let mut raw = String::new();+ if req+ .as_reader()+ .take(1_048_577)+ .read_to_string(&mut raw)+ .is_err()+ || raw.len() > 1_048_576+ {+ send(req, 400, failure("body_too_large_or_invalid"));+ return;+ }+ let command: Command = match serde_json::from_str(&raw) {+ Ok(c) => c,+ Err(_) => {+ send(req, 400, failure("invalid_json"));+ return;+ }+ };+ if caller.thread.trim().is_empty() {+ caller.thread = command.args["caller"]["aiThread"]+ .as_str()+ .unwrap_or("")+ .to_string();+ caller+ .headers+ .insert("x-adom-caller-thread".into(), caller.thread.clone());+ }+ if caller.thread.trim().is_empty() {+ send(req, 400, failure("caller_identity_required"));+ return;+ }+ eprintln!("[verb] {} caller={:?}", command.command, caller.thread);+ if is_job(&command.command) {+ let mut jobs = state.jobs.lock().unwrap();+ if let Some((id, _)) = jobs+ .iter()+ .find(|(_, j)| j.result.is_none() && j.verb == command.command)+ {+ send(+ req,+ 200,+ json!({"success":false,"errorCode":"operation_in_progress","jobId":id,"stillRunning":true,"_hint":"Poll codex_progress with the existing jobId."}),+ );+ return;+ }+ jobs.retain(|_, j| j.result.is_none() || j.started.elapsed() < Duration::from_secs(3600));+ if jobs.len() >= 100 {+ send(req, 429, failure("job_limit"));+ return;+ }+ let id = uuid::Uuid::new_v4().to_string();+ jobs.insert(+ id.clone(),+ Job {+ owner: caller.thread.clone(),+ verb: command.command.clone(),+ started: Instant::now(),+ result: None,+ },+ );+ drop(jobs);+ let worker = state.clone();+ let job_id = id.clone();+ thread::spawn(move || {+ let _guard = worker.mutation.lock().unwrap();+ let result = worker+ .dispatch(&command.command, command.args, &caller)+ .unwrap_or_else(|e| failure(&e));+ if let Some(job) = worker.jobs.lock().unwrap().get_mut(&job_id) {+ job.result = Some(finish(result));+ }+ });+ send(+ req,+ 200,+ json!({"jobId":id,"stillRunning":true,"state":"running","_next":["codex_progress"],"_hint":"Poll codex_progress with this jobId. Do not resubmit the operation."}),+ );+ } else {+ let _guard = if matches!(+ command.command.as_str(),+ "codex_disconnect" | "codex_uninstall" | "codex_auth_import"+ ) {+ match state.mutation.try_lock() {+ Ok(g) => Some(g),+ Err(_) => {+ send(req,200,failure("operation_in_progress: poll codex_progress before changing connection or credentials"));+ return;+ }+ }+ } else {+ None+ };+ let result = state+ .dispatch(&command.command, command.args, &caller)+ .unwrap_or_else(|e| failure(&e));+ send(req, 200, result);+ }+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn credential_fields_are_redacted_recursively() {+ let mut v = json!({"nested":[{"access_token":"secret","refreshToken":"secret","id_token":"secret"}],"account":{"type":"chatgpt"}});+ redact(&mut v);+ assert!(!v.to_string().contains("secret"));+ assert_eq!(v["account"]["type"], "chatgpt");+ }+ #[test]+ fn semantic_failure_in_nested_ab_response_is_not_success() {+ assert!(normalize(+ json!({"success":true,"output":"{\"success\":false,\"errorCode\":\"blocked\"}"})+ )+ .is_err());+ }+ #[test]+ fn no_arbitrary_rpc_or_automatic_logout() {+ assert_eq!(rpc_method("account/logout"), None);+ assert_eq!(rpc_method("codex_logout"), None);+ assert_eq!(rpc_method("codex_plugin_install"), Some("plugin/install"));+ }+ #[test]+ fn all_catalog_commands_route() {+ let v: Value = serde_json::from_str(CATALOG).unwrap();+ for verb in v["verbs"].as_array().unwrap() {+ let name = verb["name"].as_str().unwrap();+ assert!(+ rpc_method(name).is_some()+ || matches!(+ name,+ "codex_describe"+ | "codex_status"+ | "codex_readiness"+ | "codex_state"+ | "codex_progress"+ | "codex_desktop_install"+ | "codex_desktop_uninstall"+ | "codex_desktop_open"+ | "codex_connect"+ | "codex_disconnect"+ | "codex_uninstall"+ | "codex_auth_import"+ | "codex_events"+ | "codex_requests"+ | "codex_request_reply"+ | "codex_desktop_tools"+ | "codex_desktop_tool_call"+ ),+ "Unrouted {name}"+ );+ }+ let manifest: Value = serde_json::from_str(include_str!("../bridge.json")).unwrap();+ let catalog: std::collections::HashSet<_> = v["verbs"]+ .as_array()+ .unwrap()+ .iter()+ .map(|v| v["name"].as_str().unwrap())+ .collect();+ let declared: std::collections::HashSet<_> = manifest["verbs"]+ .as_array()+ .unwrap()+ .iter()+ .map(|v| v.as_str().unwrap())+ .collect();+ assert_eq!(catalog, declared);+ }+ #[test]+ fn plugin_pages_are_bounded_and_filter_before_pagination() {+ let r = json!({"marketplaces":[{"name":"test","path":"/marketplace.json","plugins":[{"name":"alpha","id":"alpha@test"},{"name":"beta","id":"beta@test"},{"name":"alphabet","id":"alphabet@test"}]}]});+ let p = plugin_page(r, "alpha", 0, 1);+ assert_eq!(p["total"], 2);+ assert_eq!(p["plugins"].as_array().unwrap().len(), 1);+ assert_eq!(p["nextCursor"], "1");+ }+ #[test]+ fn desktop_version_is_not_overwritten_by_bridge_version() {+ let r = finish(json!({"version":"26.901.6511.0"}));+ assert_eq!(r["version"], "26.901.6511.0");+ assert_eq!(r["bridgeVersion"], VERSION);+ }+}
bridge/src/process.rsadded+112@@ -0,0 +1,112 @@+use serde_json::{json, Value};+use std::{+ io::{Read, Write},+ process::{Command, Stdio},+ thread,+ time::{Duration, Instant},+};++pub fn hidden(command: &mut Command) -> &mut Command {+ #[cfg(windows)]+ {+ use std::os::windows::process::CommandExt;+ command.creation_flags(0x08000000);+ }+ command+}++// Drain both pipes while waiting: a full stderr pipe must never wedge a job.+pub fn run(mut command: Command, input: Option<&[u8]>, timeout: Duration) -> Result<Value, String> {+ hidden(&mut command)+ .stdin(Stdio::piped())+ .stdout(Stdio::piped())+ .stderr(Stdio::piped());+ let mut child = command.spawn().map_err(|e| e.to_string())?;+ let mut stdin = child.stdin.take().unwrap();+ if let Some(input) = input {+ stdin.write_all(input).map_err(|e| e.to_string())?;+ }+ drop(stdin);+ let stdout = child.stdout.take().unwrap();+ let stderr = child.stderr.take().unwrap();+ fn drain(mut stream: impl Read) -> Vec<u8> {+ let mut saved = Vec::new();+ let mut buf = [0; 8192];+ while let Ok(n) = stream.read(&mut buf) {+ if n == 0 {+ break;+ }+ let keep = n.min(2_000_000usize.saturating_sub(saved.len()));+ saved.extend_from_slice(&buf[..keep]);+ }+ saved+ }+ let (out_tx, out_rx) = std::sync::mpsc::channel();+ let (err_tx, err_rx) = std::sync::mpsc::channel();+ thread::spawn(move || {+ let _ = out_tx.send(drain(stdout));+ });+ thread::spawn(move || {+ let _ = err_tx.send(drain(stderr));+ });+ let started = Instant::now();+ let mut timed_out = false;+ let status = loop {+ if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {+ break status;+ }+ if started.elapsed() > timeout {+ timed_out = true;+ let _ = child.kill();+ break child.wait().map_err(|e| e.to_string())?;+ }+ thread::sleep(Duration::from_millis(100));+ };+ // A launched descendant can inherit pipes after the direct child exits.+ let out = out_rx.recv_timeout(Duration::from_secs(2));+ let err = err_rx.recv_timeout(Duration::from_secs(2));+ let incomplete = out.is_err() || err.is_err();+ Ok(+ json!({"success":status.success()&&!timed_out&&!incomplete,"exitCode":status.code(),+ "timedOut":timed_out,"outputIncomplete":incomplete,"stdout":String::from_utf8_lossy(&out.unwrap_or_default()),+ "stderr":String::from_utf8_lossy(&err.unwrap_or_default())}),+ )+}++pub fn powershell(script: &str, timeout: Duration) -> Result<Value, String> {+ use base64::Engine;+ let bytes: Vec<u8> = script.encode_utf16().flat_map(u16::to_le_bytes).collect();+ let mut cmd = Command::new("powershell.exe");+ cmd.args([+ "-NoLogo",+ "-NoProfile",+ "-NonInteractive",+ "-EncodedCommand",+ &base64::engine::general_purpose::STANDARD.encode(bytes),+ ]);+ run(cmd, None, timeout)+}++pub fn ps_quote(s: &str) -> String {+ format!("'{}'", s.replace('\'', "''"))+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn quotes_remain_literal() {+ assert_eq!(ps_quote("a'$(whoami);b"), "'a''$(whoami);b'");+ }+ #[cfg(unix)]+ #[test]+ fn drains_both_streams_and_reports_failure() {+ let mut c = Command::new("sh");+ c.args(["-c", "printf result; printf problem >&2; exit 7"]);+ let r = run(c, None, Duration::from_secs(2)).unwrap();+ assert_eq!(r["exitCode"], 7);+ assert_eq!(r["success"], false);+ assert_eq!(r["stdout"], "result");+ assert_eq!(r["stderr"], "problem");+ }+}
bridge/src/rpc.rsadded+268@@ -0,0 +1,268 @@+use serde_json::{json, Value};+use std::{+ collections::{HashMap, HashSet, VecDeque},+ io::{BufRead, BufReader, Write},+ path::Path,+ process::{Child, ChildStdin, Command, Stdio},+ sync::{+ atomic::{AtomicBool, AtomicU64, Ordering},+ mpsc, Arc, Mutex,+ },+ thread,+ time::Duration,+};++pub struct Rpc {+ child: Mutex<Child>,+ input: Mutex<ChildStdin>,+ pending: Mutex<HashMap<u64, mpsc::Sender<Value>>>,+ requests: Mutex<HashMap<String, Value>>,+ events: Mutex<VecDeque<Value>>,+ active_turns: Mutex<HashSet<String>>,+ next: AtomicU64,+ sequence: AtomicU64,+ alive: AtomicBool,+ pub mode: String,+}++impl Rpc {+ pub fn start(executable: &Path, proxy: bool) -> Result<Arc<Self>, String> {+ let mut cmd = Command::new(executable);+ cmd.arg("app-server");+ if proxy {+ cmd.arg("proxy");+ } else {+ cmd.arg("--stdio");+ }+ Self::start_command(cmd, proxy)+ }+ fn start_command(mut cmd: Command, proxy: bool) -> Result<Arc<Self>, String> {+ super::process::hidden(&mut cmd)+ .stdin(Stdio::piped())+ .stdout(Stdio::piped())+ .stderr(Stdio::null());+ let mut child = cmd.spawn().map_err(|e| e.to_string())?;+ let input = child.stdin.take().unwrap();+ let output = child.stdout.take().unwrap();+ let rpc = Arc::new(Self {+ child: Mutex::new(child),+ input: Mutex::new(input),+ pending: Mutex::new(HashMap::new()),+ requests: Mutex::new(HashMap::new()),+ events: Mutex::new(VecDeque::new()),+ active_turns: Mutex::new(HashSet::new()),+ next: AtomicU64::new(1),+ sequence: AtomicU64::new(0),+ alive: AtomicBool::new(true),+ mode: if proxy {+ "desktop-daemon-proxy"+ } else {+ "bridge-owned-app-server"+ }+ .into(),+ });+ let weak = Arc::downgrade(&rpc);+ thread::spawn(move || {+ for line in BufReader::new(output).lines() {+ let Ok(line) = line else {+ break;+ };+ let Ok(value) = serde_json::from_str::<Value>(&line) else {+ continue;+ };+ let Some(rpc) = weak.upgrade() else {+ break;+ };+ if value.get("method").is_none() {+ if let Some(id) = value["id"].as_u64() {+ if let Some(tx) = rpc.pending.lock().unwrap().remove(&id) {+ let _ = tx.send(value);+ }+ }+ } else {+ if let Some(thread_id) = value["params"]["threadId"].as_str() {+ match value["method"].as_str() {+ Some("turn/started") => {+ rpc.active_turns+ .lock()+ .unwrap()+ .insert(thread_id.to_string());+ }+ Some("turn/completed") => {+ rpc.active_turns.lock().unwrap().remove(thread_id);+ }+ _ => {}+ }+ }+ if value.get("id").is_some() {+ rpc.requests+ .lock()+ .unwrap()+ .insert(value["id"].to_string(), value.clone());+ }+ let seq = rpc.sequence.fetch_add(1, Ordering::Relaxed) + 1;+ let mut events = rpc.events.lock().unwrap();+ events.push_back(json!({"seq":seq,"event":value}));+ while events.len() > 1000 {+ events.pop_front();+ }+ }+ }+ if let Some(rpc) = weak.upgrade() {+ rpc.alive.store(false, Ordering::Relaxed);+ rpc.pending.lock().unwrap().clear();+ }+ });+ let init = rpc.call("initialize", json!({"clientInfo":{"name":"adom_codex_bridge","title":"Adom Codex Bridge","version":env!("CARGO_PKG_VERSION")},"capabilities":{"experimentalApi":true}}), Duration::from_secs(15));+ if let Err(e) = init {+ rpc.stop();+ return Err(e);+ }+ rpc.write(json!({"method":"initialized","params":{}}))?;+ Ok(rpc)+ }+ fn write(&self, value: Value) -> Result<(), String> {+ let mut input = self.input.lock().unwrap();+ writeln!(input, "{value}")+ .and_then(|_| input.flush())+ .map_err(|e| e.to_string())+ }+ pub fn call(&self, method: &str, params: Value, timeout: Duration) -> Result<Value, String> {+ if !self.alive.load(Ordering::Relaxed) {+ return Err("app_server_disconnected".into());+ }+ let id = self.next.fetch_add(1, Ordering::Relaxed);+ let (tx, rx) = mpsc::channel();+ self.pending.lock().unwrap().insert(id, tx);+ if let Err(e) = self.write(json!({"id":id,"method":method,"params":params})) {+ self.pending.lock().unwrap().remove(&id);+ return Err(e);+ }+ let response = rx.recv_timeout(timeout);+ self.pending.lock().unwrap().remove(&id);+ let v = response.map_err(|e| {+ format!("rpc_wait_failed: {e}; inspect codex_events and codex_requests before retrying")+ })?;+ if let Some(err) = v.get("error") {+ return Err(format!("app_server_error: {err}"));+ }+ Ok(v["result"].clone())+ }+ pub fn events(&self, since: u64) -> Value {+ let events = self.events.lock().unwrap();+ json!({"events":events.iter().filter(|e| e["seq"].as_u64().unwrap_or(0)>since).collect::<Vec<_>>(),+ "latestSeq":self.sequence.load(Ordering::Relaxed),"oldestSeq":events.front().map(|v|&v["seq"]),+ "alive":self.alive.load(Ordering::Relaxed),"mode":self.mode})+ }+ pub fn requests(&self) -> Value {+ json!(self.requests.lock().unwrap().values().collect::<Vec<_>>())+ }+ pub fn reply(&self, id: Value, result: Value) -> Result<Value, String> {+ let mut pending = self.requests.lock().unwrap();+ if !pending.contains_key(&id.to_string()) {+ return Err("unknown_request_id".into());+ }+ self.write(json!({"id":id,"result":result}))?;+ pending.remove(&id.to_string());+ Ok(json!({"replied":true}))+ }+ pub fn stop(&self) {+ self.alive.store(false, Ordering::Relaxed);+ let mut child = self.child.lock().unwrap();+ let _ = child.kill();+ let _ = child.wait();+ }+ pub fn alive(&self) -> bool {+ self.alive.load(Ordering::Relaxed)+ }+ pub fn busy(&self) -> bool {+ !self.active_turns.lock().unwrap().is_empty()+ || !self.requests.lock().unwrap().is_empty()+ || !self.pending.lock().unwrap().is_empty()+ }+}++impl Drop for Rpc {+ fn drop(&mut self) {+ self.stop();+ }+}++#[cfg(test)]+mod tests {+ use super::*;+ #[test]+ fn mock_app_server() {+ if std::env::var_os("CODEX_BRIDGE_TEST_SERVER").is_none() {+ return;+ }+ for line in std::io::stdin().lock().lines() {+ let v: Value = serde_json::from_str(&line.unwrap()).unwrap();+ let output = match v["method"].as_str() {+ Some("initialize") => json!({"id":v["id"],"result":{}}),+ Some("test/echo") => json!({"id":v["id"],"result":v["params"]}),+ Some("test/error") => {+ json!({"id":v["id"],"error":{"code":-1,"message":"expected failure"}})+ }+ Some("test/request") => {+ println!(+ "\n{}",+ json!({"id":"approval-1","method":"item/commandExecution/requestApproval","params":{}})+ );+ json!({"id":v["id"],"result":{}})+ }+ None if v["id"] == "approval-1" => {+ json!({"method":"test/replied","params":v["result"]})+ }+ _ => continue,+ };+ println!("\n{output}");+ std::io::stdout().flush().unwrap();+ }+ }+ #[test]+ fn native_rpc_correlates_concurrent_calls_errors_and_approval_replies() {+ let mut cmd = Command::new(std::env::current_exe().unwrap());+ cmd.args(["--exact", "rpc::tests::mock_app_server", "--nocapture"])+ .env("CODEX_BRIDGE_TEST_SERVER", "1");+ let rpc = Rpc::start_command(cmd, false).unwrap();+ let calls: Vec<_> = (0..8)+ .map(|i| {+ let rpc = rpc.clone();+ thread::spawn(move || {+ assert_eq!(+ rpc.call("test/echo", json!({"index":i}), Duration::from_secs(2))+ .unwrap()["index"],+ i+ )+ })+ })+ .collect();+ for call in calls {+ call.join().unwrap();+ }+ assert!(rpc+ .call("test/error", json!({}), Duration::from_secs(2))+ .unwrap_err()+ .contains("expected failure"));+ rpc.call("test/request", json!({}), Duration::from_secs(2))+ .unwrap();+ assert!(rpc.busy());+ assert_eq!(rpc.requests().as_array().unwrap().len(), 1);+ assert!(rpc.reply(json!("wrong"), json!({})).is_err());+ rpc.reply(json!("approval-1"), json!({"decision":"decline"}))+ .unwrap();+ assert!(!rpc.busy());+ let deadline = std::time::Instant::now() + Duration::from_secs(2);+ while rpc.events(0)["latestSeq"].as_u64().unwrap() < 2 {+ assert!(std::time::Instant::now() < deadline);+ thread::sleep(Duration::from_millis(10));+ }+ assert_eq!(rpc.events(1)["events"].as_array().unwrap().len(), 1);+ rpc.stop();+ assert!(!rpc.alive());+ assert!(rpc+ .call("test/echo", json!({}), Duration::from_secs(1))+ .is_err());+ }+}
bridge/tests/http.rsadded+75@@ -0,0 +1,75 @@+use serde_json::{json, Value};+use std::{+ net::TcpListener,+ process::{Child, Command, Stdio},+ thread,+ time::{Duration, Instant},+};++struct Server(Child);+impl Drop for Server {+ fn drop(&mut self) {+ let _ = self.0.kill();+ let _ = self.0.wait();+ }+}++#[test]+fn loopback_health_identity_origin_and_catalog_contract() {+ let listener = TcpListener::bind("127.0.0.1:0").unwrap();+ let port = listener.local_addr().unwrap().port();+ drop(listener);+ let _server = Server(+ Command::new(env!("CARGO_BIN_EXE_codex-bridge"))+ .args(["--port", &port.to_string()])+ .env("ADOM_BIND_HOST", "127.0.0.1")+ .stdout(Stdio::null())+ .stderr(Stdio::null())+ .spawn()+ .unwrap(),+ );+ let base = format!("http://127.0.0.1:{port}");+ let agent = ureq::AgentBuilder::new()+ .timeout(Duration::from_secs(2))+ .build();+ let deadline = Instant::now() + Duration::from_secs(5);+ loop {+ if agent.get(&format!("{base}/health")).call().is_ok() {+ break;+ }+ assert!(Instant::now() < deadline);+ thread::sleep(Duration::from_millis(20));+ }+ let url = format!("{base}/command");+ let body = json!({"command":"codex_describe","args":{}});+ assert!(matches!(+ agent.post(&url).send_json(body.clone()),+ Err(ureq::Error::Status(400, _))+ ));+ assert!(matches!(+ agent+ .post(&url)+ .set("X-Adom-Caller-Thread", "test")+ .set("Origin", "https://example.com")+ .send_json(body.clone()),+ Err(ureq::Error::Status(403, _))+ ));+ let catalog: Value = agent+ .post(&url)+ .set("X-Adom-Caller-Thread", "test")+ .send_json(body)+ .unwrap()+ .into_json()+ .unwrap();+ assert_eq!(catalog["success"], true);+ assert_eq!(catalog["verbs"].as_array().unwrap().len(), 40);+ let unknown: Value = agent+ .post(&url)+ .set("X-Adom-Caller-Thread", "test")+ .send_json(json!({"command":"codex_arbitrary_shell","args":{}}))+ .unwrap()+ .into_json()+ .unwrap();+ assert_eq!(unknown["success"], false);+ assert_eq!(unknown["errorCode"], "unknown_command");+}
bridge/verbs.jsonadded+456@@ -0,0 +1,456 @@+{+ "bridge": "codex",+ "version": "0.1.0",+ "verbs": [+ {+ "name": "codex_describe",+ "summary": "Describe every bridge command",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_status",+ "summary": "Read desktop installation and connection state",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_readiness",+ "summary": "Read prerequisites without installing anything",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_state",+ "summary": "Observe app windows and capture each through AB",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_progress",+ "summary": "Poll a long-running operation",+ "input": {+ "jobId": "optional string; omit to list this caller's jobs"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_desktop_install",+ "summary": "Install official Microsoft Store package 9PLM9XGG6VKS",+ "input": {+ "acceptAgreements": "boolean; true only after user acceptance"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_desktop_uninstall",+ "summary": "Remove desktop package only; preserve Codex home",+ "input": {+ "confirm": "boolean, required true; close desktop first"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_desktop_open",+ "summary": "Launch desktop and verify a window; Windows may foreground it",+ "input": {},+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_connect",+ "summary": "Connect to the desktop daemon or a separate bundled backend",+ "input": {+ "mode": "standalone (default) | desktop (Unix daemon only)"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_disconnect",+ "summary": "Disconnect this bridge's backend transport",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_uninstall",+ "summary": "AB uninstall cascade; preserve desktop, auth, projects and plugins",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_auth_import",+ "summary": "Import a staged auth cache without returning its contents",+ "input": {+ "path": "absolute host-local auth cache path; refuses existing destination"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_events",+ "summary": "Read bounded native event stream",+ "input": {+ "sinceSeq": "integer cursor; compare oldestSeq to detect missed events"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_requests",+ "summary": "Read native approval and user-input requests",+ "input": {},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_request_reply",+ "summary": "Reply to a pending request with the native response shape",+ "input": {+ "requestId": "native JSON id",+ "result": "native JSON response; user authorization required for approvals"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_account_read",+ "summary": "Read account identity",+ "input": {+ "refreshToken": "boolean, default false"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_login_start",+ "summary": "Start native browser/device-code login",+ "input": {+ "type": "chatgpt | chatgptDeviceCode"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_login_cancel",+ "summary": "Cancel this login attempt",+ "input": {+ "loginId": "string"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_models",+ "summary": "List native available models",+ "input": {+ "cursor": "optional string",+ "limit": "optional integer"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_list",+ "summary": "List native threads",+ "input": {+ "cursor": "optional string",+ "limit": "optional integer",+ "sourceKinds": "optional array"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_read",+ "summary": "Read native thread state",+ "input": {+ "threadId": "string, required",+ "includeTurns": "boolean"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_start",+ "summary": "Create a conversation",+ "input": {+ "cwd": "absolute host directory",+ "model": "optional model",+ "approvalPolicy": "on-request recommended",+ "sandbox": "read-only | workspace-write"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_resume",+ "summary": "Subscribe to an existing conversation",+ "input": {+ "threadId": "string, required"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_fork",+ "summary": "Fork an existing conversation",+ "input": {+ "threadId": "string, required"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_thread_archive",+ "summary": "Archive an existing conversation",+ "input": {+ "threadId": "string, required"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_turn_start",+ "summary": "Submit a prompt",+ "input": {+ "threadId": "string, required",+ "input": "array, e.g. [{\"type\":\"text\",\"text\":\"...\"}]"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_turn_steer",+ "summary": "Steer an active turn",+ "input": {+ "threadId": "string",+ "expectedTurnId": "string",+ "input": "native input array"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_turn_interrupt",+ "summary": "Interrupt an active turn",+ "input": {+ "threadId": "string",+ "turnId": "string"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_plugin_list",+ "summary": "List plugin marketplaces and available plugins",+ "input": {+ "forceRefetch": "optional boolean",+ "cwds": "optional absolute directory array",+ "query": "optional case-insensitive search",+ "cursor": "optional offset from nextCursor",+ "limit": "optional integer, default 30, maximum 100"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_plugin_read",+ "summary": "Inspect a plugin",+ "input": {+ "pluginName": "string",+ "marketplacePath": "absolute local marketplace path, or use remoteMarketplaceName",+ "remoteMarketplaceName": "remote catalog marketplace name"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_plugin_install",+ "summary": "Install a named plugin through native Codex",+ "input": {+ "pluginName": "string",+ "remoteMarketplaceName": "string, or marketplacePath",+ "marketplacePath": "absolute local marketplace path"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_plugin_uninstall",+ "summary": "Remove a named native plugin",+ "input": {+ "pluginId": "string"+ },+ "timeoutSeconds": 5,+ "longRunning": true,+ "statusVerb": "codex_progress",+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_skills_list",+ "summary": "List native skills",+ "input": {+ "cwds": "optional absolute directory array"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_mcp_status",+ "summary": "Read native MCP server connection status",+ "input": {+ "cursor": "optional string",+ "limit": "optional integer"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_config_read",+ "summary": "Read effective native config",+ "input": {+ "includeLayers": "optional boolean"+ },+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null,+ "example": {},+ "hint": "Native arguments are forwarded as structured JSON. Poll events and pending requests for progress."+ },+ {+ "name": "codex_desktop_tools",+ "summary": "Discover the running desktop app's native tools and JSON schemas",+ "input": {"desktopPipe": "optional verified endpoint name; required when discovery is ambiguous"},+ "timeoutSeconds": 150,+ "longRunning": false,+ "statusVerb": null+ },+ {+ "name": "codex_desktop_tool_call",+ "summary": "Invoke a discovered desktop tool through its verified named pipe",+ "input": {+ "tool": "native name from codex_desktop_tools",+ "threadId": "native Codex thread ID",+ "arguments": "native JSON object",+ "turnId": "optional native turn ID"+ ,"desktopPipe": "optional verified endpoint name; required when discovery is ambiguous"+ },+ "timeoutSeconds": 10,+ "longRunning": true,+ "statusVerb": "codex_progress"+ },+ {"name":"codex_thread_unsubscribe","summary":"Release a backend thread subscription","input":{"threadId":"string, required"},"timeoutSeconds":150,"longRunning":false,"statusVerb":null},+ {"name":"codex_thread_turns","summary":"Read paginated persisted turns","input":{"threadId":"string, required","cursor":"optional string","limit":"optional integer","itemsView":"notLoaded | summary | full"},"timeoutSeconds":150,"longRunning":false,"statusVerb":null},+ {"name":"codex_thread_items","summary":"Read paginated persisted thread items","input":{"threadId":"string, required","turnId":"optional string","cursor":"optional string","limit":"optional integer"},"timeoutSeconds":150,"longRunning":false,"statusVerb":null}+ ]+}
docs/DESKTOP-BRIDGE.md+132−6@@ -1,11 +1,137 @@-# Codex Desktop and a bidirectional Adom Bridge+# Codex Desktop Bridge -Planned after the initial Hydrogen integration and routing demonstrations.+`bridge/` is the compiled Rust implementation owned by **adom/codex**. AB runs+`codex-bridge.exe` as a persistent loopback HTTP bridge. No Node or Python runtime+is needed on the target. Windows package management uses built-in PowerShell and+WinGet; the backend executable comes from the installed desktop app. -The intended dock flow is: discover the selected host and supported OS, inspect whether Codex Desktop is installed, obtain an official supported installer, verify its publisher and version, install with visible progress, and open it. Existing installations and authentication must be preserved. The user handles account sign-in and agreement acceptance.+## Build and deploy -A Codex bridge would let Hydrogen discover the desktop app, open it, observe its windows/session state, and submit explicitly authorized work. The return path must correlate job/session IDs and deliver progress and results back to the originating Hydrogen thread. Start with supported APIs or protocol integration where available; use scoped UIA as a measured fallback. Do not assume Grok Bot's webhook contract exists in Codex Desktop.+Run `bash bridge/build.sh`. The build tests and creates+`bridge/dist/codex-bridge-windows-x64.zip` with `bridge.json` and the executable at+its root, plus SHA256SUMS. Linux cross-building needs the Rust target+`x86_64-pc-windows-gnu`, MinGW-w64, zip and sha256sum. -Proposed verbs such as `codex_desktop_status`, `codex_desktop_open`, and `codex_session_status` are design candidates, not callable features. Before implementing them, inspect current official Codex Desktop capabilities and Bridge SDK contracts, then test the connection on a dedicated host.+The candidate is dev-pinned on **arav-rog**, not AdomLapper. There is no public+auto-update manifest until review. For a development deployment, discover the+host's LOCALAPPDATA, stage the ZIP members under+`Adom Bridge/bridges-cache/codex` with a `.dev-pin` marker using AB's SDK workflow,+then `refresh_bridges {"name":"codex"}` and `bridge_list`. Never overwrite another+developer's pin. For updates, finish jobs, disconnect, pause and kill only this+bridge, transfer files, then resume/rescan. Production distribution should publish+the ZIP and a SHA256-bearing manifest on adom/codex for `bridge_install`. -This version does not ship a Codex Desktop installer or a desktop bridge, and does not imply that the routing recordings used Codex Desktop. Those recordings were orchestrated by Astra inside Adom Hydrogen.+Always pass an explicit target and recognizable caller identity:++```sh+ab --target arav-rog --ai-thread codex-desktop-bridge codex_status '{}'+ab --target arav-rog --ai-thread codex-desktop-bridge codex_describe '{}'+ab --target arav-rog --ai-thread codex-desktop-bridge codex_desktop_install '{}'+ab --target arav-rog --ai-thread codex-desktop-bridge codex_progress '{"jobId":"..."}'+ab --target arav-rog --ai-thread codex-desktop-bridge codex_desktop_open '{}'+```++Long operations return a job ID immediately. Poll until `stillRunning:false`,+then check **`result.success`**. Outer success only means the status query worked.+Omit jobId to list your jobs. Jobs remain in this bridge process for one hour;+they do not survive restart. Inspect native state before retrying a timed-out+mutation. Native events have sequence cursors and a bounded buffer.++The official Store ID `9PLM9XGG6VKS` is currently titled **ChatGPT** and includes+Codex (package `OpenAI.Codex`). Only after actual user acceptance, pass+`acceptAgreements:true`. Existing installations are left intact. Launch may+foreground the app according to Windows activation behavior; observation does not.++## Sign-in++Reuse the user's own native `$CODEX_HOME/auth.json` (usually `~/.codex/auth.json`)+via an authorized AB `send_files` transfer to a private host staging directory.+Never put token values in command arguments, chat, wiki files or logs. Call+`codex_auth_import {"path":"<absolute host staging path>"}` while disconnected.+Import refuses existing destination credentials and applies an owner-only ACL+before writing contents. Delete the staging copy immediately after import.+Verify `codex_connect` / `codex_account_read` and the visible app. An app already+open before import may need normal quit/relaunch. Preserve active tasks/drafts.++This uses the documented cached-auth mechanism, not VS Code credential scraping+or an SSO guarantee. Token refresh/revocation may require another native login.+Without a transferable cache, `codex_login_start` supports native `chatgpt` and+`chatgptDeviceCode` login; give the URL/code to the user and wait for authentication.++## Two distinct connections++**Desktop-native:** `codex_desktop_tools` discovers the live app's schemas.+`codex_desktop_tool_call` invokes them over length-prefixed JSON-RPC on a native+Windows pipe. Rust verifies the pipe's server PID against the installed package's+visible desktop process. This is the actual app, not a CLI mislabeled as desktop.++```json+{"command":"codex_desktop_tool_call","args":{"tool":"read_thread","threadId":"<native-thread-id>","arguments":{"threadId":"<native-thread-id>","turnLimit":1}}}+```++Multiple verified pipes cause a clear ambiguity error listing endpoint names.+Pass `desktopPipe` to select one explicitly; rediscover after app restart. A native+thread ID is caller context, not an AB thread name. Discover IDs using backend+`codex_thread_list`, or create a backend seed for an authorized initial test.+Do not resume desktop-owned active threads in the separate backend.++The tested desktop exposes projects, create/read/send/wait, navigation, pin/archive,+worktree forks/handoff, panels and automations. Discover schemas and honor their+instructions. Do not speculatively create schedules, enable voice, fork work or+approve native requests. This experimental native pipe is not a documented stable+public API. A successful navigation acknowledgment is not proof of a visible+screen change; verify with `codex_state` or scoped AB capture.++**App-server backend:** `codex_connect` starts the activated desktop-bundled+`codex.exe app-server --stdio` as a separate bridge-owned backend sharing Codex+home. Windows has no Unix daemon proxy. This supports accounts, plugins, skills,+MCP, config and thread/turn RPCs, but does not share desktop-owned active sessions.+`mode:desktop` explicitly fails on Windows instead of silently falling back.++`codex_events {"sinceSeq":0}` returns events; compare oldestSeq to detect gaps.+`codex_requests` exposes native approvals/user-input requests. Reply via+`codex_request_reply` only after authorization, with the native response shape.+Disconnect refuses active turns or pending requests. Interrupt only owned work.++## Plugins++Discover with `codex_plugin_list {"query":"latex","limit":10}` and nextCursor.+Pagination is applied after native catalog retrieval. Read details first, including+availability, install policy, terms, interstitial requirements and connected apps.++```json+{"command":"codex_plugin_read","args":{"pluginName":"latex","marketplacePath":"<path returned by list>"}}+{"command":"codex_plugin_install","args":{"pluginName":"latex","marketplacePath":"<path returned by list>"}}+{"command":"codex_plugin_uninstall","args":{"pluginId":"latex@openai-bundled"}}+```++Remote catalogs use `remoteMarketplaceName` instead of marketplacePath. Poll jobs+and read back `result.plugin.summary.installed`. `appsNeedingAuth` means the user+still needs to authenticate; do not auto-approve OAuth or claim usability early.++## Removal and boundaries++`codex_desktop_uninstall {"confirm":true}` removes only the selected Windows+desktop package. First disconnect and normally quit the app, including its tray+process. Running package processes block removal. Credentials, plugins, projects+and `.codex` are deliberately preserved; data deletion is a separate decision.++AB `bridge_uninstall {"name":"codex"}` invokes `codex_uninstall` as a lifecycle+cascade. It stops only the bridge-owned backend, never desktop or user data.+The HTTP listener is loopback-only and rejects browser Origin requests. Caller+identity is AB attribution, not a security boundary against same-user processes.++## Verified baseline++Test host arav-rog: AB 2.1.72, OpenAI.Codex 26.901.6511.0, native CLI 0.153.4.+Live checks covered explicit-agreement install, cached-auth identity, backend reply+`CODEX_BRIDGE_OK`, desktop-created thread reply `DESKTOP_BRIDGE_OK`, and LaTeX+install/read-back/uninstall/read-back. The temporary plugin was restored to absent.+See the PR for final lifecycle results. Windows x64 is first; other OS installers,+a dock UI, full automation/worktree action testing and third-party OAuth are not+claimed. Existing CAD recordings were made in Hydrogen, not this desktop app.++References: [Windows install](https://learn.chatgpt.com/docs/windows/windows-app),+[authentication](https://learn.chatgpt.com/docs/auth),+[app-server](https://learn.chatgpt.com/docs/app-server),+[AB SDK](https://wiki.adom.inc/adom/adom-bridge-sdk).
package.json+3−1@@ -29,6 +29,7 @@ "adom_codex.py", "dashboard/**", "skills/**",+ "skills/codex-desktop-bridge/SKILL.md", "docs/**", "README.md", "dockbar.json",@@ -43,7 +44,8 @@ "codex", "codex bootstrap", "astra kicad routing",- "codex hydrogen setup"+ "codex hydrogen setup",+ "codex desktop bridge" ], "tags": [ "codex",
skills/codex-desktop-bridge/SKILL.mdadded+34@@ -0,0 +1,34 @@+---+name: codex-desktop-bridge+description: Install, authenticate and communicate with Codex Desktop through the compiled Rust AB bridge. Manage native plugins, conversations and desktop-specific tools on an explicitly selected Windows host.+---++# Codex Desktop Bridge++Read adom/codex `docs/DESKTOP-BRIDGE.md` and installed AB SDK/runtime skills before+deployment. Source is `bridge/` in adom/codex, not AB core. Preserve development pins.++1. Run `adom-codex context`, discover targets, and pass `--target` and `--ai-thread`+ on every call. Initial test host is arav-rog; never silently use AdomLapper.+2. Call `codex_status` and `codex_describe`. Store ID 9PLM9XGG6VKS is now ChatGPT+ and includes Codex. `acceptAgreements:true` requires actual user acceptance.+3. Reuse only a user-authorized staged native cache via `codex_auth_import`.+ Never print tokens. Preserve existing credentials, remove staging promptly,+ and verify account plus the visible app. Native login is the fallback.+4. Poll every job with `codex_progress`; inspect nested `result.success`.+ After timeout inspect state before resubmitting a mutation.+5. `codex_desktop_tools` / `codex_desktop_tool_call` talk to the actual desktop.+ Discover schemas and native thread IDs. Select `desktopPipe` when ambiguous;+ rediscover after restart. Native tool instructions and approvals still apply.+6. `codex_connect` is a separate bridge-owned app-server on Windows, sharing+ auth/config but not live desktop sessions. Do not misrepresent its results as+ desktop changes or resume desktop-owned active threads through this backend.+7. Inspect `codex_requests` and obtain authorization before `codex_request_reply`.+ Never automatically approve execution, OAuth, interstitials, schedules or voice.+8. Discover plugins with bounded query/limit/cursor, read details, install, poll,+ then read back. Connected apps may still require the user's authentication.+9. `codex_state` observes via background AB capture. Verify native results and+ actual screenshots; a navigation acknowledgment is not visible proof.+10. Desktop removal requires confirm:true, disconnect and normal app quit,+ including tray. It preserves Codex home. AB bridge removal is separate and+ must not remove desktop, sign-in, plugins or projects.