From d95dbeb729f0e50177e5d12146eb7756fa6c6cb0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:16:57 +0000 Subject: [PATCH 01/23] chore(vendor): bump agent-browser, agent-desktop, tinybus and tinyinference Update the pinned submodule commits for the four vendored dependencies to their latest revisions. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- vendor/agent-browser | 2 +- vendor/agent-desktop | 2 +- vendor/tinybus | 2 +- vendor/tinyinference | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vendor/agent-browser b/vendor/agent-browser index 11303656..9a749e47 160000 --- a/vendor/agent-browser +++ b/vendor/agent-browser @@ -1 +1 @@ -Subproject commit 11303656ac6121f56bceb9769339085ef004d339 +Subproject commit 9a749e47aa872315db661cb00ff27e909bdce2e4 diff --git a/vendor/agent-desktop b/vendor/agent-desktop index 72f8af2f..b8d538c0 160000 --- a/vendor/agent-desktop +++ b/vendor/agent-desktop @@ -1 +1 @@ -Subproject commit 72f8af2f2bfb08a691fd374b6f2163061b83ea3c +Subproject commit b8d538c038872c72dbf7ae42e002d6fa03a3e599 diff --git a/vendor/tinybus b/vendor/tinybus index aa36051e..e5f1cd2d 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit aa36051e2d384b09a1fb7871758fcf22c2a7e709 +Subproject commit e5f1cd2d2a7b905ee82410a61219292ca8a85c1f diff --git a/vendor/tinyinference b/vendor/tinyinference index 02c01b72..c29d5118 160000 --- a/vendor/tinyinference +++ b/vendor/tinyinference @@ -1 +1 @@ -Subproject commit 02c01b72f70cd3747304b2a5a696b52a49fdd4fd +Subproject commit c29d5118eddfa12b83c266d7d5f07f6918dc3787 From ef60efde91e6157d8b066f630eebfb5b8bdacb74 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:21:34 +0000 Subject: [PATCH 02/23] chore(deps): refresh lockfile and vendor tinybus Regenerate Cargo.lock after dependency changes, which drops several transitive crates and pins zip to 8.6.0. The vendored tinybus submodule is bumped to match. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- Cargo.lock | 484 +++++++++++-------------------------------------- vendor/tinybus | 2 +- 2 files changed, 105 insertions(+), 381 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdfcf446..ca42a9ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,14 +82,13 @@ dependencies = [ "url", "urlencoding", "uuid", - "webpki-roots", "windows-sys 0.52.0", - "zip", + "zip 8.6.0", ] [[package]] name = "agent-desktop-core" -version = "0.10.1" +version = "0.9.4" dependencies = [ "base64 0.23.1", "libc", @@ -103,7 +102,7 @@ dependencies = [ [[package]] name = "agent-desktop-linux" -version = "0.10.1" +version = "0.9.4" dependencies = [ "agent-desktop-core", "thiserror 2.0.20", @@ -111,7 +110,7 @@ dependencies = [ [[package]] name = "agent-desktop-macos" -version = "0.10.1" +version = "0.9.4" dependencies = [ "accessibility-sys", "agent-desktop-core", @@ -130,15 +129,10 @@ dependencies = [ [[package]] name = "agent-desktop-windows" -version = "0.10.1" +version = "0.9.4" dependencies = [ "agent-desktop-core", - "serde_json", "thiserror 2.0.20", - "tracing", - "uiautomation", - "windows 0.62.2", - "windows-sys 0.61.2", ] [[package]] @@ -157,7 +151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags 2.13.1", + "bitflags", "cfg-if", "libc", ] @@ -187,6 +181,15 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arboard" version = "3.6.1" @@ -221,17 +224,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atomicwrites" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3" -dependencies = [ - "rustix 0.38.44", - "tempfile", - "windows-sys 0.52.0", -] - [[package]] name = "autocfg" version = "1.5.1" @@ -250,12 +242,6 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.13.1" @@ -335,8 +321,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -352,17 +336,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.2" @@ -374,19 +347,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20 0.9.1", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.45" @@ -408,16 +368,6 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout", - "zeroize", -] - -[[package]] -name = "clack-private-fs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fcca21d8096fca7f1c3a22c3109e38927c9b587007c9c7a13b327e5a955baba" -dependencies = [ - "windows-sys 0.61.2", ] [[package]] @@ -445,7 +395,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" dependencies = [ - "nix 0.27.1", + "nix", "winapi", ] @@ -477,7 +427,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation", "core-graphics-types", "foreign-types", @@ -490,7 +440,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation", "core-graphics-types", "foreign-types", @@ -503,7 +453,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.13.1", + "bitflags", "core-foundation", "libc", ] @@ -514,7 +464,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags 2.13.1", + "bitflags", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -579,6 +529,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + [[package]] name = "crypto-common" version = "0.1.7" @@ -626,6 +582,17 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "derive_arbitrary" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "digest" version = "0.10.7" @@ -675,7 +642,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.13.1", + "bitflags", "objc2 0.6.4", ] @@ -809,16 +776,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs4" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e72ed92b67c146290f88e9c89d60ca163ea417a446f61ffd7b72df3e7f1dfd5" -dependencies = [ - "rustix 1.1.4", - "windows-sys 0.61.2", -] - [[package]] name = "futures" version = "0.3.34" @@ -923,7 +880,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.4", + "rustix", "windows-link", ] @@ -940,18 +897,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - [[package]] name = "getrandom" version = "0.4.3" @@ -961,7 +906,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi 6.0.0", + "r-efi", "rand_core 0.10.1", "wasm-bindgen", ] @@ -988,15 +933,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - [[package]] name = "hmac" version = "0.12.1" @@ -1093,7 +1029,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.10", ] [[package]] @@ -1362,16 +1298,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.105" @@ -1398,12 +1324,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1511,7 +1431,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.13.1", + "bitflags", "jni-sys 0.3.1", "log", "ndk-sys", @@ -1540,23 +1460,11 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.13.1", + "bitflags", "cfg-if", "libc", ] -[[package]] -name = "nix" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" -dependencies = [ - "bitflags 2.13.1", - "cfg-if", - "cfg_aliases", - "libc", -] - [[package]] name = "ntapi" version = "0.4.3" @@ -1645,7 +1553,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -1661,7 +1569,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.13.1", + "bitflags", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -1674,7 +1582,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags 2.13.1", + "bitflags", "libc", "objc2 0.6.4", "objc2-core-audio", @@ -1689,7 +1597,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "bitflags 2.13.1", + "bitflags", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -1713,7 +1621,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags 2.13.1", + "bitflags", "objc2 0.6.4", ] @@ -1723,7 +1631,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1735,7 +1643,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.6.2", "dispatch2", "libc", @@ -1748,7 +1656,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.13.1", + "bitflags", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -1779,7 +1687,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -1791,7 +1699,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1804,7 +1712,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags 2.13.1", + "bitflags", "objc2 0.6.4", "objc2-core-foundation", ] @@ -1815,7 +1723,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1827,7 +1735,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.13.1", + "bitflags", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1899,24 +1807,13 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.13.1", + "bitflags", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "polyval" version = "0.6.2" @@ -2048,12 +1945,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - [[package]] name = "r-efi" version = "6.0.0" @@ -2067,27 +1958,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha 0.3.1", + "rand_chacha", "rand_core 0.6.4", ] -[[package]] -name = "rand" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - [[package]] name = "rand" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "chacha20 0.10.2", + "chacha20", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -2102,16 +1983,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - [[package]] name = "rand_core" version = "0.6.4" @@ -2121,15 +1992,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - [[package]] name = "rand_core" version = "0.10.1" @@ -2151,7 +2013,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags", ] [[package]] @@ -2232,7 +2094,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 1.0.10", ] [[package]] @@ -2299,29 +2161,16 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", -] - [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -2647,7 +2496,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -2739,7 +2588,7 @@ dependencies = [ "toml", "tracing", "ureq", - "zip", + "zip 2.6.1", ] [[package]] @@ -2914,17 +2763,16 @@ dependencies = [ [[package]] name = "tinyinference-core" -version = "0.5.0" +version = "0.3.0" dependencies = [ "anyhow", "httpdate", - "regex", "url", ] [[package]] name = "tinyinference-decisions" -version = "0.5.0" +version = "0.3.0" dependencies = [ "async-trait", "httpdate", @@ -2937,78 +2785,22 @@ dependencies = [ [[package]] name = "tinyinference-llm" -version = "0.5.0" +version = "0.3.0" dependencies = [ "async-trait", - "base64 0.23.1", "bytes", "futures", "reqwest", "serde", "serde_json", + "sha2 0.11.0", "thiserror 2.0.20", "tinyinference-core", - "tinyinference-providers", "tinytools-agent", "tokio", "tracing", ] -[[package]] -name = "tinyinference-providers" -version = "0.5.0" -dependencies = [ - "base64 0.23.1", - "rand 0.10.3", - "reqwest", - "serde", - "serde_json", - "sha2 0.11.0", - "tinystoragedrivers-secrets", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "tinystoragedrivers-core" -version = "1.0.0" -source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" -dependencies = [ - "async-trait", - "data-encoding", - "serde", - "serde_json", - "sha2 0.10.9", - "thiserror 2.0.20", - "tokio", - "ulid", - "zstd", -] - -[[package]] -name = "tinystoragedrivers-secrets" -version = "1.0.0" -source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" -dependencies = [ - "async-trait", - "atomicwrites", - "chacha20poly1305", - "clack-private-fs", - "fs4", - "hkdf", - "rustix 1.1.4", - "serde", - "serde_json", - "sha2 0.10.9", - "tinystoragedrivers-core", - "tokio", - "winapi-util", - "windows-permissions", - "winsafe", - "zeroize", -] - [[package]] name = "tinystr" version = "0.8.4" @@ -3021,8 +2813,8 @@ dependencies = [ [[package]] name = "tinytools" -version = "0.5.0" -source = "git+https://github.com/tinyhumansai/tinytools?rev=bd60b9b52f1998b4483b9cf107a72d3338e672f8#bd60b9b52f1998b4483b9cf107a72d3338e672f8" +version = "0.3.0" +source = "git+https://github.com/tinyhumansai/tinytools?rev=a5d04a7f3af0abc1dd748f87f94871b36ae9fe71#a5d04a7f3af0abc1dd748f87f94871b36ae9fe71" dependencies = [ "anyhow", "async-trait", @@ -3032,8 +2824,8 @@ dependencies = [ [[package]] name = "tinytools-agent" -version = "0.5.0" -source = "git+https://github.com/tinyhumansai/tinytools?rev=bd60b9b52f1998b4483b9cf107a72d3338e672f8#bd60b9b52f1998b4483b9cf107a72d3338e672f8" +version = "0.3.0" +source = "git+https://github.com/tinyhumansai/tinytools?rev=a5d04a7f3af0abc1dd748f87f94871b36ae9fe71#a5d04a7f3af0abc1dd748f87f94871b36ae9fe71" dependencies = [ "regex", "serde", @@ -3097,6 +2889,7 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", + "webpki-roots 0.26.11", ] [[package]] @@ -3184,7 +2977,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.13.1", + "bitflags", "bytes", "futures-util", "http", @@ -3277,39 +3070,6 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" -[[package]] -name = "uiautomation" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2eacf9390a8491bc949b04230f40c12520c6423a2962a6087eea1ef14373af" -dependencies = [ - "chrono", - "uiautomation_derive", - "windows 0.62.2", - "windows-core 0.62.2", -] - -[[package]] -name = "uiautomation_derive" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c893ec96e05c3c81ba46f838f3190549ec185ef377d582ab3d44202b2dfacbd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - -[[package]] -name = "ulid" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" -dependencies = [ - "rand 0.9.5", - "web-time", -] - [[package]] name = "unicase" version = "2.9.0" @@ -3352,7 +3112,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots", + "webpki-roots 1.0.10", ] [[package]] @@ -3445,15 +3205,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.128" @@ -3542,6 +3293,15 @@ dependencies = [ "wasm-bindgen", ] +[[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.10", +] + [[package]] name = "webpki-roots" version = "1.0.10" @@ -3754,16 +3514,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-permissions" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2ccdc3c6bf4d4a094e031b63fadd08d8e42abd259940eb8aa5fdc09d4bf9be" -dependencies = [ - "bitflags 1.3.2", - "winapi", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -3976,18 +3726,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "writeable" version = "0.6.4" @@ -4001,7 +3739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix 1.1.4", + "rustix", "x11rb-protocol", ] @@ -4018,7 +3756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.4", + "rustix", ] [[package]] @@ -4143,15 +3881,29 @@ dependencies = [ [[package]] name = "zip" -version = "9.0.5" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zip" +version = "8.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" dependencies = [ "crc32fast", "flate2", "indexmap", "memchr", - "nix 0.31.3", "typed-path", "zopfli", ] @@ -4180,34 +3932,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.1.2+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zune-core" version = "0.5.3" diff --git a/vendor/tinybus b/vendor/tinybus index e5f1cd2d..d621ed0a 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit e5f1cd2d2a7b905ee82410a61219292ca8a85c1f +Subproject commit d621ed0aaa1c7b1bc5d0bf0d0abb10927a26bdaa From f7a1e691ef8ca3b0d27d5230918e860cdefabe00 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:25:36 +0000 Subject: [PATCH 03/23] test(browser): cover worker fetch egress denial in live tests Add a live Chromium test asserting that a fetch issued from a worker script never reaches a denied destination, and extend the tinybus egress fixture to record resolved addresses and tolerate extra connections so the new case can be observed. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 70 ++++++++++++++++++ .../browser_egress_live_tests.rs | 73 +++++++++++++++---- 2 files changed, 128 insertions(+), 15 deletions(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 085f0214..4d71aa6a 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -123,6 +123,76 @@ async fn live_chromium_subresource_never_reaches_a_denied_destination() { ); } +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let html = format!("

worker fixture

"); + let worker = format!( + "fetch('http://127.0.0.1:{}/private').catch(() => {{}});", + denied_address.port() + ); + let fixture_server = tokio::spawn(async move { + for _ in 0..2 { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), fixture.accept()) + .await + .expect("Chromium requests page and worker script") + .unwrap(); + let mut request = [0; 4096]; + let size = socket.read(&mut request).await.unwrap(); + let request = String::from_utf8_lossy(&request[..size]); + let (content_type, body) = if request.contains("GET /worker.js") { + ("application/javascript", worker.as_str()) + } else { + ("text/html", html.as_str()) + }; + socket + .write_all( + format!( + "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .as_bytes(), + ) + .await + .unwrap(); + } + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: Arc::new(AtomicUsize::new(0)), + denied_targets: denied_targets.clone(), + })) + .open("live-egress-worker"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + wait_for_denied_target(&denied_targets, denied_address.port()).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + fixture_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the denied worker fetch destination must never receive a connection" + ); +} + #[tokio::test] #[cfg(live_browser_tests)] async fn live_chromium_redirect_never_reaches_a_denied_destination() { diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests.rs index 66c41916..b61c63cf 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests.rs @@ -3,7 +3,7 @@ use std::sync::{ Arc, Mutex, - atomic::{AtomicBool, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }; use std::time::Duration; @@ -22,6 +22,7 @@ use tinycomputer_bus::{ struct EgressHost { allowed: Arc, allowed_address: String, + resolved_address: Arc>, observations: Arc>>, } @@ -30,6 +31,7 @@ impl EgressHost { async fn authorize(&self, request: BrowserEgressTarget) -> tinybus::Result { let allow = self.allowed.load(Ordering::SeqCst) && request.hostname == "fixture.test" + && *self.resolved_address.lock().unwrap() == self.allowed_address && request.port == self .allowed_address @@ -41,6 +43,12 @@ impl EgressHost { "authorize:{}:{}:{}:{}", request.policy_id, request.expected_generation, request.hostname, request.port )); + self.observations.lock().unwrap().push(format!( + "resolve:{}:{}:{}", + request.hostname, + request.port, + self.resolved_address.lock().unwrap() + )); Ok(BrowserEgressGrant { generation: request.expected_generation, authorization: if allow { @@ -85,11 +93,15 @@ impl EgressHost { } } -async fn serve_fixture(listener: tokio::net::TcpListener, expected: &'static [u8]) { - let (mut stream, _) = listener - .accept() +async fn serve_fixture( + listener: tokio::net::TcpListener, + expected: &'static [u8], + extra_connections: Arc, +) { + let (mut stream, _) = tokio::time::timeout(Duration::from_secs(10), listener.accept()) .await - .expect("authorized Chromium socket arrives"); + .expect("authorized Chromium socket arrives before the deadline") + .expect("fixture listener accepts the authorized socket"); let mut request = [0_u8; 4096]; let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut request).await; assert!(String::from_utf8_lossy(&request).contains("GET /")); @@ -101,6 +113,12 @@ async fn serve_fixture(listener: tokio::net::TcpListener, expected: &'static [u8 ] .concat(); let _ = tokio::io::AsyncWriteExt::write_all(&mut stream, &response).await; + drop(stream); + + while let Ok((stream, _)) = listener.accept().await { + extra_connections.fetch_add(1, Ordering::SeqCst); + drop(stream); + } } fn successful_data(reply: &DesktopResponse) -> &serde_json::Value { @@ -115,13 +133,20 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch let port = allowed_listener.local_addr()?.port(); let denied_listener = tokio::net::TcpListener::bind(("127.0.0.2", port)).await?; let allowed_address = allowed_listener.local_addr()?.to_string(); - let allowed_server = tokio::spawn(serve_fixture(allowed_listener, b"allowed fixture")); + let rebound_address = denied_listener.local_addr()?.to_string(); + let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); + let allowed_server = tokio::spawn(serve_fixture( + allowed_listener, + b"allowed fixture", + extra_allowed_connections.clone(), + )); let bus = MemoryBus::new(); let broker = Broker::new(); let broker_task = broker.spawn(bus.clone()); let host_connection = Connection::connect(bus.connect().await?).await?; let allowed = Arc::new(AtomicBool::new(true)); + let resolved_address = Arc::new(Mutex::new(allowed_address.clone())); let observations = Arc::new(Mutex::new(Vec::new())); host_connection .serve_at( @@ -129,6 +154,7 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch EgressHost { allowed: allowed.clone(), allowed_address: allowed_address.clone(), + resolved_address: resolved_address.clone(), observations: observations.clone(), }, ) @@ -170,21 +196,19 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch "authorized navigation failed: {:?}", navigated.error ); - tokio::time::timeout(Duration::from_secs(10), allowed_server) - .await - .expect("authorized fixture server completes within ten seconds") - .expect("fixture server task completes"); - let authorizations_before_policy_change = observations .lock() .unwrap() .iter() .filter(|call| call.starts_with("authorize:fixture-policy:41:fixture.test:")) .count(); - // Simulate a policy refresh: the host changes from granting the fixture - // socket to denying requests for the same hostname. + // Rebind the same test hostname to a private fixture socket and revoke the + // old grant. The loopback fixtures model the host resolver's two answers; + // Chromium itself never resolves or dials the hostname directly. + *resolved_address.lock().unwrap() = rebound_address.clone(); allowed.store(false, Ordering::SeqCst); - let denied_url = format!("http://fixture.test:{port}/after-policy-change"); + let previously_allowed_connections = extra_allowed_connections.load(Ordering::SeqCst); + let denied_url = format!("http://fixture.test:{port}/after-dns-rebind"); let denied: DesktopResponse = proxy .call( browser_names::methods::NAVIGATE, @@ -201,6 +225,17 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch .is_err(), "denied policy-change socket received a connection" ); + assert!( + tokio::time::timeout(Duration::from_millis(500), async { + while extra_allowed_connections.load(Ordering::SeqCst) == previously_allowed_connections + { + tokio::task::yield_now().await; + } + }) + .await + .is_err(), + "denied rebound request reached the previously allowed socket" + ); let calls = observations.lock().unwrap(); assert!( @@ -222,8 +257,16 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch !calls .iter() .any(|call| call == &format!("consume:{denied_address}")), - "denied socket was consumed: {calls:?}" + "denied rebound socket was consumed: {calls:?}" + ); + assert!( + calls + .iter() + .any(|call| call == &format!("resolve:fixture.test:{port}:{rebound_address}")), + "the controlled resolver did not provide the private rebound answer: {calls:?}" ); + allowed_server.abort(); + let _ = allowed_server.await; broker_task.abort(); Ok(()) } From ce5180ad3ae1feb31a856c2e028c93a041d3a156 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:26:08 +0000 Subject: [PATCH 04/23] test: run live browser tests in feature test script The feature test script now runs the browser crate's live tests with the live browser flag enabled, so that path is exercised alongside the existing workspace and feature test runs. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- scripts/test-features.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-features.sh b/scripts/test-features.sh index aac4a46d..ab0aee20 100755 --- a/scripts/test-features.sh +++ b/scripts/test-features.sh @@ -13,6 +13,7 @@ case "${RUNNER_OS:-$(uname -s)}" in esac cargo test --locked --workspace cargo test --locked --workspace --all-features +TINYCOMPUTER_LIVE_BROWSER_TESTS=1 cargo test --locked -p tinycomputer-browser --features agent-browser live_ cargo test --locked -p tinycomputer --features static-link cargo run --locked -p tinycomputer-examples --bin basic forbidden="$(cargo tree -p tinycomputer-bus -e normal,build --prefix none | grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2|agent-desktop' || true)" From 96207102c13d5a3c1646923d1d0c3b29862433f2 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:26:49 +0000 Subject: [PATCH 05/23] docs(test): fix live browser test command filter Co-authored-by: Medulla --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index a8d5318e..afe5ebb2 100644 --- a/.env.example +++ b/.env.example @@ -120,4 +120,4 @@ TINYCOMPUTER_LAB_SELF_EMAIL= # TINYCOMPUTER_LIVE_BROWSER_TESTS=1 # Compile and run TinyComputer's module-path egress test against installed Chromium # using only local fixture listeners: -# TINYCOMPUTER_LIVE_BROWSER_EGRESS_TESTS=1 cargo test --manifest-path crates/tinycomputer/Cargo.toml -p tinycomputer --lib --features static-link live_chromium_module_dials_only_the_host_returned_socket_and_rejects_changed_policy -- --exact --nocapture +# TINYCOMPUTER_LIVE_BROWSER_EGRESS_TESTS=1 cargo test --manifest-path crates/tinycomputer/Cargo.toml -p tinycomputer --lib --features static-link live_chromium_module_dials_only_the_host_returned_socket_and_rejects_changed_policy -- --nocapture From b75b60b4448f3caed196714107727ed25cabbecc Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:29:15 +0000 Subject: [PATCH 06/23] test(browser): cover DNS rebinding and allowed cross-origin egress Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 4d71aa6a..6da2f76c 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -6,6 +6,7 @@ use crate::engine::Launcher; use crate::{BrowserDialTarget, DialGrant, EgressAuthorizer}; use serde_json::json; use std::{ + collections::HashMap, io, net::SocketAddr, sync::{ @@ -123,6 +124,70 @@ async fn live_chromium_subresource_never_reaches_a_denied_destination() { ); } +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_uses_a_host_permitted_cross_origin_subresource_socket() { + let cdn_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let cdn_address = cdn_fixture.local_addr().unwrap(); + let page_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let page_address = page_fixture.local_addr().unwrap(); + let html = format!( + "

page fixture

", + cdn_address.port() + ); + let page_server = tokio::spawn(async move { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), page_fixture.accept()) + .await + .expect("Chromium requests the permitted page") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}", + html.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + }); + let cdn_server = tokio::spawn(async move { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), cdn_fixture.accept()) + .await + .expect("Chromium requests the permitted cross-origin asset") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + }); + let addresses = HashMap::from([ + ("127.0.0.1".to_owned(), page_address), + ("cdn.fixture.test".to_owned(), cdn_address), + ]); + let mut browser = EnforcedAgentBrowser::new(Arc::new(AllowFixtureHosts { + addresses, + authorizations: Arc::new(AtomicUsize::new(0)), + })) + .open("live-egress-permitted-cross-origin"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", page_address.port()) + })) + .await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + page_server.await.unwrap(); + cdn_server.await.unwrap(); +} + #[tokio::test] #[cfg(live_browser_tests)] async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { @@ -193,6 +258,96 @@ async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { ); } +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_dns_rebinding_never_reaches_the_rebound_private_socket() { + let private = std::net::TcpListener::bind("127.0.0.2:0").unwrap(); + private.set_nonblocking(true).unwrap(); + let private_address = private.local_addr().unwrap(); + let allowed_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = allowed_listener.local_addr().unwrap().port(); + let allowed_address = allowed_listener.local_addr().unwrap(); + let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); + let observed_addresses = Arc::new(Mutex::new(Vec::new())); + let extra_connections = extra_allowed_connections.clone(); + let body = format!("

fixture

"); + let allowed_server = tokio::spawn(async move { + let (mut socket, _) = + tokio::time::timeout(Duration::from_secs(10), allowed_listener.accept()) + .await + .expect("authorized navigation reaches the allowed fixture") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + drop(socket); + + while let Ok((socket, _)) = allowed_listener.accept().await { + extra_connections.fetch_add(1, Ordering::SeqCst); + drop(socket); + } + }); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RebindingAuthorizer { + allowed_address, + private_address, + authorizations: AtomicUsize::new(0), + denied_targets: denied_targets.clone(), + observed_addresses: observed_addresses.clone(), + })) + .open("live-egress-dns-rebinding"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://fixture.test:{port}/") + })) + .await; + wait_for_denied_target(&denied_targets, port).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + let observed_addresses = observed_addresses.lock().unwrap(); + assert_eq!( + observed_addresses.first(), + Some(&allowed_address), + "the controlled resolver first returns the permitted address" + ); + assert!( + observed_addresses.len() >= 2 + && observed_addresses[1..] + .iter() + .all(|address| *address == private_address), + "the controlled resolver must rebind the name to the private address: {observed_addresses:?}" + ); + assert!( + tokio::time::timeout(Duration::from_millis(500), async { + while extra_allowed_connections.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .is_err(), + "the denied rebound request reached the previously allowed socket" + ); + assert_eq!( + private.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the rebound private destination must never receive a connection" + ); + allowed_server.abort(); + let _ = allowed_server.await; +} + #[tokio::test] #[cfg(live_browser_tests)] async fn live_chromium_redirect_never_reaches_a_denied_destination() { @@ -313,6 +468,132 @@ struct RejectAllButFixture { denied_targets: Arc>>, } +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct RebindingAuthorizer { + allowed_address: SocketAddr, + private_address: SocketAddr, + authorizations: AtomicUsize, + denied_targets: Arc>>, + observed_addresses: Arc>>, +} + +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct AllowFixtureHosts { + addresses: HashMap, + authorizations: Arc, +} + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for AllowFixtureHosts { + fn authorize( + &self, + target: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + self.authorizations.fetch_add(1, Ordering::SeqCst); + let address = self + .addresses + .get(&target.hostname) + .copied() + .filter(|address| address.port() == target.port); + Box::pin(async move { + match address { + Some(address) => Ok(DialGrant { + lease: "live-cross-origin-fixture-lease".into(), + addresses: vec![address], + }), + None => Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")), + } + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for RebindingAuthorizer { + fn authorize( + &self, + target: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + let attempt = self.authorizations.fetch_add(1, Ordering::SeqCst); + let resolved_address = if attempt == 0 { + self.allowed_address + } else { + self.private_address + }; + self.observed_addresses + .lock() + .unwrap() + .push(resolved_address); + let allowed = attempt == 0 + && target.hostname == "fixture.test" + && target.port == self.allowed_address.port(); + if !allowed { + self.denied_targets + .lock() + .unwrap() + .push((target.hostname, target.port)); + } + Box::pin(async move { + if allowed { + Ok(DialGrant { + lease: "live-dns-rebinding-lease".into(), + addresses: vec![resolved_address], + }) + } else { + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "rebound private address denied", + )) + } + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} + #[cfg(live_browser_tests)] async fn wait_for_denied_target(denied_targets: &Arc>>, port: u16) { tokio::time::timeout(Duration::from_secs(3), async { From 9a54fbcec4969514958727f531153189d4b8e350 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:30:10 +0000 Subject: [PATCH 07/23] test(browser): stabilize worker egress fixtures Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 20 ++++++++++++------- scripts/test-features.sh | 2 +- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 6da2f76c..8eb71cc7 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -203,29 +203,35 @@ async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { denied_address.port() ); let fixture_server = tokio::spawn(async move { - for _ in 0..2 { + loop { let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), fixture.accept()) .await - .expect("Chromium requests page and worker script") + .expect("Chromium requests the page and worker script") .unwrap(); let mut request = [0; 4096]; let size = socket.read(&mut request).await.unwrap(); let request = String::from_utf8_lossy(&request[..size]); - let (content_type, body) = if request.contains("GET /worker.js") { - ("application/javascript", worker.as_str()) + let (status, content_type, body, worker_script) = if request.contains("GET /worker.js") + { + ("200 OK", "application/javascript", worker.as_str(), true) + } else if request.contains("GET / ") { + ("200 OK", "text/html", html.as_str(), false) } else { - ("text/html", html.as_str()) + ("404 Not Found", "text/plain", "not found", false) }; socket .write_all( format!( - "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len() ) .as_bytes(), ) .await .unwrap(); + if worker_script { + break; + } } }); let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { @@ -596,7 +602,7 @@ impl EgressAuthorizer for RebindingAuthorizer { #[cfg(live_browser_tests)] async fn wait_for_denied_target(denied_targets: &Arc>>, port: u16) { - tokio::time::timeout(Duration::from_secs(3), async { + tokio::time::timeout(Duration::from_secs(10), async { loop { let was_denied = denied_targets .lock() diff --git a/scripts/test-features.sh b/scripts/test-features.sh index ab0aee20..a7b75f57 100755 --- a/scripts/test-features.sh +++ b/scripts/test-features.sh @@ -13,7 +13,7 @@ case "${RUNNER_OS:-$(uname -s)}" in esac cargo test --locked --workspace cargo test --locked --workspace --all-features -TINYCOMPUTER_LIVE_BROWSER_TESTS=1 cargo test --locked -p tinycomputer-browser --features agent-browser live_ +RUST_TEST_THREADS=1 TINYCOMPUTER_LIVE_BROWSER_TESTS=1 cargo test --locked -p tinycomputer-browser --features agent-browser live_ cargo test --locked -p tinycomputer --features static-link cargo run --locked -p tinycomputer-examples --bin basic forbidden="$(cargo tree -p tinycomputer-bus -e normal,build --prefix none | grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2|agent-desktop' || true)" From d0ecf6d2cfbca4d76b6eb5def9159bb75aaf45ad Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 01:39:36 +0000 Subject: [PATCH 08/23] chore(deps): bump vendored tinybus to 0.1.9 Update the vendored tinybus submodule from 0.1.6 to 0.1.9 and refresh Cargo.lock to match. The new revision drops the zip 2.6.1 dependency in favour of rawzip and adds icu_casemap and unicode-normalization. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- Cargo.lock | 101 ++++++++++++++++++++++++++----------------------- vendor/tinybus | 2 +- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca42a9ee..9349ed2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ "urlencoding", "uuid", "windows-sys 0.52.0", - "zip 8.6.0", + "zip", ] [[package]] @@ -181,15 +181,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "arbitrary" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arboard" version = "3.6.1" @@ -529,12 +520,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - [[package]] name = "crypto-common" version = "0.1.7" @@ -582,17 +567,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -[[package]] -name = "derive_arbitrary" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", -] - [[package]] name = "digest" version = "0.10.7" @@ -1079,6 +1053,28 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_casemap" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2edfd120d8445de853cf53a1abec916f638f933a70a6f4bf8f7e2d7b7f884e9" +dependencies = [ + "icu_casemap_data", + "icu_collections", + "icu_locale_core", + "icu_properties", + "icu_provider", + "potential_utf", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_casemap_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059fcda0fc8d33effc6633a8cf0d8a3c17caf7d7c91918fba9918744f501e4e8" + [[package]] name = "icu_collections" version = "2.3.0" @@ -1087,6 +1083,7 @@ checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", + "serde", "utf8_iter", "yoke", "zerofrom", @@ -1101,6 +1098,7 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", @@ -1155,6 +1153,8 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -1832,6 +1832,8 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -2007,6 +2009,12 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "rawzip" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9999039f74cd73c03158cddfe4362991703c74efe5ce5c8073026f80a5e48c" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -2572,11 +2580,13 @@ dependencies = [ [[package]] name = "tinybus" -version = "0.1.6" +version = "0.1.9" dependencies = [ "async-trait", "flate2", + "icu_casemap", "libc", + "rawzip", "serde", "serde_json", "sha2 0.10.9", @@ -2587,13 +2597,13 @@ dependencies = [ "tokio", "toml", "tracing", + "unicode-normalization", "ureq", - "zip 2.6.1", ] [[package]] name = "tinybus-macros" -version = "0.1.6" +version = "0.1.9" dependencies = [ "proc-macro2", "quote", @@ -2602,7 +2612,7 @@ dependencies = [ [[package]] name = "tinybus-module" -version = "0.1.6" +version = "0.1.9" dependencies = [ "async-trait", "serde", @@ -2808,6 +2818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -3082,6 +3093,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + [[package]] name = "universal-hash" version = "0.5.1" @@ -3333,7 +3353,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -3855,6 +3875,7 @@ dependencies = [ "displaydoc", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -3863,6 +3884,7 @@ version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -3879,21 +3901,6 @@ dependencies = [ "syn 3.0.9", ] -[[package]] -name = "zip" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "flate2", - "indexmap", - "memchr", - "zopfli", -] - [[package]] name = "zip" version = "8.6.0" diff --git a/vendor/tinybus b/vendor/tinybus index d621ed0a..d22fb0bc 160000 --- a/vendor/tinybus +++ b/vendor/tinybus @@ -1 +1 @@ -Subproject commit d621ed0aaa1c7b1bc5d0bf0d0abb10927a26bdaa +Subproject commit d22fb0bcc3d4d35b5e5b56c37fa0292ce3d96303 From cf5d861b19bb668a86ef8e511de3a441ca9f0bf3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 02:01:13 +0000 Subject: [PATCH 09/23] chore(deps): pin agent-browser zip fix Co-authored-by: Medulla --- vendor/agent-browser | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/agent-browser b/vendor/agent-browser index 9a749e47..76e4892d 160000 --- a/vendor/agent-browser +++ b/vendor/agent-browser @@ -1 +1 @@ -Subproject commit 9a749e47aa872315db661cb00ff27e909bdce2e4 +Subproject commit 76e4892d1946e80b28b5b97fc29c44158e847c77 From c5cd60bd543c3efd39bcdf0647a268296b929ef3 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 02:17:06 +0000 Subject: [PATCH 10/23] chore(deps): update lockfile dependencies Refresh the lockfile to pick up zip 9.0.5, which adds a nix 0.31.3 dependency, and drop the now-unused webpki-roots 0.26.11 entry in favour of the 1.0.10 release. Also bump windows-sys for the getrandom crate. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- Cargo.lock | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9349ed2c..52916c39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,7 @@ dependencies = [ "url", "urlencoding", "uuid", + "webpki-roots", "windows-sys 0.52.0", "zip", ] @@ -386,7 +387,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" dependencies = [ - "nix", + "nix 0.27.1", "winapi", ] @@ -1003,7 +1004,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.10", + "webpki-roots", ] [[package]] @@ -1465,6 +1466,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -2102,7 +2115,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.10", + "webpki-roots", ] [[package]] @@ -2900,7 +2913,6 @@ dependencies = [ "tokio", "tokio-rustls", "tungstenite", - "webpki-roots 0.26.11", ] [[package]] @@ -3132,7 +3144,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots 1.0.10", + "webpki-roots", ] [[package]] @@ -3313,15 +3325,6 @@ dependencies = [ "wasm-bindgen", ] -[[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.10", -] - [[package]] name = "webpki-roots" version = "1.0.10" @@ -3353,7 +3356,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3903,14 +3906,15 @@ dependencies = [ [[package]] name = "zip" -version = "8.6.0" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "crc32fast", "flate2", "indexmap", "memchr", + "nix 0.31.3", "typed-path", "zopfli", ] From 26b46766baa18a9d6c8dc64fdc446c7b9b1ad80e Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 02:35:58 +0000 Subject: [PATCH 11/23] chore(deps): pin agent-desktop trace sink Co-authored-by: Medulla --- vendor/agent-desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/agent-desktop b/vendor/agent-desktop index b8d538c0..fee077c4 160000 --- a/vendor/agent-desktop +++ b/vendor/agent-desktop @@ -1 +1 @@ -Subproject commit b8d538c038872c72dbf7ae42e002d6fa03a3e599 +Subproject commit fee077c47cbad3317b7bf7ec7a99a2b6ae233376 From d7dcfd5993c62d03e03597b26deb80e7f706d130 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 02:44:34 +0000 Subject: [PATCH 12/23] chore(vendor): bump tinyinference submodule Update the vendored tinyinference submodule to the latest upstream commit. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- vendor/tinyinference | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/tinyinference b/vendor/tinyinference index c29d5118..03adc8b9 160000 --- a/vendor/tinyinference +++ b/vendor/tinyinference @@ -1 +1 @@ -Subproject commit c29d5118eddfa12b83c266d7d5f07f6918dc3787 +Subproject commit 03adc8b92ec8ada4d69533f30731f56e7775b8c8 From 9bd55826533bb3d0fa530c77383f1c4fcdb0ddcf Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 02:44:54 +0000 Subject: [PATCH 13/23] chore(deps): bump workspace crates to 0.10.1 and refresh lockfile Update the agent-desktop crates to 0.10.1 and regenerate Cargo.lock, which pulls in new transitive dependencies for the Windows backend and adds several crates to the dependency graph. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- Cargo.lock | 405 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 359 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52916c39..8bcd7dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "agent-desktop-core" -version = "0.9.4" +version = "0.10.1" dependencies = [ "base64 0.23.1", "libc", @@ -103,7 +103,7 @@ dependencies = [ [[package]] name = "agent-desktop-linux" -version = "0.9.4" +version = "0.10.1" dependencies = [ "agent-desktop-core", "thiserror 2.0.20", @@ -111,7 +111,7 @@ dependencies = [ [[package]] name = "agent-desktop-macos" -version = "0.9.4" +version = "0.10.1" dependencies = [ "accessibility-sys", "agent-desktop-core", @@ -130,10 +130,15 @@ dependencies = [ [[package]] name = "agent-desktop-windows" -version = "0.9.4" +version = "0.10.1" dependencies = [ "agent-desktop-core", + "serde_json", "thiserror 2.0.20", + "tracing", + "uiautomation", + "windows 0.62.2", + "windows-sys 0.61.2", ] [[package]] @@ -152,7 +157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d" dependencies = [ "alsa-sys", - "bitflags", + "bitflags 2.13.1", "cfg-if", "libc", ] @@ -216,6 +221,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atomicwrites" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef1bb8d1b645fe38d51dfc331d720fb5fc2c94b440c76cc79c80ff265ca33e3" +dependencies = [ + "rustix 0.38.44", + "tempfile", + "windows-sys 0.52.0", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -234,6 +250,12 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" @@ -313,6 +335,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -328,6 +352,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.2" @@ -339,6 +374,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.45" @@ -360,6 +408,16 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout", + "zeroize", +] + +[[package]] +name = "clack-private-fs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fcca21d8096fca7f1c3a22c3109e38927c9b587007c9c7a13b327e5a955baba" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -419,7 +477,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation", "core-graphics-types", "foreign-types", @@ -432,7 +490,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation", "core-graphics-types", "foreign-types", @@ -445,7 +503,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags", + "bitflags 2.13.1", "core-foundation", "libc", ] @@ -456,7 +514,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58" dependencies = [ - "bitflags", + "bitflags 2.13.1", "libc", "objc2-audio-toolbox", "objc2-core-audio", @@ -617,7 +675,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2 0.6.4", ] @@ -751,6 +809,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e72ed92b67c146290f88e9c89d60ca163ea417a446f61ffd7b72df3e7f1dfd5" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "futures" version = "0.3.34" @@ -855,7 +923,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix", + "rustix 1.1.4", "windows-link", ] @@ -872,6 +940,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -881,7 +961,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -908,6 +988,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1299,6 +1388,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.105" @@ -1325,6 +1424,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1432,7 +1537,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags", + "bitflags 2.13.1", "jni-sys 0.3.1", "log", "ndk-sys", @@ -1461,7 +1566,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cfg-if", "libc", ] @@ -1472,7 +1577,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "cfg-if", "cfg_aliases", "libc", @@ -1566,7 +1671,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -1582,7 +1687,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", @@ -1595,7 +1700,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08" dependencies = [ - "bitflags", + "bitflags 2.13.1", "libc", "objc2 0.6.4", "objc2-core-audio", @@ -1610,7 +1715,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-foundation 0.3.2", ] @@ -1634,7 +1739,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2 0.6.4", ] @@ -1644,7 +1749,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1656,7 +1761,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.6.2", "dispatch2", "libc", @@ -1669,7 +1774,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags", + "bitflags 2.13.1", "dispatch2", "objc2 0.6.4", "objc2-core-foundation", @@ -1700,7 +1805,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -1712,7 +1817,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1725,7 +1830,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "bitflags", + "bitflags 2.13.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -1736,7 +1841,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1748,7 +1853,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags", + "bitflags 2.13.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1820,13 +1925,24 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags", + "bitflags 2.13.1", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "polyval" version = "0.6.2" @@ -1960,6 +2076,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -1973,17 +2095,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", + "rand_chacha 0.3.1", "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ - "chacha20", + "chacha20 0.10.2", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -1998,6 +2130,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -2007,6 +2149,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -2034,7 +2185,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", ] [[package]] @@ -2182,16 +2333,29 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags", + "bitflags 2.13.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -2517,7 +2681,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -2786,16 +2950,17 @@ dependencies = [ [[package]] name = "tinyinference-core" -version = "0.3.0" +version = "0.5.0" dependencies = [ "anyhow", "httpdate", + "regex", "url", ] [[package]] name = "tinyinference-decisions" -version = "0.3.0" +version = "0.5.0" dependencies = [ "async-trait", "httpdate", @@ -2808,22 +2973,78 @@ dependencies = [ [[package]] name = "tinyinference-llm" -version = "0.3.0" +version = "0.5.0" dependencies = [ "async-trait", + "base64 0.23.1", "bytes", "futures", "reqwest", "serde", "serde_json", - "sha2 0.11.0", "thiserror 2.0.20", "tinyinference-core", + "tinyinference-providers", "tinytools-agent", "tokio", "tracing", ] +[[package]] +name = "tinyinference-providers" +version = "0.5.0" +dependencies = [ + "base64 0.23.1", + "rand 0.10.3", + "reqwest", + "serde", + "serde_json", + "sha2 0.11.0", + "tinystoragedrivers-secrets", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "tinystoragedrivers-core" +version = "1.0.0" +source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" +dependencies = [ + "async-trait", + "data-encoding", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 2.0.20", + "tokio", + "ulid", + "zstd", +] + +[[package]] +name = "tinystoragedrivers-secrets" +version = "1.0.0" +source = "git+https://github.com/tinyhumansai/tinystoragedrivers?tag=v1.0.0#141fd9ed0ad5d14a8fb5a1b3e837a719e81ac08d" +dependencies = [ + "async-trait", + "atomicwrites", + "chacha20poly1305", + "clack-private-fs", + "fs4", + "hkdf", + "rustix 1.1.4", + "serde", + "serde_json", + "sha2 0.10.9", + "tinystoragedrivers-core", + "tokio", + "winapi-util", + "windows-permissions", + "winsafe", + "zeroize", +] + [[package]] name = "tinystr" version = "0.8.4" @@ -2837,8 +3058,8 @@ dependencies = [ [[package]] name = "tinytools" -version = "0.3.0" -source = "git+https://github.com/tinyhumansai/tinytools?rev=a5d04a7f3af0abc1dd748f87f94871b36ae9fe71#a5d04a7f3af0abc1dd748f87f94871b36ae9fe71" +version = "0.6.0" +source = "git+https://github.com/tinyhumansai/tinytools?rev=3a01641f8ebd8a55656cd212f9a90638d958a447#3a01641f8ebd8a55656cd212f9a90638d958a447" dependencies = [ "anyhow", "async-trait", @@ -2848,8 +3069,8 @@ dependencies = [ [[package]] name = "tinytools-agent" -version = "0.3.0" -source = "git+https://github.com/tinyhumansai/tinytools?rev=a5d04a7f3af0abc1dd748f87f94871b36ae9fe71#a5d04a7f3af0abc1dd748f87f94871b36ae9fe71" +version = "0.6.0" +source = "git+https://github.com/tinyhumansai/tinytools?rev=3a01641f8ebd8a55656cd212f9a90638d958a447#3a01641f8ebd8a55656cd212f9a90638d958a447" dependencies = [ "regex", "serde", @@ -3000,7 +3221,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags", + "bitflags 2.13.1", "bytes", "futures-util", "http", @@ -3093,6 +3314,39 @@ version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "uiautomation" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2eacf9390a8491bc949b04230f40c12520c6423a2962a6087eea1ef14373af" +dependencies = [ + "chrono", + "uiautomation_derive", + "windows 0.62.2", + "windows-core 0.62.2", +] + +[[package]] +name = "uiautomation_derive" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c893ec96e05c3c81ba46f838f3190549ec185ef377d582ab3d44202b2dfacbd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ulid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe" +dependencies = [ + "rand 0.9.5", + "web-time", +] + [[package]] name = "unicase" version = "2.9.0" @@ -3237,6 +3491,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.128" @@ -3537,6 +3800,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-permissions" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2ccdc3c6bf4d4a094e031b63fadd08d8e42abd259940eb8aa5fdc09d4bf9be" +dependencies = [ + "bitflags 1.3.2", + "winapi", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -3749,6 +4022,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "winsafe" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.4" @@ -3762,7 +4047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" dependencies = [ "gethostname", - "rustix", + "rustix 1.1.4", "x11rb-protocol", ] @@ -3779,7 +4064,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -3943,6 +4228,34 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.2+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zune-core" version = "0.5.3" From 6bb1b300dfbd89c5dd1fea92c367f1f1937bf6bd Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 22 Oct 2026 03:10:58 +0000 Subject: [PATCH 14/23] test(browser): bind rebinding target to the allowed port The DNS rebinding live test now binds its private listener to the same port as the allowed listener on IPv6 loopback, so the rebound connection is actually directed at the private socket rather than an unrelated port. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 8eb71cc7..6a43e470 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -267,11 +267,11 @@ async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { #[tokio::test] #[cfg(live_browser_tests)] async fn live_chromium_dns_rebinding_never_reaches_the_rebound_private_socket() { - let private = std::net::TcpListener::bind("127.0.0.2:0").unwrap(); - private.set_nonblocking(true).unwrap(); - let private_address = private.local_addr().unwrap(); let allowed_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let port = allowed_listener.local_addr().unwrap().port(); + let private = std::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).unwrap(); + private.set_nonblocking(true).unwrap(); + let private_address = private.local_addr().unwrap(); let allowed_address = allowed_listener.local_addr().unwrap(); let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); let observed_addresses = Arc::new(Mutex::new(Vec::new())); From f2157ccbbce201202fc55b73e89f9b1559251159 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 08:42:05 +0000 Subject: [PATCH 15/23] test: add live browser egress test run to feature script The feature test script now runs the browser egress live tests with the egress environment flag enabled, matching the existing live browser test invocation so that this suite is exercised alongside the others. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- scripts/test-features.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/test-features.sh b/scripts/test-features.sh index a7b75f57..5debf3af 100755 --- a/scripts/test-features.sh +++ b/scripts/test-features.sh @@ -14,6 +14,7 @@ esac cargo test --locked --workspace cargo test --locked --workspace --all-features RUST_TEST_THREADS=1 TINYCOMPUTER_LIVE_BROWSER_TESTS=1 cargo test --locked -p tinycomputer-browser --features agent-browser live_ +RUST_TEST_THREADS=1 TINYCOMPUTER_LIVE_BROWSER_EGRESS_TESTS=1 cargo test --locked -p tinycomputer --features static-link tinybus_module::browser_egress_live_tests -- --nocapture cargo test --locked -p tinycomputer --features static-link cargo run --locked -p tinycomputer-examples --bin basic forbidden="$(cargo tree -p tinycomputer-bus -e normal,build --prefix none | grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2|agent-desktop' || true)" From c1467dcf37ed36f4157ac74b52e3a9565ad03966 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 08:53:37 +0000 Subject: [PATCH 16/23] test(browser): clone observed addresses before asserting Clone the locked address list before the assertion so the mutex guard is released prior to the comparison, avoiding a held lock across the assert. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../tinycomputer-browser/src/linked/linked_tests/live_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 1ed16172..d32e5ca7 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -367,7 +367,7 @@ async fn live_chromium_dns_rebinding_never_reaches_the_rebound_private_socket() .execute(json!({"id":"close","action":"close"})) .await; assert_eq!(navigation["success"], true, "{navigation}"); - let observed_addresses = observed_addresses.lock().unwrap(); + let observed_addresses = observed_addresses.lock().unwrap().clone(); assert_eq!( observed_addresses.first(), Some(&allowed_address), From 7c8001803133d53c95f4b5b87450e9260fba4ec9 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 08:53:49 +0000 Subject: [PATCH 17/23] test(browser): use to_string for static worker fixture html Replace the unnecessary format! call with a to_string conversion on the static HTML string in the live worker fetch test, since the literal contains no interpolation. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../tinycomputer-browser/src/linked/linked_tests/live_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index d32e5ca7..f88a3381 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -242,7 +242,7 @@ async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { let denied_targets = Arc::new(Mutex::new(Vec::new())); let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = fixture.local_addr().unwrap(); - let html = format!("

worker fixture

"); + let html = "

worker fixture

".to_string(); let worker = format!( "fetch('http://127.0.0.1:{}/private').catch(() => {{}});", denied_address.port() From c38ef880efa16e438c8e14e502837bc613d1eeca Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 11:56:10 +0000 Subject: [PATCH 18/23] test(browser): bind denied listeners on distinct ephemeral ports The live egress tests previously bound the denied listener to a second loopback address on the same port as the allowed listener, which is not portable across environments. Each denied listener now binds to its own ephemeral port and the tests assert the ports differ, so the fixtures exercise host-based denial rather than address-based separation. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 8 +++++--- .../browser_egress_live_tests/dns_rebinding_tests.rs | 3 ++- .../src/tinybus_module/browser_egress_live_tests/mod.rs | 6 ++++-- .../tinybus_module/browser_egress_live_tests/worker.rs | 7 ++++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index f88a3381..21a89cd6 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -69,10 +69,12 @@ async fn live_chromium_loopback_navigation_uses_the_authenticated_session_proxy( #[tokio::test] #[cfg(live_browser_tests)] async fn live_chromium_dials_only_the_exact_host_authorized_socket() { - let allowed = TcpListener::bind("127.0.0.2:0").await.unwrap(); + let allowed = TcpListener::bind("127.0.0.1:0").await.unwrap(); let allowed_address = allowed.local_addr().unwrap(); - let denied = std::net::TcpListener::bind(("127.0.0.1", allowed_address.port())).unwrap(); + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + assert_ne!(allowed_address.port(), denied_address.port()); let allowed_server = tokio::spawn(async move { let (mut socket, _) = allowed.accept().await.unwrap(); @@ -95,7 +97,7 @@ async fn live_chromium_dials_only_the_exact_host_authorized_socket() { .execute(json!({ "id":"navigate", "action":"navigate", - "url":format!("http://127.0.0.1:{}/", allowed_address.port()) + "url":format!("http://127.0.0.1:{}/", denied_address.port()) })) .await; browser diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs index 54a608de..e0d0d416 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs @@ -6,9 +6,10 @@ use super::*; async fn live_chromium_module_never_dials_a_rebound_private_socket() -> tinybus::Result<()> { let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = allowed_listener.local_addr()?.port(); - let rebound_listener = tokio::net::TcpListener::bind(("127.0.0.2", port)).await?; + let rebound_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let allowed_address = allowed_listener.local_addr()?.to_string(); let rebound_address = rebound_listener.local_addr()?.to_string(); + assert_ne!(port, rebound_listener.local_addr()?.port()); let extra_connections = Arc::new(AtomicUsize::new(0)); let (served_tx, served_rx) = tokio::sync::oneshot::channel(); let server = tokio::spawn(serve_fixture( diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs index 90923124..24b53f98 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs @@ -275,7 +275,9 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch -> tinybus::Result<()> { let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = allowed_listener.local_addr()?.port(); - let denied_listener = tokio::net::TcpListener::bind(("127.0.0.2", port)).await?; + let denied_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let denied_port = denied_listener.local_addr()?.port(); + assert_ne!(port, denied_port); let allowed_address = allowed_listener.local_addr()?.to_string(); let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); let (served_tx, served_rx) = tokio::sync::oneshot::channel(); @@ -303,7 +305,7 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch // Simulate a policy refresh: the host changes from granting the fixture // socket to denying requests for the same hostname. browser.allowed.store(false, Ordering::SeqCst); - let denied_url = format!("http://fixture.test:{port}/after-policy-change"); + let denied_url = format!("http://fixture.test:{denied_port}/after-policy-change"); let denied: DesktopResponse = browser .proxy .call( diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs index 4d77c63e..ddd1bcee 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs @@ -5,12 +5,13 @@ async fn live_chromium_module_routes_worker_egress_through_host_authorization() -> tinybus::Result<()> { let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let page_port = allowed_listener.local_addr()?.port(); - let denied_listener = tokio::net::TcpListener::bind("127.0.0.2:0").await?; + let denied_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let denied_port = denied_listener.local_addr()?.port(); + assert_ne!(page_port, denied_port); let html = format!( "" ) .into_bytes(); @@ -30,7 +31,7 @@ async fn live_chromium_module_routes_worker_egress_through_host_authorization() start_browser_with_fixture_host(allowed_address, page_port, "/worker-fixture", served_rx) .await?; - let worker_authorization = format!("authorize:fixture-policy:41:127.0.0.2:{denied_port}"); + let worker_authorization = format!("authorize:fixture-policy:41:127.0.0.1:{denied_port}"); let worker_authorized = tokio::time::timeout(Duration::from_secs(10), async { loop { if browser From f94c53926d12c965f12f594374f75dde5c82aa21 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 11:57:06 +0000 Subject: [PATCH 19/23] test(browser): authorize denied host in live egress tests The live egress tests now pass the denied listener's full socket address to the browser fixture host instead of only its port, so the denied host is explicitly authorized and the tests exercise the intended rejection path. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 2 +- .../tinybus_module/browser_egress_live_tests/mod.rs | 13 +++++++++---- .../browser_egress_live_tests/worker.rs | 7 ++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs index 21a89cd6..86ab91af 100644 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs @@ -97,7 +97,7 @@ async fn live_chromium_dials_only_the_exact_host_authorized_socket() { .execute(json!({ "id":"navigate", "action":"navigate", - "url":format!("http://127.0.0.1:{}/", denied_address.port()) + "url":format!("http://{denied_address}/") })) .await; browser diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs index 24b53f98..9025ed2e 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/mod.rs @@ -276,7 +276,8 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = allowed_listener.local_addr()?.port(); let denied_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; - let denied_port = denied_listener.local_addr()?.port(); + let denied_address = denied_listener.local_addr()?; + let denied_port = denied_address.port(); assert_ne!(port, denied_port); let allowed_address = allowed_listener.local_addr()?.to_string(); let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); @@ -290,9 +291,13 @@ async fn live_chromium_module_dials_only_the_host_returned_socket_and_rejects_ch None, )); - let browser = - start_browser_with_fixture_host(allowed_address.clone(), port, "/allowed", served_rx) - .await?; + let browser = start_browser_with_fixture_hosts( + vec![allowed_address.clone(), denied_address.to_string()], + port, + "/allowed", + served_rx, + ) + .await?; let authorizations_before_policy_change = browser .observations diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs index ddd1bcee..5200a1a1 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/worker.rs @@ -6,12 +6,13 @@ async fn live_chromium_module_routes_worker_egress_through_host_authorization() let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let page_port = allowed_listener.local_addr()?.port(); let denied_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; - let denied_port = denied_listener.local_addr()?.port(); + let denied_address = denied_listener.local_addr()?; + let denied_port = denied_address.port(); assert_ne!(page_port, denied_port); let html = format!( "" ) .into_bytes(); @@ -31,7 +32,7 @@ async fn live_chromium_module_routes_worker_egress_through_host_authorization() start_browser_with_fixture_host(allowed_address, page_port, "/worker-fixture", served_rx) .await?; - let worker_authorization = format!("authorize:fixture-policy:41:127.0.0.1:{denied_port}"); + let worker_authorization = format!("authorize:fixture-policy:41:{denied_address}"); let worker_authorized = tokio::time::timeout(Duration::from_secs(10), async { loop { if browser From ce5b656bac1cce10d4cc786c8e770c4b779b0714 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 12:01:55 +0000 Subject: [PATCH 20/23] test(browser_egress): clone observations to release the lock earlier The DNS rebinding test now clones the observation list instead of holding the mutex guard across the assertions, so the lock is released before the proxy call that follows. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../browser_egress_live_tests/dns_rebinding_tests.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs index e0d0d416..8f353655 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs @@ -67,7 +67,7 @@ async fn live_chromium_module_never_dials_a_rebound_private_socket() -> tinybus: "the request reached the previously allowed socket after rebinding" ); - let calls = browser.observations.lock().unwrap(); + let calls = browser.observations.lock().unwrap().clone(); assert!( calls .iter() @@ -88,8 +88,6 @@ async fn live_chromium_module_never_dials_a_rebound_private_socket() -> tinybus: .any(|call| call == &format!("consume:{rebound_address}")), "the rebound socket was consumed: {calls:?}" ); - drop(calls); - let closed: DesktopResponse = browser .proxy .call( From 9e0d7fbe7da49f4e593d984bcec2d9968363c981 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 12:07:37 +0000 Subject: [PATCH 21/23] test(browser): split live egress tests into focused modules The monolithic live_tests.rs file was replaced by a live_tests directory containing separate modules for navigation, denied requests, and DNS rebinding scenarios. This keeps each live Chromium egress test group readable and makes it easier to extend coverage without growing a single oversized file. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../src/linked/linked_tests/live_tests.rs | 759 ------------------ .../live_tests/denied_requests_tests.rs | 247 ++++++ .../live_tests/dns_rebinding_tests.rs | 91 +++ .../src/linked/linked_tests/live_tests/mod.rs | 270 +++++++ .../live_tests/navigation_tests.rs | 158 ++++ 5 files changed, 766 insertions(+), 759 deletions(-) delete mode 100644 crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs create mode 100644 crates/tinycomputer-browser/src/linked/linked_tests/live_tests/denied_requests_tests.rs create mode 100644 crates/tinycomputer-browser/src/linked/linked_tests/live_tests/dns_rebinding_tests.rs create mode 100644 crates/tinycomputer-browser/src/linked/linked_tests/live_tests/mod.rs create mode 100644 crates/tinycomputer-browser/src/linked/linked_tests/live_tests/navigation_tests.rs diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs deleted file mode 100644 index 86ab91af..00000000 --- a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests.rs +++ /dev/null @@ -1,759 +0,0 @@ -//! Live Chromium egress tests that use local HTTP and WebSocket fixtures. -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use super::EnforcedAgentBrowser; -use crate::engine::Launcher; -use crate::{BrowserDialTarget, DialGrant, EgressAuthorizer}; -use serde_json::json; -use std::{ - collections::HashMap, - io, - net::SocketAddr, - sync::{ - Arc, Mutex, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpListener, -}; - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_loopback_navigation_uses_the_authenticated_session_proxy() { - let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = fixture.local_addr().unwrap(); - let server = tokio::spawn(async move { - let (mut socket, _) = fixture.accept().await.unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all( - b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 22\r\nConnection: close\r\n\r\n

proxy fixture

", - ) - .await - .unwrap(); - }); - let authorizations = Arc::new(AtomicUsize::new(0)); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { - address, - authorizations: authorizations.clone(), - denied_targets: Arc::new(Mutex::new(Vec::new())), - })) - .open("live-egress"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", address.port()) - })) - .await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!( - navigation["success"], - true, - "{navigation}; proxy authorizations: {}", - authorizations.load(Ordering::SeqCst) - ); - server.await.unwrap(); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_dials_only_the_exact_host_authorized_socket() { - let allowed = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let allowed_address = allowed.local_addr().unwrap(); - let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - denied.set_nonblocking(true).unwrap(); - let denied_address = denied.local_addr().unwrap(); - assert_ne!(allowed_address.port(), denied_address.port()); - - let allowed_server = tokio::spawn(async move { - let (mut socket, _) = allowed.accept().await.unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all( - b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 30\r\nConnection: close\r\n\r\n

pinned socket fixture

", - ) - .await - .unwrap(); - }); - let mut browser = EnforcedAgentBrowser::new(Arc::new(AuthorizeExactSocket(allowed_address))) - .open("live-egress-socket-pinning"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://{denied_address}/") - })) - .await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - - assert_eq!(navigation["success"], true, "{navigation}"); - allowed_server.await.unwrap(); - assert_eq!( - denied.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "Chromium must reach only the exact socket returned by the host grant" - ); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_subresource_never_reaches_a_denied_destination() { - let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - denied.set_nonblocking(true).unwrap(); - let denied_address = denied.local_addr().unwrap(); - let denied_targets = Arc::new(Mutex::new(Vec::new())); - let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = fixture.local_addr().unwrap(); - let body = format!( - "", - denied_address.port() - ); - let fixture_server = tokio::spawn(async move { - let (mut socket, _) = fixture.accept().await.unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), body - ) - .as_bytes(), - ) - .await - .unwrap(); - }); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { - address, - authorizations: Arc::new(AtomicUsize::new(0)), - denied_targets: denied_targets.clone(), - })) - .open("live-egress-subresource"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", address.port()) - })) - .await; - wait_for_denied_target(&denied_targets, denied_address.port()).await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], true, "{navigation}"); - fixture_server.await.unwrap(); - assert_eq!( - denied.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "the denied subresource destination must never receive a connection" - ); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_uses_a_host_permitted_cross_origin_subresource_socket() { - let cdn_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let cdn_address = cdn_fixture.local_addr().unwrap(); - let page_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let page_address = page_fixture.local_addr().unwrap(); - let html = format!( - "

page fixture

", - cdn_address.port() - ); - let page_server = tokio::spawn(async move { - let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), page_fixture.accept()) - .await - .expect("Chromium requests the permitted page") - .unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}", - html.len() - ); - socket.write_all(response.as_bytes()).await.unwrap(); - }); - let cdn_server = tokio::spawn(async move { - let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), cdn_fixture.accept()) - .await - .expect("Chromium requests the permitted cross-origin asset") - .unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all(b"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") - .await - .unwrap(); - }); - let addresses = HashMap::from([ - ("127.0.0.1".to_owned(), page_address), - ("cdn.fixture.test".to_owned(), cdn_address), - ]); - let mut browser = EnforcedAgentBrowser::new(Arc::new(AllowFixtureHosts { - addresses, - authorizations: Arc::new(AtomicUsize::new(0)), - })) - .open("live-egress-permitted-cross-origin"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", page_address.port()) - })) - .await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], true, "{navigation}"); - page_server.await.unwrap(); - cdn_server.await.unwrap(); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { - let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - denied.set_nonblocking(true).unwrap(); - let denied_address = denied.local_addr().unwrap(); - let denied_targets = Arc::new(Mutex::new(Vec::new())); - let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = fixture.local_addr().unwrap(); - let html = "

worker fixture

".to_string(); - let worker = format!( - "fetch('http://127.0.0.1:{}/private').catch(() => {{}});", - denied_address.port() - ); - let fixture_server = tokio::spawn(async move { - loop { - let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), fixture.accept()) - .await - .expect("Chromium requests the page and worker script") - .unwrap(); - let mut request = [0; 4096]; - let size = socket.read(&mut request).await.unwrap(); - let request = String::from_utf8_lossy(&request[..size]); - let (status, content_type, body, worker_script) = if request.contains("GET /worker.js") - { - ("200 OK", "application/javascript", worker.as_str(), true) - } else if request.contains("GET / ") { - ("200 OK", "text/html", html.as_str(), false) - } else { - ("404 Not Found", "text/plain", "not found", false) - }; - socket - .write_all( - format!( - "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ) - .as_bytes(), - ) - .await - .unwrap(); - if worker_script { - break; - } - } - }); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { - address, - authorizations: Arc::new(AtomicUsize::new(0)), - denied_targets: denied_targets.clone(), - })) - .open("live-egress-worker"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", address.port()) - })) - .await; - wait_for_denied_target(&denied_targets, denied_address.port()).await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], true, "{navigation}"); - fixture_server.await.unwrap(); - assert_eq!( - denied.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "the denied worker fetch destination must never receive a connection" - ); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_dns_rebinding_never_reaches_the_rebound_private_socket() { - let allowed_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let port = allowed_listener.local_addr().unwrap().port(); - let private = std::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).unwrap(); - private.set_nonblocking(true).unwrap(); - let private_address = private.local_addr().unwrap(); - let allowed_address = allowed_listener.local_addr().unwrap(); - let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); - let observed_addresses = Arc::new(Mutex::new(Vec::new())); - let extra_connections = extra_allowed_connections.clone(); - let body = format!("

fixture

"); - let allowed_server = tokio::spawn(async move { - let (mut socket, _) = - tokio::time::timeout(Duration::from_secs(10), allowed_listener.accept()) - .await - .expect("authorized navigation reaches the allowed fixture") - .unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - let response = format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", - body.len() - ); - socket.write_all(response.as_bytes()).await.unwrap(); - drop(socket); - - while let Ok((socket, _)) = allowed_listener.accept().await { - extra_connections.fetch_add(1, Ordering::SeqCst); - drop(socket); - } - }); - let denied_targets = Arc::new(Mutex::new(Vec::new())); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RebindingAuthorizer { - allowed_address, - private_address, - authorizations: AtomicUsize::new(0), - denied_targets: denied_targets.clone(), - observed_addresses: observed_addresses.clone(), - })) - .open("live-egress-dns-rebinding"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://fixture.test:{port}/") - })) - .await; - wait_for_denied_target(&denied_targets, port).await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], true, "{navigation}"); - let observed_addresses = observed_addresses.lock().unwrap().clone(); - assert_eq!( - observed_addresses.first(), - Some(&allowed_address), - "the controlled resolver first returns the permitted address" - ); - assert!( - observed_addresses.len() >= 2 - && observed_addresses[1..] - .iter() - .all(|address| *address == private_address), - "the controlled resolver must rebind the name to the private address: {observed_addresses:?}" - ); - assert!( - tokio::time::timeout(Duration::from_millis(500), async { - while extra_allowed_connections.load(Ordering::SeqCst) == 0 { - tokio::task::yield_now().await; - } - }) - .await - .is_err(), - "the denied rebound request reached the previously allowed socket" - ); - assert_eq!( - private.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "the rebound private destination must never receive a connection" - ); - allowed_server.abort(); - let _ = allowed_server.await; -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_redirect_never_reaches_a_denied_destination() { - let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - denied.set_nonblocking(true).unwrap(); - let denied_address = denied.local_addr().unwrap(); - let denied_targets = Arc::new(Mutex::new(Vec::new())); - let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = fixture.local_addr().unwrap(); - let fixture_server = tokio::spawn(async move { - let (mut socket, _) = fixture.accept().await.unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all( - format!( - "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/private\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", - denied_address.port() - ) - .as_bytes(), - ) - .await - .unwrap(); - }); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { - address, - authorizations: Arc::new(AtomicUsize::new(0)), - denied_targets: denied_targets.clone(), - })) - .open("live-egress-redirect"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", address.port()) - })) - .await; - wait_for_denied_target(&denied_targets, denied_address.port()).await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], false, "{navigation}"); - fixture_server.await.unwrap(); - assert_eq!( - denied.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "the denied redirect destination must never receive a connection" - ); -} - -#[tokio::test] -#[cfg(live_browser_tests)] -async fn live_chromium_websocket_never_reaches_a_denied_destination() { - let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - denied.set_nonblocking(true).unwrap(); - let denied_address = denied.local_addr().unwrap(); - let denied_targets = Arc::new(Mutex::new(Vec::new())); - let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let address = fixture.local_addr().unwrap(); - let body = format!( - "", - denied_address.port() - ); - let fixture_server = tokio::spawn(async move { - let (mut socket, _) = fixture.accept().await.unwrap(); - let mut request = [0; 4096]; - let _ = socket.read(&mut request).await.unwrap(); - socket - .write_all( - format!( - "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", - body.len(), body - ) - .as_bytes(), - ) - .await - .unwrap(); - }); - let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { - address, - authorizations: Arc::new(AtomicUsize::new(0)), - denied_targets: denied_targets.clone(), - })) - .open("live-egress-websocket"); - let launch = browser - .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) - .await; - assert_eq!(launch["success"], true, "{launch}"); - let navigation = browser - .execute(json!({ - "id":"navigate", - "action":"navigate", - "url":format!("http://127.0.0.1:{}/", address.port()) - })) - .await; - wait_for_denied_target(&denied_targets, denied_address.port()).await; - browser - .execute(json!({"id":"close","action":"close"})) - .await; - assert_eq!(navigation["success"], true, "{navigation}"); - fixture_server.await.unwrap(); - assert_eq!( - denied.accept().unwrap_err().kind(), - io::ErrorKind::WouldBlock, - "the denied WebSocket destination must never receive a connection" - ); -} - -#[cfg(live_browser_tests)] -#[derive(Debug)] -struct RejectAllButFixture { - address: SocketAddr, - authorizations: Arc, - denied_targets: Arc>>, -} - -#[cfg(live_browser_tests)] -#[derive(Debug)] -struct RebindingAuthorizer { - allowed_address: SocketAddr, - private_address: SocketAddr, - authorizations: AtomicUsize, - denied_targets: Arc>>, - observed_addresses: Arc>>, -} - -#[cfg(live_browser_tests)] -#[derive(Debug)] -struct AllowFixtureHosts { - addresses: HashMap, - authorizations: Arc, -} - -#[cfg(live_browser_tests)] -impl EgressAuthorizer for AllowFixtureHosts { - fn authorize( - &self, - target: BrowserDialTarget, - ) -> std::pin::Pin> + Send>> { - self.authorizations.fetch_add(1, Ordering::SeqCst); - let address = self - .addresses - .get(&target.hostname) - .copied() - .filter(|address| address.port() == target.port); - Box::pin(async move { - match address { - Some(address) => Ok(DialGrant { - lease: "live-cross-origin-fixture-lease".into(), - addresses: vec![address], - }), - None => Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")), - } - }) - } - - fn consume( - &self, - _: BrowserDialTarget, - grant: DialGrant, - address: SocketAddr, - ) -> std::pin::Pin> + Send>> { - Box::pin(async move { - if grant.addresses.contains(&address) { - Ok(()) - } else { - Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) - } - }) - } - - fn validate( - &self, - _: String, - ) -> std::pin::Pin> + Send>> { - Box::pin(async { Ok(true) }) - } -} - -#[cfg(live_browser_tests)] -impl EgressAuthorizer for RebindingAuthorizer { - fn authorize( - &self, - target: BrowserDialTarget, - ) -> std::pin::Pin> + Send>> { - let attempt = self.authorizations.fetch_add(1, Ordering::SeqCst); - let resolved_address = if attempt == 0 { - self.allowed_address - } else { - self.private_address - }; - self.observed_addresses - .lock() - .unwrap() - .push(resolved_address); - let allowed = attempt == 0 - && target.hostname == "fixture.test" - && target.port == self.allowed_address.port(); - if !allowed { - self.denied_targets - .lock() - .unwrap() - .push((target.hostname, target.port)); - } - Box::pin(async move { - if allowed { - Ok(DialGrant { - lease: "live-dns-rebinding-lease".into(), - addresses: vec![resolved_address], - }) - } else { - Err(io::Error::new( - io::ErrorKind::PermissionDenied, - "rebound private address denied", - )) - } - }) - } - - fn consume( - &self, - _: BrowserDialTarget, - grant: DialGrant, - address: SocketAddr, - ) -> std::pin::Pin> + Send>> { - Box::pin(async move { - if grant.addresses.contains(&address) { - Ok(()) - } else { - Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) - } - }) - } - - fn validate( - &self, - _: String, - ) -> std::pin::Pin> + Send>> { - Box::pin(async { Ok(true) }) - } -} - -#[cfg(live_browser_tests)] -#[derive(Debug)] -struct AuthorizeExactSocket(SocketAddr); - -#[cfg(live_browser_tests)] -impl EgressAuthorizer for AuthorizeExactSocket { - fn authorize( - &self, - _: BrowserDialTarget, - ) -> std::pin::Pin> + Send>> { - let address = self.0; - Box::pin(async move { - Ok(DialGrant { - lease: "live-pinned-socket-lease".into(), - addresses: vec![address], - }) - }) - } - - fn consume( - &self, - _: BrowserDialTarget, - grant: DialGrant, - address: SocketAddr, - ) -> std::pin::Pin> + Send>> { - Box::pin(async move { - if grant.addresses.contains(&address) { - Ok(()) - } else { - Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) - } - }) - } - - fn validate( - &self, - _: String, - ) -> std::pin::Pin> + Send>> { - Box::pin(async { Ok(true) }) - } -} - -#[cfg(live_browser_tests)] -async fn wait_for_denied_target(denied_targets: &Arc>>, port: u16) { - tokio::time::timeout(Duration::from_secs(10), async { - loop { - let was_denied = denied_targets - .lock() - .unwrap() - .iter() - .any(|(_, denied_port)| *denied_port == port); - if was_denied { - break; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .expect("Chromium should attempt the denied destination"); -} - -#[cfg(live_browser_tests)] -impl EgressAuthorizer for RejectAllButFixture { - fn authorize( - &self, - target: BrowserDialTarget, - ) -> std::pin::Pin> + Send>> { - let address = self.address; - self.authorizations.fetch_add(1, Ordering::SeqCst); - let allowed = target.hostname == "127.0.0.1" && target.port == address.port(); - if !allowed { - self.denied_targets - .lock() - .unwrap() - .push((target.hostname, target.port)); - } - Box::pin(async move { - if allowed { - Ok(DialGrant { - lease: "live-fixture-lease".into(), - addresses: vec![address], - }) - } else { - Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) - } - }) - } - - fn consume( - &self, - _: BrowserDialTarget, - grant: DialGrant, - address: SocketAddr, - ) -> std::pin::Pin> + Send>> { - Box::pin(async move { - if grant.addresses.contains(&address) { - Ok(()) - } else { - Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) - } - }) - } - - fn validate( - &self, - _: String, - ) -> std::pin::Pin> + Send>> { - Box::pin(async { Ok(true) }) - } -} diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/denied_requests_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/denied_requests_tests.rs new file mode 100644 index 00000000..3882f41b --- /dev/null +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/denied_requests_tests.rs @@ -0,0 +1,247 @@ +use super::*; + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_subresource_never_reaches_a_denied_destination() { + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let body = format!( + "", + denied_address.port() + ); + let fixture_server = tokio::spawn(async move { + let (mut socket, _) = fixture.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all( + format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), body + ) + .as_bytes(), + ) + .await + .unwrap(); + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: Arc::new(AtomicUsize::new(0)), + denied_targets: denied_targets.clone(), + })) + .open("live-egress-subresource"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + wait_for_denied_target(&denied_targets, denied_address.port()).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + fixture_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the denied subresource destination must never receive a connection" + ); +} + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_worker_fetch_never_reaches_a_denied_destination() { + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let html = "

worker fixture

".to_string(); + let worker = format!( + "fetch('http://127.0.0.1:{}/private').catch(() => {{}});", + denied_address.port() + ); + let fixture_server = tokio::spawn(async move { + loop { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), fixture.accept()) + .await + .expect("Chromium requests the page and worker script") + .unwrap(); + let mut request = [0; 4096]; + let size = socket.read(&mut request).await.unwrap(); + let request = String::from_utf8_lossy(&request[..size]); + let (status, content_type, body, worker_script) = if request.contains("GET /worker.js") + { + ("200 OK", "application/javascript", worker.as_str(), true) + } else if request.contains("GET / ") { + ("200 OK", "text/html", html.as_str(), false) + } else { + ("404 Not Found", "text/plain", "not found", false) + }; + socket + .write_all( + format!( + "HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .as_bytes(), + ) + .await + .unwrap(); + if worker_script { + break; + } + } + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: Arc::new(AtomicUsize::new(0)), + denied_targets: denied_targets.clone(), + })) + .open("live-egress-worker"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + wait_for_denied_target(&denied_targets, denied_address.port()).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + fixture_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the denied worker fetch destination must never receive a connection" + ); +} + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_redirect_never_reaches_a_denied_destination() { + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let fixture_server = tokio::spawn(async move { + let (mut socket, _) = fixture.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all( + format!( + "HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:{}/private\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + denied_address.port() + ) + .as_bytes(), + ) + .await + .unwrap(); + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: Arc::new(AtomicUsize::new(0)), + denied_targets: denied_targets.clone(), + })) + .open("live-egress-redirect"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + wait_for_denied_target(&denied_targets, denied_address.port()).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], false, "{navigation}"); + fixture_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the denied redirect destination must never receive a connection" + ); +} + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_websocket_never_reaches_a_denied_destination() { + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let body = format!( + "", + denied_address.port() + ); + let fixture_server = tokio::spawn(async move { + let (mut socket, _) = fixture.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all( + format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), body + ) + .as_bytes(), + ) + .await + .unwrap(); + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: Arc::new(AtomicUsize::new(0)), + denied_targets: denied_targets.clone(), + })) + .open("live-egress-websocket"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + wait_for_denied_target(&denied_targets, denied_address.port()).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + fixture_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the denied WebSocket destination must never receive a connection" + ); +} diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/dns_rebinding_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/dns_rebinding_tests.rs new file mode 100644 index 00000000..557d7a0e --- /dev/null +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/dns_rebinding_tests.rs @@ -0,0 +1,91 @@ +use super::*; + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_dns_rebinding_never_reaches_the_rebound_private_socket() { + let allowed_listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = allowed_listener.local_addr().unwrap().port(); + let private = std::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).unwrap(); + private.set_nonblocking(true).unwrap(); + let private_address = private.local_addr().unwrap(); + let allowed_address = allowed_listener.local_addr().unwrap(); + let extra_allowed_connections = Arc::new(AtomicUsize::new(0)); + let observed_addresses = Arc::new(Mutex::new(Vec::new())); + let extra_connections = extra_allowed_connections.clone(); + let body = format!("

fixture

"); + let allowed_server = tokio::spawn(async move { + let (mut socket, _) = + tokio::time::timeout(Duration::from_secs(10), allowed_listener.accept()) + .await + .expect("authorized navigation reaches the allowed fixture") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + drop(socket); + + while let Ok((socket, _)) = allowed_listener.accept().await { + extra_connections.fetch_add(1, Ordering::SeqCst); + drop(socket); + } + }); + let denied_targets = Arc::new(Mutex::new(Vec::new())); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RebindingAuthorizer { + allowed_address, + private_address, + authorizations: AtomicUsize::new(0), + denied_targets: denied_targets.clone(), + observed_addresses: observed_addresses.clone(), + })) + .open("live-egress-dns-rebinding"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://fixture.test:{port}/") + })) + .await; + wait_for_denied_target(&denied_targets, port).await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + let observed_addresses = observed_addresses.lock().unwrap().clone(); + assert_eq!( + observed_addresses.first(), + Some(&allowed_address), + "the controlled resolver first returns the permitted address" + ); + assert!( + observed_addresses.len() >= 2 + && observed_addresses[1..] + .iter() + .all(|address| *address == private_address), + "the controlled resolver must rebind the name to the private address: {observed_addresses:?}" + ); + assert!( + tokio::time::timeout(Duration::from_millis(500), async { + while extra_allowed_connections.load(Ordering::SeqCst) == 0 { + tokio::task::yield_now().await; + } + }) + .await + .is_err(), + "the denied rebound request reached the previously allowed socket" + ); + assert_eq!( + private.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "the rebound private destination must never receive a connection" + ); + allowed_server.abort(); + let _ = allowed_server.await; +} diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/mod.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/mod.rs new file mode 100644 index 00000000..92045cc2 --- /dev/null +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/mod.rs @@ -0,0 +1,270 @@ +//! Live Chromium egress tests that use local HTTP and WebSocket fixtures. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::EnforcedAgentBrowser; +use crate::engine::Launcher; +use crate::{BrowserDialTarget, DialGrant, EgressAuthorizer}; +use serde_json::json; +use std::{ + collections::HashMap, + io, + net::SocketAddr, + sync::{ + Arc, Mutex, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, +}; + +mod denied_requests_tests; +mod dns_rebinding_tests; +mod navigation_tests; + +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct RejectAllButFixture { + address: SocketAddr, + authorizations: Arc, + denied_targets: Arc>>, +} + +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct RebindingAuthorizer { + allowed_address: SocketAddr, + private_address: SocketAddr, + authorizations: AtomicUsize, + denied_targets: Arc>>, + observed_addresses: Arc>>, +} + +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct AllowFixtureHosts { + addresses: HashMap, + authorizations: Arc, +} + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for AllowFixtureHosts { + fn authorize( + &self, + target: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + self.authorizations.fetch_add(1, Ordering::SeqCst); + let address = self + .addresses + .get(&target.hostname) + .copied() + .filter(|address| address.port() == target.port); + Box::pin(async move { + match address { + Some(address) => Ok(DialGrant { + lease: "live-cross-origin-fixture-lease".into(), + addresses: vec![address], + }), + None => Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")), + } + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for RebindingAuthorizer { + fn authorize( + &self, + target: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + let attempt = self.authorizations.fetch_add(1, Ordering::SeqCst); + let resolved_address = if attempt == 0 { + self.allowed_address + } else { + self.private_address + }; + self.observed_addresses + .lock() + .unwrap() + .push(resolved_address); + let allowed = attempt == 0 + && target.hostname == "fixture.test" + && target.port == self.allowed_address.port(); + if !allowed { + self.denied_targets + .lock() + .unwrap() + .push((target.hostname, target.port)); + } + Box::pin(async move { + if allowed { + Ok(DialGrant { + lease: "live-dns-rebinding-lease".into(), + addresses: vec![resolved_address], + }) + } else { + Err(io::Error::new( + io::ErrorKind::PermissionDenied, + "rebound private address denied", + )) + } + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} + +#[cfg(live_browser_tests)] +#[derive(Debug)] +struct AuthorizeExactSocket(SocketAddr); + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for AuthorizeExactSocket { + fn authorize( + &self, + _: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + let address = self.0; + Box::pin(async move { + Ok(DialGrant { + lease: "live-pinned-socket-lease".into(), + addresses: vec![address], + }) + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} + +#[cfg(live_browser_tests)] +async fn wait_for_denied_target(denied_targets: &Arc>>, port: u16) { + tokio::time::timeout(Duration::from_secs(10), async { + loop { + let was_denied = denied_targets + .lock() + .unwrap() + .iter() + .any(|(_, denied_port)| *denied_port == port); + if was_denied { + break; + } + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("Chromium should attempt the denied destination"); +} + +#[cfg(live_browser_tests)] +impl EgressAuthorizer for RejectAllButFixture { + fn authorize( + &self, + target: BrowserDialTarget, + ) -> std::pin::Pin> + Send>> { + let address = self.address; + self.authorizations.fetch_add(1, Ordering::SeqCst); + let allowed = target.hostname == "127.0.0.1" && target.port == address.port(); + if !allowed { + self.denied_targets + .lock() + .unwrap() + .push((target.hostname, target.port)); + } + Box::pin(async move { + if allowed { + Ok(DialGrant { + lease: "live-fixture-lease".into(), + addresses: vec![address], + }) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn consume( + &self, + _: BrowserDialTarget, + grant: DialGrant, + address: SocketAddr, + ) -> std::pin::Pin> + Send>> { + Box::pin(async move { + if grant.addresses.contains(&address) { + Ok(()) + } else { + Err(io::Error::new(io::ErrorKind::PermissionDenied, "denied")) + } + }) + } + + fn validate( + &self, + _: String, + ) -> std::pin::Pin> + Send>> { + Box::pin(async { Ok(true) }) + } +} diff --git a/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/navigation_tests.rs b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/navigation_tests.rs new file mode 100644 index 00000000..d3b1e199 --- /dev/null +++ b/crates/tinycomputer-browser/src/linked/linked_tests/live_tests/navigation_tests.rs @@ -0,0 +1,158 @@ +use super::*; + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_loopback_navigation_uses_the_authenticated_session_proxy() { + let fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let address = fixture.local_addr().unwrap(); + let server = tokio::spawn(async move { + let (mut socket, _) = fixture.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all( + b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 22\r\nConnection: close\r\n\r\n

proxy fixture

", + ) + .await + .unwrap(); + }); + let authorizations = Arc::new(AtomicUsize::new(0)); + let mut browser = EnforcedAgentBrowser::new(Arc::new(RejectAllButFixture { + address, + authorizations: authorizations.clone(), + denied_targets: Arc::new(Mutex::new(Vec::new())), + })) + .open("live-egress"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", address.port()) + })) + .await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!( + navigation["success"], + true, + "{navigation}; proxy authorizations: {}", + authorizations.load(Ordering::SeqCst) + ); + server.await.unwrap(); +} + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_dials_only_the_exact_host_authorized_socket() { + let allowed = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let allowed_address = allowed.local_addr().unwrap(); + let denied = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + denied.set_nonblocking(true).unwrap(); + let denied_address = denied.local_addr().unwrap(); + assert_ne!(allowed_address.port(), denied_address.port()); + + let allowed_server = tokio::spawn(async move { + let (mut socket, _) = allowed.accept().await.unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all( + b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 30\r\nConnection: close\r\n\r\n

pinned socket fixture

", + ) + .await + .unwrap(); + }); + let mut browser = EnforcedAgentBrowser::new(Arc::new(AuthorizeExactSocket(allowed_address))) + .open("live-egress-socket-pinning"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://{denied_address}/") + })) + .await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + + assert_eq!(navigation["success"], true, "{navigation}"); + allowed_server.await.unwrap(); + assert_eq!( + denied.accept().unwrap_err().kind(), + io::ErrorKind::WouldBlock, + "Chromium must reach only the exact socket returned by the host grant" + ); +} + +#[tokio::test] +#[cfg(live_browser_tests)] +async fn live_chromium_uses_a_host_permitted_cross_origin_subresource_socket() { + let cdn_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let cdn_address = cdn_fixture.local_addr().unwrap(); + let page_fixture = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let page_address = page_fixture.local_addr().unwrap(); + let html = format!( + "

page fixture

", + cdn_address.port() + ); + let page_server = tokio::spawn(async move { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), page_fixture.accept()) + .await + .expect("Chromium requests the permitted page") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}", + html.len() + ); + socket.write_all(response.as_bytes()).await.unwrap(); + }); + let cdn_server = tokio::spawn(async move { + let (mut socket, _) = tokio::time::timeout(Duration::from_secs(10), cdn_fixture.accept()) + .await + .expect("Chromium requests the permitted cross-origin asset") + .unwrap(); + let mut request = [0; 4096]; + let _ = socket.read(&mut request).await.unwrap(); + socket + .write_all(b"HTTP/1.1 200 OK\r\nContent-Type: image/png\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") + .await + .unwrap(); + }); + let addresses = HashMap::from([ + ("127.0.0.1".to_owned(), page_address), + ("cdn.fixture.test".to_owned(), cdn_address), + ]); + let mut browser = EnforcedAgentBrowser::new(Arc::new(AllowFixtureHosts { + addresses, + authorizations: Arc::new(AtomicUsize::new(0)), + })) + .open("live-egress-permitted-cross-origin"); + let launch = browser + .execute(json!({"id":"launch","action":"launch","headless":true,"args":[]})) + .await; + assert_eq!(launch["success"], true, "{launch}"); + let navigation = browser + .execute(json!({ + "id":"navigate", + "action":"navigate", + "url":format!("http://127.0.0.1:{}/", page_address.port()) + })) + .await; + browser + .execute(json!({"id":"close","action":"close"})) + .await; + assert_eq!(navigation["success"], true, "{navigation}"); + page_server.await.unwrap(); + cdn_server.await.unwrap(); +} From 6267be3b978f7fdbcc1c2d62a7eacb598660ac4a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 12:12:38 +0000 Subject: [PATCH 22/23] test(browser_egress): bind rebound listener to same port on ipv6 The DNS rebinding test now binds the rebound listener to the same port as the allowed listener but on the IPv6 loopback address, so the two sockets share a port while remaining distinct endpoints. The assertions were updated to check that the ports match and the addresses differ, which better exercises the rebinding scenario. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../browser_egress_live_tests/dns_rebinding_tests.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs index 8f353655..788692a7 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs @@ -6,10 +6,11 @@ use super::*; async fn live_chromium_module_never_dials_a_rebound_private_socket() -> tinybus::Result<()> { let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = allowed_listener.local_addr()?.port(); - let rebound_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let rebound_listener = tokio::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).await?; let allowed_address = allowed_listener.local_addr()?.to_string(); let rebound_address = rebound_listener.local_addr()?.to_string(); - assert_ne!(port, rebound_listener.local_addr()?.port()); + assert_eq!(port, rebound_listener.local_addr()?.port()); + assert_ne!(allowed_address, rebound_address); let extra_connections = Arc::new(AtomicUsize::new(0)); let (served_tx, served_rx) = tokio::sync::oneshot::channel(); let server = tokio::spawn(serve_fixture( From 8b5eb8e277a7478562ca94e0061b8cc64a6fd41f Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 23 Oct 2026 12:12:44 +0000 Subject: [PATCH 23/23] style: wrap long listener bind line in dns rebinding test Reformat the rebound listener bind call in the DNS rebinding live test to satisfy the line length limit. No behaviour change. Auto-committed-on: cuttlefish Co-authored-by: Medulla --- .../browser_egress_live_tests/dns_rebinding_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs index 788692a7..2d944438 100644 --- a/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs +++ b/crates/tinycomputer/src/tinybus_module/browser_egress_live_tests/dns_rebinding_tests.rs @@ -6,7 +6,8 @@ use super::*; async fn live_chromium_module_never_dials_a_rebound_private_socket() -> tinybus::Result<()> { let allowed_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = allowed_listener.local_addr()?.port(); - let rebound_listener = tokio::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).await?; + let rebound_listener = + tokio::net::TcpListener::bind((std::net::Ipv6Addr::LOCALHOST, port)).await?; let allowed_address = allowed_listener.local_addr()?.to_string(); let rebound_address = rebound_listener.local_addr()?.to_string(); assert_eq!(port, rebound_listener.local_addr()?.port());