diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index c808685..58bdc02 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ blank_issues_enabled: true contact_links: - name: Security reports - url: https://github.com/tinyhumansai/rust-template/security/policy + url: https://github.com/tinyhumansai/tinybrowser/security/policy about: Please do not report vulnerabilities through public issues. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba8c2fc..e26de19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,20 @@ jobs: rust: name: Rust runs-on: ubuntu-latest + env: + # A browser module is not meaningfully tested without a browser, and the + # coverage gate below is measured over the same run: with these unset, the + # protocol conversation would be both unexercised and uncovered, which is + # precisely the half that cannot be checked any other way. + # + # GitHub's Ubuntu runners ship Google Chrome at a path the module already + # looks in, so there is nothing to install. + TINYBROWSER_LIVE_TESTS: "1" + # Runners restrict unprivileged user namespaces, so Chrome has no usable + # sandbox here. Accepting that in a disposable CI VM is a different + # decision from applying it on somebody's host, which is why the module + # reports the condition rather than working around it by default. + TINYBROWSER_CHROME_ARGS: --no-sandbox steps: - uses: actions/checkout@v7 with: @@ -38,6 +52,31 @@ jobs: - uses: Swatinem/rust-cache@v2 + # Named explicitly rather than left to the module's own discovery order. + # The runner has several browsers on it, including a snap-packaged + # `/usr/bin/chromium` that starts, says nothing, and is timed out — and a + # build should not go red because the search order changed under it. This + # also prints which browser was chosen, so a future failure says so in the + # log instead of costing a round trip to find out. + - name: Select a browser for the live tests + run: | + set -euo pipefail + for candidate in \ + /usr/bin/google-chrome \ + /usr/bin/google-chrome-stable \ + /opt/google/chrome/chrome \ + /usr/bin/chromium-browser \ + /usr/bin/chromium + do + [ -x "$candidate" ] || continue + echo "selected $candidate" + "$candidate" --version || true + echo "TINYBROWSER_CHROME=$candidate" >> "$GITHUB_ENV" + exit 0 + done + echo "no browser on this runner; the live tests cannot run" >&2 + exit 1 + - name: Check formatting run: cargo fmt --all -- --check @@ -54,27 +93,27 @@ jobs: run: cargo test # `cargo build --all-targets` only *compiles* an example. `AGENTS.md` - # promises `cargo run -p template --example basic` works, and a compiled + # promises `cargo run -p tinybrowser --example basic` works, and a compiled # example can still fail on its first line. - name: Run the bundled example - run: cargo run -p template --example basic + run: cargo run -p tinybrowser --example basic - # `crates/template-bus` exists so a host can name the payload types + # `crates/tinybrowser-bus` exists so a host can name the payload types # without compiling the module. That promise is invisible in a diff, # because a forbidden dependency arrives transitively through a feature # someone enabled one crate away — so it is asserted rather than # documented. # - # The FORWARD form is required. `cargo tree -i -p template-bus` + # The FORWARD form is required. `cargo tree -i -p tinybrowser-bus` # discards the `-p` scope, prints the whole-workspace inverse tree, and # exits 0 looking clean even when this crate is the one at fault. - name: Assert the contract crate stays transport-free run: | set -euo pipefail - forbidden="$(cargo tree -p template-bus -e normal,build --prefix none \ + forbidden="$(cargo tree -p tinybrowser-bus -e normal,build --prefix none \ | grep -Ei 'tinybus|tokio|reqwest|ureq|hyper|rusqlite|git2' || true)" if [ -n "$forbidden" ]; then - echo "template-bus pulled in a dependency its manifest forbids:" >&2 + echo "tinybrowser-bus pulled in a dependency its manifest forbids:" >&2 echo "$forbidden" >&2 echo >&2 echo "The contract is what a host compiles against. It must stay free" >&2 @@ -128,7 +167,7 @@ jobs: run: | set -euo pipefail msrv="$(cargo metadata --format-version 1 --no-deps \ - | jq -r '.packages[] | select(.name == "template") | .rust_version')" + | jq -r '.packages[] | select(.name == "tinybrowser") | .rust_version')" if [[ -z "$msrv" || "$msrv" == "null" ]]; then echo "workspace.package.rust-version is not set in Cargo.toml" >&2 exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4acf379..f20cff8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,9 @@ permissions: env: # The workspace member that ships as the loadable module. Its package name is - # the artifact name and the library name; `crates/template-bus` rides along on + # the artifact name and the library name; `crates/tinybrowser-bus` rides along on # the same inherited version and is not packaged separately. - RELEASE_PACKAGE: template + RELEASE_PACKAGE: tinybrowser jobs: prepare: @@ -259,7 +259,7 @@ jobs: macOS) module="target/release/lib${library_name}.dylib" ;; *) echo "unsupported Unix runner: ${RUNNER_OS}" >&2; exit 1 ;; esac - cargo run --locked --package template --example verify_module -- "$module" + cargo run --locked --package tinybrowser --example verify_module -- "$module" - name: Verify Windows module through TinyBus loader if: ${{ runner.os == 'Windows' }} @@ -270,7 +270,7 @@ jobs: $ErrorActionPreference = 'Stop' $libraryName = $env:CRATE_NAME.Replace('-', '_') $module = "target/release/$libraryName.dll" - $verifyRoot = Join-Path $env:RUNNER_TEMP 'template-module-verify' + $verifyRoot = Join-Path $env:RUNNER_TEMP 'tinybrowser-module-verify' New-Item -ItemType Directory -Force $verifyRoot | Out-Null $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() @@ -300,7 +300,7 @@ jobs: $verifiedModule = Join-Path $verifyRoot "$libraryName.dll" Copy-Item -LiteralPath $module -Destination $verifiedModule - cargo run --locked --package template --example verify_module -- $verifiedModule + cargo run --locked --package tinybrowser --example verify_module -- $verifiedModule - name: Assemble Unix module package if: ${{ runner.os != 'Windows' }} @@ -463,7 +463,7 @@ jobs: verify_root="/opt/${CRATE_NAME}-module-verify" install -d -m 700 "$verify_root" install -m 755 "target/release/lib${library_name}.so" "$verify_root/" - cargo run --locked --package template --example verify_module -- \ + cargo run --locked --package tinybrowser --example verify_module -- \ "$verify_root/lib${library_name}.so" - name: Assemble distribution module package @@ -587,5 +587,5 @@ jobs: cargo run --manifest-path vendor/tinybus/Cargo.toml --locked \ --package tinybus --all-features --example github_module_host -- \ "$release_url" "$archive" "$sha256" - cargo run --locked --package template --example verify_github_release -- \ + cargo run --locked --package tinybrowser --example verify_github_release -- \ "$release_url" "$archive" "$sha256" diff --git a/AGENTS.md b/AGENTS.md index ee8fdfc..cb3b79d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,63 +4,45 @@ This file is the single source of truth for how humans and coding agents work in this repository. `CLAUDE.md` is a symlink to this file, so every agent reads the same instructions. -When you generate a new project from this template, keep this file and adapt -the project-specific parts (crate name, module map, feature flags, commands). -Delete guidance that no longer applies rather than leaving it to rot. - -## Template Checklist - -Do this once, in a single commit, before writing feature code: - -- [ ] Rename `crates/template` and `crates/template-bus` to the project's crate - names, and update `name` in each manifest plus the `template-bus` entry in - the root `[workspace.dependencies]`. -- [ ] Set `description`, `keywords`, and `categories` in each manifest, and - `repository` in the root `[workspace.package]`. -- [ ] Rename the crate references in `README.md`, both `src/lib.rs` files, - `crates/template/examples/`, and `crates/template/tests/` (search for - `template` and `template_bus`). -- [ ] Replace the placeholder `greeting` module in both crates with the first - real feature area — payload types in the contract crate, behavior in the - module crate — keeping the `mod.rs` / `types.rs` / `test.rs` layout. -- [ ] Confirm `license` and `LICENSE` match the project's intended license. -- [ ] Update the security contact in `SECURITY.md`. -- [ ] Rename the TinyBus interface, object path, and member constants in - `crates/template-bus/src/names/`, and the matching `provides` / `methods` - declarations in `crates/template/src/tinybus_module/`, while keeping - `vendor/tinybus` pinned. -- [ ] Reset `CONTRACT_VERSION` in `crates/template-bus/src/version/` for the new - contract. -- [ ] Replace `ROADMAP.md` with the real plan, or delete it. -- [ ] Rewrite the "Project Structure" section below to describe this workspace. +This project began as `rust-template` and no longer is one: the template +checklist has been worked through and removed. What remains describes this +workspace. ## Project Structure This is a Rust 2024 cargo workspace rooted at a virtual `Cargo.toml`. Every crate lives under `crates/`, one directory per package, each directory named for -the package it holds. There is no root package: the crate that ships as the -loadable module is `crates/template`, the same as any other member. +the package it holds. There is no root package: the crate a host loads is +`crates/tinybrowser`, the same as any other member. ```text Cargo.toml # virtual workspace: members, [workspace.package], # [workspace.dependencies], [workspace.lints] crates/ -├── template-bus/ # the wire contract: what crosses the bus, nothing else +├── tinybrowser-bus/ # the wire contract: what crosses the bus, nothing else │ ├── README.md # why the contract is its own crate │ └── src/ │ ├── lib.rs # crate docs + the entire public re-export surface │ ├── names/ # interface, object path, one constant per member │ ├── version/ # contract version and the host bind rule -│ └── / # one directory per payload family -└── template/ # the module: behavior, adapter, and the cdylib +│ ├── session/ # opening, listing, closing a browser +│ ├── page/ # navigation, extraction, evaluation +│ ├── snapshot/ # the accessibility tree and its refs +│ ├── action/ # every interaction, and how an element is named +│ ├── output/ # screenshots and the handle protocol that carries them +│ └── errors/ # the wire error names, and which an agent can act on +└── tinybrowser/ # the engine: behavior, adapter, and the cdylib ├── src/ - │ ├── lib.rs # crate docs + public surface, re-exporting the contract - │ ├── error/mod.rs # crate-wide `Error` and `Result` - │ ├── tinybus_module/ # TinyBus interface, ABI exports, integration tests - │ └── / # one directory per feature area - │ ├── mod.rs # module docs, wiring, smallest useful public API - │ ├── types.rs # substantial type definitions - │ └── test.rs # module-local unit tests + │ ├── lib.rs # crate docs + public surface, re-exporting the contract + │ ├── error/ # crate-wide `Error` and `Result` + │ ├── engine/ # `Browser`: the facade every bus member calls + │ ├── cdp/ # the socket, the endpoint, and launching a browser + │ ├── session/ # one browser and page, plus policy and refs + │ ├── snapshot/ # accessibility tree in, indented text and refs out + │ ├── interact/ # target resolution, key parsing, the in-page scripts + │ ├── extract/ # reading a page as text, Markdown, or DOM + │ ├── capture/ # screenshots and the held-output store + │ └── tinybus_module/ # TinyBus interface, ABI exports, integration tests ├── tests/ # integration tests against the public API only └── examples/ # runnable, compiled-in-CI usage examples vendor/tinybus/ # pinned TinyBus host types and module SDK @@ -72,19 +54,37 @@ docs/ ### The two-crate split -`crates/template-bus` holds every type that crosses the bus and the names of the -members that carry them. It has no transport, no runtime, and no behavior, and -CI asserts it stays that way. A host that only makes calls depends on it alone. +`crates/tinybrowser-bus` holds every type that crosses the bus and the names of +the members that carry them. It has no transport, no runtime, and no behavior, +and CI asserts it stays that way. A host that only makes calls depends on it +alone — which is the whole point, because that host is a binary that +deliberately does not want a browser stack in its build. -`crates/template` depends on it and re-exports all of it, so -`template::GreetRequest` and `template_bus::GreetRequest` are the *same* type -rather than structural twins. That direction is load-bearing: a parallel set of -payload types for hosts would mean a conversion at every call site that nothing -checks. +`crates/tinybrowser` depends on it and re-exports all of it, so +`tinybrowser::Action` and `tinybrowser_bus::Action` are the *same* type rather +than structural twins. That direction is load-bearing: a parallel set of payload +types for hosts would mean a conversion at every call site that nothing checks. The rule for deciding where something goes: a payload type describes what a frame carries and belongs in the contract; anything that answers a frame, holds -a connection, or touches an engine belongs in the module crate. +a connection, or touches a browser belongs in the module crate. + +### Where the layers meet + +`engine::Browser` is the only thing `tinybus_module` calls, and it is the only +thing that holds sessions and outputs. Everything below it — `cdp`, `session`, +`snapshot`, `interact`, `extract`, `capture` — is private and written against +`session::Session` rather than against the protocol. Two rules keep that from +eroding: + +- **No decisions in the adapter.** `tinybus_module` deserializes, calls one + method, and maps an error to a wire name. Anything decided there could only be + tested through a bus, and a Rust caller using `Browser` directly would not get + it. +- **Judgement goes in a pure function.** Snapshot rendering, key parsing, URL + policy, and output chunking are all pure, and all unit-tested without a + browser. If a rule can only be exercised against a live Chrome, it is in the + wrong place. Add a crate by creating `crates//` — `members = ["crates/*"]` picks it up by existing. Inherit `version`, `edition`, `rust-version`, `license`, and @@ -114,7 +114,7 @@ broad ones. Keep public exports centralized in each crate's `src/lib.rs` so downstream users have one predictable surface. Put shared error variants in -`crates/template/src/error/mod.rs` and return the crate-wide `Result` from +`crates/tinybrowser/src/error/mod.rs` and return the crate-wide `Result` from fallible public APIs. ## Build And Test @@ -133,8 +133,11 @@ Supporting commands: - `cargo fmt --all` — format before committing. - `cargo test ` — run a focused subset while iterating. -- `cargo test -p template-bus` — run one crate's suite. -- `cargo run -p template --example basic` — run the bundled example. +- `cargo test -p tinybrowser-bus` — run one crate's suite. +- `cargo run -p tinybrowser --example basic` — run the bundled example. +- `TINYBROWSER_LIVE_TESTS=1 cargo test -p tinybrowser --test live_chrome` — the + end-to-end suite, against a real browser. Opt-in, because a machine without + one would fail it for a reason unrelated to the change. - `cargo doc --no-deps --all-features` — build the rustdoc CI also builds with `RUSTDOCFLAGS="-D warnings"`. - `cargo test --doc` — run doctests alone when editing documentation examples. @@ -185,7 +188,7 @@ add one: - gate anything optional behind a Cargo feature, documented in `Cargo.toml`; - declare it once in the root `[workspace.dependencies]` when more than one crate needs it, and take it with `{ workspace = true }`; -- never add one to `crates/template-bus` that pulls in a transport, an async +- never add one to `crates/tinybrowser-bus` that pulls in a transport, an async runtime, an HTTP client, or a native library — CI fails the build if you do; - leave a comment above the entry explaining *why* the crate is needed and what uses it — see the existing entries for the expected tone; @@ -226,7 +229,14 @@ new module capability requires more. for tests) rather than mixing runtimes. - Tests must be deterministic and independent of network, wall-clock time, and execution order. Gate any live/network test behind a feature or an env var and - name it `live_*` so it is easy to exclude. + name it `live_*` so it is easy to exclude. Use an env var rather than a Cargo + feature here: the contract command is `cargo test --all-features`, which would + switch a feature on. +- **A live test that skips silently is worse than no test.** `live_chrome` + checks its opt-in once and then fails if it cannot find a browser. A suite that + quietly does nothing reports green for a build in which nothing was checked. +- The unit suites must not need a browser. Anything that would is a sign the + judgement belongs in a pure function that can be tested without one. - Maintain at least 90% line coverage in every source file. Add or update tests with every behavior change, and note any deliberately untested edge case in the pull request description. @@ -295,7 +305,7 @@ Releases run from `.github/workflows/release.yml` via a manual an interrupted release after its version commit and tag exist. The workflow re-runs the full validation suite, computes the next version, updates the root `[workspace.package]` version and `Cargo.lock`, commits and tags -`vX.Y.Z`, builds `crates/template` as a TinyBus module for every supported +`vX.Y.Z`, builds `crates/tinybrowser` as a TinyBus module for every supported platform, pushes, and creates an immutable GitHub release with installable native packages. diff --git a/Cargo.lock b/Cargo.lock index b4f454e..f5f47e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,18 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64" version = "0.23.1" @@ -40,12 +52,27 @@ version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" @@ -68,6 +95,41 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -83,6 +145,22 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "derive_arbitrary" version = "1.4.2" @@ -94,6 +172,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "displaydoc" version = "0.2.7" @@ -153,6 +241,65 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -160,8 +307,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -171,8 +320,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core 0.10.1", + "wasm-bindgen", ] [[package]] @@ -191,12 +343,198 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -207,12 +545,29 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.189" @@ -225,12 +580,24 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.8.3" @@ -247,6 +614,17 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -265,6 +643,24 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -274,6 +670,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.20", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.20", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -289,6 +741,100 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + [[package]] name = "ring" version = "0.17.14" @@ -303,6 +849,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustix" version = "1.1.4" @@ -337,6 +889,7 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ + "web-time", "zeroize", ] @@ -351,6 +904,18 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "serde" version = "1.0.229" @@ -403,18 +968,90 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "subtle" version = "2.6.1" @@ -433,14 +1070,34 @@ dependencies = [ ] [[package]] -name = "syn" -version = "3.0.3" +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.119", ] [[package]] @@ -468,32 +1125,32 @@ dependencies = [ ] [[package]] -name = "template" -version = "0.2.1" +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "serde_json", - "template-bus", - "thiserror", - "tinybus", - "tinybus-module", - "tokio", + "thiserror-impl 1.0.69", ] [[package]] -name = "template-bus" -version = "0.2.1" +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "serde", - "serde_json", + "thiserror-impl 2.0.20", ] [[package]] -name = "thiserror" -version = "2.0.20" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -507,6 +1164,34 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tinybrowser" +version = "0.2.1" +dependencies = [ + "base64 0.22.1", + "futures-util", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.20", + "tinybrowser-bus", + "tinybus", + "tinybus-module", + "tokio", + "tokio-tungstenite", + "url", + "uuid", +] + +[[package]] +name = "tinybrowser-bus" +version = "0.2.1" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinybus" version = "0.1.0" @@ -517,7 +1202,7 @@ dependencies = [ "serde_json", "tar", "tempfile", - "thiserror", + "thiserror 2.0.20", "tinybus-macros", "tokio", "toml", @@ -547,6 +1232,31 @@ dependencies = [ "tracing", ] +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" @@ -554,8 +1264,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", + "libc", + "mio", "pin-project-lite", + "signal-hook-registry", + "socket2", "tokio-macros", + "windows-sys 0.61.2", ] [[package]] @@ -569,6 +1284,32 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + [[package]] name = "toml" version = "0.8.23" @@ -610,6 +1351,51 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -641,6 +1427,38 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.7", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -659,7 +1477,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" dependencies = [ - "base64", + "base64 0.23.1", "flate2", "log", "percent-encoding", @@ -667,7 +1485,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots", + "webpki-roots 1.0.9", ] [[package]] @@ -676,24 +1494,158 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" dependencies = [ - "base64", + "base64 0.23.1", "http", "httparse", "log", ] +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8-zero" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cefc03fd367c0c6d4305de1b312cf00248c4114f4a0418ce6a6af769e3b0bd9" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "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.9", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -800,6 +1752,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + [[package]] name = "xattr" version = "1.6.1" @@ -810,12 +1768,109 @@ dependencies = [ "rustix", ] +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + [[package]] name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + [[package]] name = "zip" version = "2.4.2" @@ -829,7 +1884,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror", + "thiserror 2.0.20", "zopfli", ] diff --git a/Cargo.toml b/Cargo.toml index fae3c38..855102d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,15 +22,15 @@ version = "0.2.1" edition = "2024" rust-version = "1.88" license = "GPL-3.0-only" -repository = "https://github.com/tinyhumansai/rust-template" +repository = "https://github.com/tinyhumansai/tinybrowser" [workspace.dependencies] -# The wire contract. `crates/template` depends on it and re-exports it, so a +# The wire contract. `crates/tinybrowser` depends on it and re-exports it, so a # host that only makes calls takes this crate alone. # No `version` requirement on purpose: the workspace version moves on every # release, and a pinned requirement here would stop resolving the moment it did. # Nothing in this workspace is published, so the path is the whole address. -template-bus = { path = "crates/template-bus" } +tinybrowser-bus = { path = "crates/tinybrowser-bus" } # TinyBus defines the message types, interface macro, and frozen module ABI # used by the generated integration. Socket and CLI features are unnecessary # here. diff --git a/MODULE.md b/MODULE.md index 651906e..32dd078 100644 --- a/MODULE.md +++ b/MODULE.md @@ -1,15 +1,46 @@ -# Template TinyBus Module +# TinyBrowser Module -This package contains the native `template` module for TinyBus module ABI +This package contains the native `tinybrowser` module for TinyBus module ABI v1. Install only the archive matching the host operating system and architecture. -The module claims `ai.tinyhumans.template.Greeting`, serves the object at -`/ai/tinyhumans/template/Greeting`, and provides the `Greet` method. The -method accepts a `GreetRequest` and returns a `GreetResponse` carrying -`Hello, !`; empty names are rejected. Both payload types, the interface -name, the object path, and the member names are published as the `template-bus` -crate, so a host names them from a library rather than by string literal. +The module claims `ai.tinyhumans.tinybrowser.Browser`, serves the object at +`/ai/tinyhumans/tinybrowser/Browser`, and provides twelve methods: +`OpenSession`, `CloseSession`, `ListSessions`, `Navigate`, `Snapshot`, +`Perform`, `ReadPage`, `Evaluate`, `Screenshot`, `ReadOutput`, `ReleaseOutput`, +and `ContractVersion`. Every payload type, the interface name, the object path, +and the member names are published as the `tinybrowser-bus` crate, so a host +names them from a library rather than by string literal. + +## What it needs from the host + +A Chrome or Chromium, or a DevTools endpoint to attach to. The module looks in +the conventional install locations for the platform. Two environment variables +override that, and both describe the machine rather than any one caller: + +- `TINYBROWSER_CHROME` — the browser binary to launch. +- `TINYBROWSER_CHROME_ARGS` — extra launch flags for every session. + `--no-sandbox` belongs here on a host where unprivileged user namespaces are + restricted, which is the default on Ubuntu 23.10 and later and in containers + without the right capabilities. + +A launched browser gets a fresh profile directory that is removed when its +session closes, so one session's cookies and logins never reach the next. A +session that attaches to an endpoint leaves that browser running when it closes. + +## Operational notes + +A session is a browser process. The module holds at most eight at once and +refuses the ninth rather than starting it; a host is expected to close what it +opens. Screenshots are held for collection, capped at 16 outstanding and expired +after five minutes, and a host that reads one to completion should release it. + +The `allowed_origins` on a session is a guard rail, not a sandbox: it refuses +navigations the module is asked to make, and a page's own JavaScript can +navigate around it. A host that needs a real boundary puts the browser in a +network namespace that only reaches what it should. + +## Installing The archive contains one `.so`, `.dylib`, or `.dll` plus `modules.toml`. Keep those files together when copying them into a TinyBus module directory. The @@ -22,8 +53,8 @@ archive. Install directly from a tagged release with: ```sh tinybus modules load-github \ - https://github.com/tinyhumansai/rust-template/releases/tag/v0.1.5 \ - template-0.1.5-ubuntu-24.04-x86_64.tar.gz \ + https://github.com/tinyhumansai/tinybrowser/releases/tag/v0.1.0 \ + tinybrowser-0.1.0-ubuntu-24.04-x86_64.tar.gz \ ``` diff --git a/README.md b/README.md index 67a4e39..9512907 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,143 @@ -# Rust Template - -A production-ready Rust 2024 TinyBus module template used by TinyHumans AI. It -ships the workspace layout, TinyBus ABI adapter, error handling, testing, -documentation, CI, and multi-platform release workflow that every new -integration in this organization starts from. - -It is a two-crate cargo workspace. `crates/template-bus` is the wire contract — -member names, payload types, and the contract version, with no transport and no -behavior — and `crates/template` is the implementation, built as both an `rlib` -and the `cdylib` TinyBus loads. A host that only makes calls depends on the -contract crate alone and compiles neither the module nor `tinybus` itself. - -## Use This Template - -Choose **Use this template** on GitHub, create a repository, then work through -the checklist at the top of [`AGENTS.md`](AGENTS.md): - -- rename the `crates/template` and `crates/template-bus` directories and the - `name` fields in their manifests, and set the shared `description`, - `repository`, `keywords`, and `categories`; -- update this README and the crate documentation in `crates/template/src/lib.rs`; -- replace the placeholder `greeting` module with the first real feature area, in - both crates: the payload types in the contract, the behavior in the module; -- rename the TinyBus interface, object path, and member constants in - `crates/template-bus/src/names/`, and the matching `provides` / `methods` - declarations in `crates/template/src/tinybus_module/`; -- update the security contact and repository links in the community files; -- replace `ROADMAP.md` with the real plan, or delete it; -- change the license if GPL-3.0-only is not appropriate. - -Search for `template` and `template_bus` to find every remaining -template-specific value. - -## What You Get - -| Area | What is configured | -| --- | --- | -| Layout | A cargo workspace under `crates/`, split into a dependency-light wire contract and the module that implements it; directory modules with `mod.rs` / `types.rs` / `test.rs`, a crate-wide error type, integration tests, and a runnable example | -| Lints | `unsafe_code` forbidden, `missing_docs`, clippy `all` + `pedantic`, no `unwrap`/`expect`/`panic`/`todo` in library code — all declared once in `[workspace.lints]` so every crate, local run, and CI run agree | -| CI | Format, clippy, build, test (default and all features), a run of the bundled example, an assertion that the contract crate stays transport-free, at least 90% line coverage in every source file, rustdoc with `-D warnings`, an MSRV build, and a `cargo-deny` supply-chain check | -| Release | Manual `workflow_dispatch` bump that validates, versions, tags, and creates installable native module packages for every supported platform | -| Community | Issue and pull request templates, Dependabot, contributing, security, support, and code of conduct docs | -| Agents | [`AGENTS.md`](AGENTS.md) as the single source of truth, symlinked as `CLAUDE.md`, plus a `.claude/settings.json` allowlist for the standard commands | -| Vendor | TinyBus host types and module SDK pinned as the `vendor/tinybus` build-time submodule | +# tinybrowser + +A browser for agents, shipped as a loadable [TinyBus](https://github.com/tinyhumansai/tinybus) +module. -## Layout +`tinybrowser` drives a real Chrome over the Chrome DevTools Protocol — launching +it, navigating, snapshotting the accessibility tree, dispatching real input +events, extracting text, taking screenshots — and publishes all of it as a +handful of bus members. A host loads one `cdylib` and gets a browser without a +browser stack in its build. ```text -Cargo.toml # virtual workspace: members, shared metadata, lints -crates/ -├── template-bus/ # the wire contract — what crosses the bus -│ ├── README.md # why the contract is its own crate -│ └── src/ -│ ├── lib.rs # crate docs + the entire public re-export surface -│ ├── names/ # interface, object path, one constant per member -│ ├── greeting/ # payload types, one directory per family -│ │ ├── mod.rs -│ │ ├── types.rs -│ │ └── test.rs -│ └── version/ # contract version and the host bind rule -└── template/ # the module — behavior, adapter, and the cdylib - ├── src/ - │ ├── lib.rs # crate docs + public surface, re-exporting the contract - │ ├── error/ # crate-wide `Error` and `Result` - │ ├── greeting/ # one directory per feature area - │ └── tinybus_module/ # bus interface, setup, and ABI v1 exports - ├── tests/ - │ └── public_api.rs # integration tests against the public API only - └── examples/ - ├── basic.rs # ordinary library API usage - ├── verify_module.rs # local dynamic-module verification - └── verify_github_release.rs # tagged-release download and bus call -vendor/ -└── tinybus/ # pinned TinyBus git submodule -docs/ -├── README.md # documentation index and conventions -├── specs/ # behavior and architecture specifications -├── plans/ # implementation-ordered delivery plans -└── adr/ # immutable architecture decision records +OpenSession -> Navigate -> Snapshot -> Perform -> Snapshot -> ... + | | + ReadPage Screenshot ``` -The split is the point. A payload type describes what a frame carries; the -behavior that answers it is a different obligation. `template` depends on -`template-bus` and re-exports all of it, so `template::GreetRequest` and -`template_bus::GreetRequest` are the *same* type rather than structural twins, -and a host is never forced to choose between linking the whole module and -redefining the vocabulary. See -[`crates/template-bus/README.md`](crates/template-bus/README.md). +## Why it exists -Within each crate, feature areas use directory modules: implementation and -exports live in `mod.rs`, substantial types move to `types.rs`, and unit tests -live in `test.rs`. [`AGENTS.md`](AGENTS.md) holds the complete repository -guidance, and `CLAUDE.md` is a symlink to it so every coding agent reads one -source of truth. +An agent host that wants to look at a web page has bad options. Shelling out to +a browser CLI means a subprocess, a JSON parser around its output, and a binary +to install and version-match. Linking a browser stack in means dragging a +WebSocket client, a TLS stack, an image codec and a protocol surface into a +binary that mostly does something else — and a crash anywhere in it is a crash +in the host. -## Development +This is the third option: the browser lives behind a wire. The host keeps a +proxy and a `serde` derive. + +## What an agent sees + +A snapshot, not HTML: + +```text +- RootWebArea "Example Domain" + - heading "Example Domain" @e1 + - paragraph "This domain is for use in illustrative examples." @e2 + - link "More information..." @e3 +``` + +That is the browser's own accessibility tree — an order of magnitude smaller +than the DOM, with hidden nodes already gone and every control carrying its +role, name, and state. The agent picks `@e3` and passes it straight back as the +target of a click, so the thing it acts on is the thing it saw. A ref from an +older view is refused rather than resolved against whatever now occupies that +position. + +## The surface + +| Member | What it does | +| --- | --- | +| `OpenSession` / `CloseSession` / `ListSessions` | Launch or attach a browser, and give it back | +| `Navigate` | Go somewhere, waiting as far as `commit`, `load`, or `networkIdle` | +| `Snapshot` | The accessibility tree, with refs | +| `Perform` | Click, fill, type, press, select, check, hover, scroll, wait, read, go back | +| `ReadPage` | The page as text, Markdown, or serialized DOM | +| `Evaluate` | JavaScript in, value out | +| `Screenshot` + `ReadOutput` / `ReleaseOutput` | An image, collected in chunks | +| `ContractVersion` | What a host checks before its first real call | + +Every name and payload is published by `tinybrowser-bus`, a two-dependency crate +a host links instead of repeating string literals. + +## Using it + +### From Rust, directly + +```rust,no_run +use tinybrowser::{Action, Browser, NavigateRequest, SnapshotRequest, Target}; + +# async fn example() -> tinybrowser::Result<()> { +let browser = Browser::new(); +let session = browser.open_session(Default::default()).await?; + +browser.navigate(&session.id, &NavigateRequest::new("https://example.com")).await?; +let snapshot = browser.snapshot(&session.id, &SnapshotRequest::interactive()).await?; +println!("{}", snapshot.tree); + +browser + .perform(&session.id, &Action::Click { target: Target::parse("@e1"), new_tab: false }) + .await?; +browser.close_session(&session.id).await?; +# Ok(()) +# } +``` -Clone with submodules, or initialize them before building: +### From a host, over the bus + +`crates/tinybrowser/examples/over_the_bus.rs` is the reference: load the module, +wait for it to claim its name, check the contract version, then call. Run it +against a module you have built: ```sh -git submodule update --init --recursive +cargo build -p tinybrowser --release +cargo run -p tinybrowser --example over_the_bus -- \ + target/release/libtinybrowser.so https://example.com ``` +`docs/openhuman-integration.md` covers wiring it into an OpenHuman host and the +agent-facing tool that sits on top. + +## Requirements + +A Chrome or Chromium on the host, or a DevTools endpoint to attach to. The +module looks in the conventional places; `TINYBROWSER_CHROME` names one +explicitly, and `TINYBROWSER_CHROME_ARGS` adds launch flags every session needs +— `--no-sandbox` on a host where unprivileged user namespaces are restricted, +most often. + +A host that already runs a browser should point the module at it instead, with +`SessionOptions::endpoint`. + +## Development + ```sh +git submodule update --init --recursive + cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings cargo build --all-targets --all-features cargo test --all-features -cargo run -p template --example basic -cargo build -p template --release --lib # produces the installable cdylib ``` -Those four checks are exactly what CI runs. Optional extras: +The end-to-end suite drives a real browser and is opt-in, because a runner +without one would fail it for the wrong reason: ```sh -cargo doc --no-deps --all-features # CI builds this with RUSTDOCFLAGS="-D warnings" -cargo deny check all # supply-chain check; see deny.toml -cargo install cargo-llvm-cov # once, before running the coverage gate -.github/scripts/check-file-coverage.sh 90 coverage.json +TINYBROWSER_LIVE_TESTS=1 cargo test -p tinybrowser --test live_chrome ``` -## Releasing - -Run the **Release** workflow from the Actions tab with a `patch`, `minor`, or -`major` bump. Use `current` only to resume an interrupted release whose version -commit and tag already exist. The workflow revalidates the workspace, versions -and tags it — one `[workspace.package]` version that every member inherits — -builds `crates/template` as a TinyBus `cdylib`, and creates a GitHub release. -Assets follow `template--.` and contain the -native module, its SHA-256 `modules.toml`, license, and -[`MODULE.md`](MODULE.md). Every release also publishes `checksum.toml`, which -TinyBus uses to verify an archive before extraction. The workflow loads the -published Ubuntu archive through TinyBus's GitHub release API and calls its -`Greet` method before declaring the release successful. TinyBus itself is not -shipped by this repository; the pinned submodule is the build-time SDK. The stable native -matrix covers Ubuntu 22.04 and 24.04 on x86_64 and ARM64; Fedora 43 and 44 on -x86_64 and ARM64; rolling Arch Linux on its officially supported x86_64 -architecture; macOS 15 and 26 on Intel and Apple Silicon; Windows Server 2022 -and 2025 on x86_64; and Windows 11 on ARM64. Preview, deprecated, and unofficial -architecture images are not release gates. Do not hand-edit the version in the -root `Cargo.toml`. - -## Documentation - -- [`AGENTS.md`](AGENTS.md) — repository guidelines for humans and agents -- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to propose a change -- [`docs/specs/`](docs/specs/README.md) — behavior and architecture specs -- [`docs/plans/`](docs/plans/README.md) — test-first implementation plans -- [`docs/adr/`](docs/adr/0001-record-architecture-decisions.md) — architecture - decision records -- [`SECURITY.md`](SECURITY.md) — how to report a vulnerability +`AGENTS.md` is the full working agreement. `CLAUDE.md` is a symlink to it. + +## Credit + +The design owes a great deal to Vercel's +[`agent-browser`](https://github.com/vercel-labs/agent-browser): the +accessibility tree as the thing an agent reads, `@ref` addressing scoped to a +snapshot, and hit-testing a click point before dispatching at it. See +`THIRD-PARTY.md`. ## License -GPL-3.0-only. See [LICENSE](LICENSE). +GPL-3.0-only. See `LICENSE`. diff --git a/ROADMAP.md b/ROADMAP.md index 1134024..d8a7bbd 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,26 +1,48 @@ # Roadmap -Replace this file with the real plan for the crate generated from this -template, or delete it if the project does not need a public roadmap. - -Keep it short and honest: what exists, what is next, and what is deliberately -out of scope. A roadmap that lists everything is a roadmap nobody trusts. +What exists, what is next, and what is deliberately out of scope. ## Shipped -- module layout, crate-wide error type, and the public re-export surface -- lint configuration in `[lints]`, enforced identically locally and in CI -- CI: format, clippy, build, test, per-file coverage, rustdoc, MSRV, and - supply-chain checks -- a manual release workflow that versions, tags, publishes to crates.io, and - creates a GitHub release with crate and TinyBus runtime/module assets +- a CDP engine: launching or attaching, one multiplexed socket, flat page + sessions, and teardown that does not leave a browser behind +- accessibility snapshots rendered as indented text with `@ref` addressing, + scoped to the generation that minted them +- interactions through real input events — click with hit-testing, fill, type, + press, select, check, hover, scroll, wait — addressed by ref, CSS selector, or + semantic locator +- extraction as text, Markdown, or serialized DOM, and JavaScript evaluation +- screenshots, held and collected in chunks with a digest to verify them +- an error taxonomy where every failure carries a stable wire name, and the + contract says which of them an agent can act on +- the `TinyBus` module: twelve members, a version handshake, and a `cdylib` that + CI loads through the real dynamic loader +- an end-to-end suite against a real Chrome, serving its own fixtures on + loopback ## Next -- the first real feature area, replacing the placeholder `greeting` module -- module-level `README.md` and `docs/spec/` entries as modules grow +- multiple tabs per session: `Tabs`, `SelectTab`, and adopting the page a click + with `new_tab` opened, which currently opens a tab the session does not drive +- cookie and storage state a host can save and restore, so a session can resume + an authenticated one without the module holding credentials +- network interception: refusing or recording requests, which is also the only + way to make the origin policy a boundary rather than a guard rail +- `Snapshot` diffing, so an agent acting in a loop reads what changed rather than + the whole tree each time ## Out Of Scope -- anything that cannot be tested deterministically -- convenience wrappers that hide the crate's error taxonomy from callers +- an agent, a model, or tool schemas. This module drives a browser and describes + what it sees; deciding what to click is the host's job, and a module shipping + its own prompt would be one more thing to keep in step with a model it cannot + see. +- a sandbox. `allowed_origins` is a guard rail an in-page navigation can defeat, + and it says so. A host that needs a boundary puts the browser in a network + namespace. +- persistence. Sessions live in memory and end with the process. +- downloading a browser. Finding one is a fixed list and an environment + variable; a module that fetches and executes a binary is a different kind of + thing. +- anything that cannot be tested deterministically, or only against a live page + on the public internet. diff --git a/THIRD-PARTY.md b/THIRD-PARTY.md new file mode 100644 index 0000000..9ea03e5 --- /dev/null +++ b/THIRD-PARTY.md @@ -0,0 +1,45 @@ +# Third-Party Notices + +## agent-browser + +Copyright Vercel, Inc. Licensed under the Apache License, Version 2.0. + + + +`crates/tinybrowser` is an independent implementation, but its design is taken +from `agent-browser` and it would not look the way it does without it. The +specific debts: + +- **The accessibility tree is what an agent reads.** Not the DOM, not a + screenshot — the browser's own computed answer to "what is here and what does + it do", rendered as indented text. This is the central idea, and it is theirs. +- **`@ref` addressing scoped to a snapshot.** Every actionable node in a + snapshot carries a short ref an agent passes straight back, so the thing it + acts on is the thing it saw, and a ref from an older view is refused rather + than silently resolved against whatever now occupies that position. +- **Hit-testing before dispatching a click.** Measuring the click point, + checking with `elementFromPoint` that the target is what sits there, and + failing with the name of the covering element rather than delivering the click + to a consent banner and reporting success. +- **Flat CDP session attachment**, with one socket multiplexing browser-level + and page-level commands. +- **The interaction vocabulary** — click, fill, type, press, select, hover, + scroll, wait, and semantic locators by role, text, label, placeholder, and + test id — follows theirs closely, deliberately, so that a host can move + between the two without relearning what the verbs mean. + +No code was copied. The protocol conversation, the error taxonomy, the session +and held-output model, the origin policy, and the whole `TinyBus` surface are +this repository's own, and the licences differ — `agent-browser` is Apache-2.0 +and this project is GPL-3.0-only, a direction that is compatible one way and not +the other. + +The Apache-2.0 licence requires that its notice travel with derivative work. +This file is that notice, and it is here whether or not the requirement strictly +attaches, because the credit is owed either way. + +## TinyBus + +`vendor/tinybus` is a git submodule of +and carries its own licence and copyright. It is pinned by gitlink and never +modified from this repository. diff --git a/crates/template-bus/README.md b/crates/template-bus/README.md deleted file mode 100644 index 7f8e99c..0000000 --- a/crates/template-bus/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# template-bus - -Every type that crosses the template module's `TinyBus` boundary, and the names -of the members that carry them. - -The template ships as a loadable module so a host does not compile the -implementation: `crates/template` is built as a `cdylib` and exports one object. -A host can load that binary but cannot `use` anything out of it, so the payload -vocabulary has to be published as an ordinary library. This is it. - -| module | what it holds | -| ---------- | ------------------------------------------------------------ | -| `names` | interface name, object path, one constant per member | -| `greeting` | the value vocabulary: the `Greet` request and response | -| `version` | `CONTRACT_VERSION` and the bind rule a host applies to it | - -Two dependencies, both pure Rust: `serde` and `serde_json`. - -## This crate sits underneath `template` - -`template` **depends on this crate and re-exports all of it**. That direction -matters, and it is the opposite of the obvious one. - -A *host* needs the payload types and needs nothing else: it loads the module and -makes calls, so it names `GreetRequest` and `GreetResponse` but implements no -behavior and links no transport. Making it depend on the whole module crate — and -through it on `tinybus`, `tokio`, and the module SDK — to spell a payload type -would be the wrong shape. - -The alternative, a parallel set of payload types for hosts, is worse: a -`GreetRequest` defined twice is two distinct types, with a conversion at every -call site that nothing checks. One definition, here, at the bottom. - -Because the re-export is by module as well as by item, `template::GreetRequest`, -`template::names::OBJECT_PATH`, and `template_bus::greeting::GreetRequest` all -resolve to the same items, not twins. - -So: a module author depends on `template` and gets behavior and vocabulary. A -host depends on `template-bus` and gets vocabulary alone. - -## What is deliberately absent - -**No behavior.** `greet` lives in `crates/template`. A payload type describes -what a frame carries, not what the module does with it. The split is readable -off the path: a name here is data, a name there is an obligation. - -**No transport.** This crate does not depend on `tinybus` and holds no -connection, client, or codec. A host already owns its connection — its reconnect -policy, its timeouts, its tracing — and the useful part is the vocabulary. - -That is also structural, not just preference: `tinybus` is vendored as a -submodule whose manifest inherits fields from its own nested -`[workspace.package]`. Keeping the contract crate transport-free is what keeps -it down to two dependencies and what lets anything in the workspace — or outside -it — depend on it freely. CI asserts the dependency tree stays that way. - -## Making a call - -Arguments travel as a positional JSON array — `#[tinybus::interface]` decodes -them into a tuple — and the member name comes from `names`: - -```rust,ignore -use template_bus::{names, GreetRequest, GreetResponse}; - -let proxy = connection.proxy(names::INTERFACE, names::OBJECT_PATH, names::INTERFACE)?; -let reply: GreetResponse = proxy - .call(names::methods::GREET, (GreetRequest::new("Ferris"),)) - .await?; -assert_eq!(reply.greeting, "Hello, Ferris!"); -``` - -Nothing above is a string literal at a call site. Renaming the interface, the -path, or a member is therefore a compile error in every consumer rather than an -`UnknownMethod` discovered at runtime. - -## Staying in step with the module - -`names::METHODS` lists every member in dispatch order. `crates/template` asserts -its served members against that list, so a method added to the interface without -an entry here fails that crate's tests rather than surfacing in a host. - -## Versioning - -`CONTRACT_VERSION` describes *this vocabulary*, not the package. Bump its major -component when a payload's wire form changes incompatibly or a member is removed -or renamed, and its minor component when a member or an optional field is added. -It is deliberately independent of the package version the release workflow owns, -which tracks the shipped artifact. - -The payload tests pin the serde representation, because that representation is -the wire form: a host and a module that disagree about a field name fail at -runtime with a decode error, so the shape is asserted rather than assumed. - -## Generating a project from the template - -Rename the interface, the object path, and the member constants in `names` -together, replace `greeting` with the first real payload family, and reset -`CONTRACT_VERSION` to `(1, 0)` for the new contract. Keep the crate -dependency-light: the moment it links a transport or a runtime, the reason it -exists is gone. diff --git a/crates/template-bus/src/greeting/mod.rs b/crates/template-bus/src/greeting/mod.rs deleted file mode 100644 index f810aab..0000000 --- a/crates/template-bus/src/greeting/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! The payloads the `Greet` member exchanges. -//! -//! A module root like this one documents the module, wires its pieces together, -//! and exposes the smallest useful API. The type definitions live in the -//! sibling `types.rs`, and the unit tests in `test.rs`, wired in at the bottom -//! of this file. -//! -//! Replace this module with the first real payload family the module carries. -//! Payload types are `serde`-derived, `#[non_exhaustive]`, and hold owned data: -//! they are decoded from a frame, so they can borrow nothing from the caller. - -mod types; - -pub use types::{GreetRequest, GreetResponse}; - -#[cfg(test)] -mod test; diff --git a/crates/template-bus/src/greeting/test.rs b/crates/template-bus/src/greeting/test.rs deleted file mode 100644 index 1a30000..0000000 --- a/crates/template-bus/src/greeting/test.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Unit tests for the `Greet` payloads. -//! -//! These pin the serde representation. It is the wire form: a host and a module -//! that disagree about a field name fail at runtime with a decode error, so the -//! shape is asserted here rather than assumed. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use super::{GreetRequest, GreetResponse}; - -#[test] -fn a_request_serializes_to_its_wire_form() { - let encoded = serde_json::to_value(GreetRequest::new("Ferris")).unwrap(); - assert_eq!(encoded, serde_json::json!({ "name": "Ferris" })); -} - -#[test] -fn a_response_serializes_to_its_wire_form() { - let encoded = serde_json::to_value(GreetResponse::new("Hello, Ferris!")).unwrap(); - assert_eq!(encoded, serde_json::json!({ "greeting": "Hello, Ferris!" })); -} - -#[test] -fn a_request_round_trips_through_json() { - let request = GreetRequest::new(" Ferris "); - let encoded = serde_json::to_string(&request).unwrap(); - assert_eq!( - serde_json::from_str::(&encoded).unwrap(), - request - ); -} - -#[test] -fn a_response_round_trips_through_json() { - let response = GreetResponse::new("Hello, Ferris!"); - let encoded = serde_json::to_string(&response).unwrap(); - assert_eq!( - serde_json::from_str::(&encoded).unwrap(), - response - ); -} - -#[test] -fn a_request_missing_its_name_is_rejected() { - let decoded = serde_json::from_value::(serde_json::json!({})); - assert!(decoded.is_err()); -} - -#[test] -fn a_response_missing_its_greeting_is_rejected() { - let decoded = serde_json::from_value::(serde_json::json!({})); - assert!(decoded.is_err()); -} - -#[test] -fn constructors_accept_both_borrowed_and_owned_names() { - assert_eq!( - GreetRequest::new(String::from("Ferris")), - GreetRequest::new("Ferris") - ); - assert_eq!( - GreetResponse::new(String::from("Hi")), - GreetResponse::new("Hi") - ); -} diff --git a/crates/template-bus/src/greeting/types.rs b/crates/template-bus/src/greeting/types.rs deleted file mode 100644 index d70b376..0000000 --- a/crates/template-bus/src/greeting/types.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Request and response types for the `Greet` member. - -use serde::{Deserialize, Serialize}; - -/// The argument to [`crate::names::methods::GREET`]. -/// -/// The module trims surrounding whitespace from [`GreetRequest::name`] and -/// rejects a name that is empty once trimmed. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[non_exhaustive] -pub struct GreetRequest { - /// The name to greet. - pub name: String, -} - -impl GreetRequest { - /// Builds a request greeting `name`. - /// - /// # Examples - /// - /// ``` - /// # use template_bus::GreetRequest; - /// assert_eq!(GreetRequest::new("Ferris").name, "Ferris"); - /// ``` - #[must_use] - pub fn new(name: impl Into) -> Self { - Self { name: name.into() } - } -} - -/// The reply from [`crate::names::methods::GREET`]. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[non_exhaustive] -pub struct GreetResponse { - /// The rendered greeting. - pub greeting: String, -} - -impl GreetResponse { - /// Builds a reply carrying `greeting`. - /// - /// # Examples - /// - /// ``` - /// # use template_bus::GreetResponse; - /// assert_eq!(GreetResponse::new("Hello, Ferris!").greeting, "Hello, Ferris!"); - /// ``` - #[must_use] - pub fn new(greeting: impl Into) -> Self { - Self { - greeting: greeting.into(), - } - } -} diff --git a/crates/template-bus/src/lib.rs b/crates/template-bus/src/lib.rs deleted file mode 100644 index a1857d1..0000000 --- a/crates/template-bus/src/lib.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Every type that crosses the template module's `TinyBus` boundary, and the -//! names of the members that carry them. -//! -//! This crate ships as a loadable `TinyBus` module: `crates/template` is built -//! as a `cdylib` and exports one object. A host that loads that binary can call -//! into it but cannot `use` anything out of it, so the payload vocabulary has -//! to be published as an ordinary library. This is that library. -//! -//! # What is here -//! -//! - [`names`] — the interface name, the object path, and one constant per -//! member, plus [`names::METHODS`] listing them in dispatch order. -//! - [`greeting`] — the value vocabulary: the request and response payloads the -//! `Greet` member exchanges. -//! - [`version`] — [`CONTRACT_VERSION`] and the [`is_compatible`] bind rule. -//! -//! # What is deliberately not here -//! -//! **No behavior.** The `greet` implementation lives in `crates/template`, -//! which depends on this crate and re-exports it. A payload type describes what -//! a frame carries, not what the module does with it. -//! -//! **No transport.** This crate does not depend on `tinybus` and holds no -//! connection, client, or codec. A host already owns its connection — its -//! reconnect policy, its timeouts, its tracing — and the useful part is the -//! vocabulary, not another wrapper around it. -//! -//! That is also a structural necessity, not only a preference: `tinybus` is -//! vendored as a submodule whose manifest inherits fields from its own nested -//! `[workspace.package]`. A crate that every workspace member can depend on has -//! to stay transport-free, and staying transport-free is what keeps this crate -//! down to two pure-Rust dependencies. -//! -//! # This crate sits underneath the implementation, not beside it -//! -//! `template` **depends on this crate and re-exports all of it**, so -//! `template::GreetRequest` and `template_bus::greeting::GreetRequest` are the -//! *same type*, not structural twins. Defining a parallel set of payload types -//! for hosts would mean a conversion at every call site that nothing checks. -//! One definition, here, at the bottom. -//! -//! So: a module author depends on `template` and gets behavior and vocabulary. -//! A host depends on `template-bus` and gets vocabulary alone. -//! -//! # Staying in step with the module -//! -//! [`names::METHODS`] lists every member. `crates/template` asserts its served -//! members against that list, in order, so a method added to the interface -//! without an entry here fails that crate's tests rather than surfacing as an -//! unknown method in a host at runtime. -//! -//! # Example -//! -//! ``` -//! use template_bus::{names, GreetRequest, GreetResponse}; -//! -//! let body = serde_json::to_value([GreetRequest::new("Ferris")])?; -//! assert_eq!(names::methods::GREET, "Greet"); -//! assert_eq!(names::OBJECT_PATH, "/ai/tinyhumans/template/Greeting"); -//! -//! let reply: GreetResponse = serde_json::from_value( -//! serde_json::json!({ "greeting": "Hello, Ferris!" }), -//! )?; -//! assert_eq!(reply.greeting, "Hello, Ferris!"); -//! # Ok::<(), serde_json::Error>(()) -//! ``` - -pub mod greeting; -pub mod names; -pub mod version; - -pub use greeting::{GreetRequest, GreetResponse}; -pub use names::{INTERFACE, METHODS, OBJECT_PATH}; -pub use version::{CONTRACT_VERSION, is_compatible}; diff --git a/crates/template-bus/src/names/mod.rs b/crates/template-bus/src/names/mod.rs deleted file mode 100644 index 4da1547..0000000 --- a/crates/template-bus/src/names/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! The bus identity of the template module: interface name, object path, and -//! one constant per member. -//! -//! Nothing here is a string literal at a call site. A host names a member -//! through [`methods`] and the object through [`OBJECT_PATH`], so a rename is a -//! compile error in every consumer rather than a runtime "unknown method". -//! -//! When generating a project from this template, rename all three together — -//! the interface, the path, and the member constants — and keep -//! [`METHODS`] in the same order as the interface's dispatch table. - -/// The well-known interface name the module claims on the bus. -pub const INTERFACE: &str = "ai.tinyhumans.template.Greeting"; - -/// The object path the module serves its interface at. -pub const OBJECT_PATH: &str = "/ai/tinyhumans/template/Greeting"; - -/// One constant per member of [`INTERFACE`]. -pub mod methods { - /// Builds a greeting for a name. - /// - /// Takes a [`crate::GreetRequest`] and returns a [`crate::GreetResponse`]. - pub const GREET: &str = "Greet"; -} - -/// Every member of [`INTERFACE`], in the order the interface dispatches them. -/// -/// `crates/template` asserts its declared manifest methods against this list, -/// so the two cannot drift. -pub const METHODS: &[&str] = &[methods::GREET]; - -#[cfg(test)] -mod test; diff --git a/crates/template-bus/src/names/test.rs b/crates/template-bus/src/names/test.rs deleted file mode 100644 index bf7bea2..0000000 --- a/crates/template-bus/src/names/test.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Unit tests for the bus name table. - -use super::{INTERFACE, METHODS, OBJECT_PATH, methods}; - -#[test] -fn the_object_path_is_the_interface_in_path_form() { - let expected = format!("/{}", INTERFACE.replace('.', "/")); - assert_eq!(OBJECT_PATH, expected); -} - -#[test] -fn every_member_is_listed_exactly_once() { - let mut sorted = METHODS.to_vec(); - sorted.sort_unstable(); - let mut deduplicated = sorted.clone(); - deduplicated.dedup(); - assert_eq!(sorted, deduplicated); -} - -#[test] -fn the_method_table_holds_the_declared_members() { - assert_eq!(METHODS, [methods::GREET]); -} - -#[test] -fn no_member_name_is_empty() { - assert!(METHODS.iter().all(|method| !method.is_empty())); -} diff --git a/crates/template/Cargo.toml b/crates/template/Cargo.toml deleted file mode 100644 index e1bcdf4..0000000 --- a/crates/template/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "template" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -description = "A production-ready template for installable TinyBus modules." -documentation = "https://docs.rs/template" -readme = "../../README.md" -keywords = ["tinybus", "module", "plugin", "template"] -categories = ["development-tools"] -publish = false - -[lib] -# Keep the ordinary Rust library for tests and downstream reuse while also -# producing the native module artifact that TinyBus loads at runtime. -crate-type = ["rlib", "cdylib"] - -[dependencies] -# The wire contract: member names, payload types, and the contract version. -# Re-exported wholesale from `src/lib.rs` so a consumer takes one dependency -# rather than two, and so `template::GreetRequest` and -# `template_bus::GreetRequest` are the same type. -template-bus = { workspace = true } -tinybus = { workspace = true } -tinybus-module = { workspace = true } -thiserror = { workspace = true } - -[dev-dependencies] -tokio = { workspace = true } -# The GitHub release verifier passes an explicit empty module configuration. -serde_json = { workspace = true } - -[features] -default = [] - -[lints] -workspace = true diff --git a/crates/template/examples/basic.rs b/crates/template/examples/basic.rs deleted file mode 100644 index 99233ec..0000000 --- a/crates/template/examples/basic.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Minimal end-to-end usage of the crate. -//! -//! Examples are compiled and linted in CI, so they cannot drift from the API. -//! Run it with: -//! -//! ```sh -//! cargo run --example basic -//! ``` - -use template::{Result, greet}; - -fn main() -> Result<()> { - println!("{}", greet("Rust")?); - - // Failure modes are part of the public contract; show them too. - match greet(" ") { - Ok(greeting) => println!("{greeting}"), - Err(error) => println!("expected failure: {error}"), - } - - Ok(()) -} diff --git a/crates/template/src/error/mod.rs b/crates/template/src/error/mod.rs deleted file mode 100644 index b8ddbe0..0000000 --- a/crates/template/src/error/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Crate-wide error and result types. -//! -//! Every fallible public function in this crate returns [`Result`], and every -//! failure mode is a distinct [`Error`] variant. Add a variant rather than -//! encoding new context into an existing message: callers match on variants, -//! and message text is not a stable API. -//! -//! Variants carry the data a caller needs to react, keep their `#[error]` -//! message lowercase and free of trailing punctuation, and are documented so -//! the rendered rustdoc explains when each one occurs. - -/// Errors returned by this crate. -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -#[non_exhaustive] -pub enum Error { - /// A required name was empty or contained only whitespace. - #[error("name must not be empty")] - EmptyName, -} - -/// The crate's standard result type. -/// -/// Use this alias in public signatures instead of spelling out -/// `std::result::Result`. -pub type Result = std::result::Result; - -#[cfg(test)] -mod test; diff --git a/crates/template/src/error/test.rs b/crates/template/src/error/test.rs deleted file mode 100644 index 4c5d609..0000000 --- a/crates/template/src/error/test.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Unit tests for the crate-wide error type. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use super::*; - -#[test] -fn renders_a_human_readable_message() { - assert_eq!(Error::EmptyName.to_string(), "name must not be empty"); -} - -#[test] -fn is_a_standard_error() { - fn assert_error(_: &E) {} - - assert_error(&Error::EmptyName); -} diff --git a/crates/template/src/greeting/mod.rs b/crates/template/src/greeting/mod.rs deleted file mode 100644 index 862fa21..0000000 --- a/crates/template/src/greeting/mod.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! Greeting behavior used to demonstrate the template's module layout. -//! -//! A module root like this one documents the module, wires its pieces -//! together, and exposes the smallest useful API. Substantial type definitions -//! belong in a sibling `types.rs`, and unit tests belong in `test.rs`, wired in -//! at the bottom of this file. -//! -//! Replace this module with the crate's first real feature area. - -use crate::{Error, Result}; - -/// Returns a friendly greeting for `name`. -/// -/// Surrounding whitespace is trimmed before the greeting is built. -/// -/// # Examples -/// -/// ``` -/// # use template::greet; -/// assert_eq!(greet(" Ferris ")?, "Hello, Ferris!"); -/// # Ok::<(), template::Error>(()) -/// ``` -/// -/// # Errors -/// -/// Returns [`Error::EmptyName`] when `name` is empty or contains only -/// whitespace. -pub fn greet(name: &str) -> Result { - let name = name.trim(); - if name.is_empty() { - return Err(Error::EmptyName); - } - - Ok(format!("Hello, {name}!")) -} - -#[cfg(test)] -mod test; diff --git a/crates/template/src/greeting/test.rs b/crates/template/src/greeting/test.rs deleted file mode 100644 index de04ef4..0000000 --- a/crates/template/src/greeting/test.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! Unit tests for the greeting module. -//! -//! Unit tests live next to the code they cover and may reach into private -//! items. Tests of the public contract belong in `tests/` instead. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use super::*; - -#[test] -fn greets_a_named_person() { - assert_eq!(greet("Ferris").unwrap(), "Hello, Ferris!"); -} - -#[test] -fn trims_the_name() { - assert_eq!(greet(" Ferris ").unwrap(), "Hello, Ferris!"); -} - -#[test] -fn rejects_an_empty_name() { - assert_eq!(greet("").unwrap_err(), Error::EmptyName); -} - -#[test] -fn rejects_a_whitespace_only_name() { - assert_eq!(greet(" \t\n ").unwrap_err(), Error::EmptyName); -} diff --git a/crates/template/src/lib.rs b/crates/template/src/lib.rs deleted file mode 100644 index 566fa7e..0000000 --- a/crates/template/src/lib.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! A production-ready starting point for an installable `TinyBus` module. -//! -//! This crate is a template. It ships the layout, lint configuration, error -//! handling, testing, and documentation conventions described in `AGENTS.md`. -//! The compiled `cdylib` exports `TinyBus` module ABI v1 and serves the example -//! [`greet`] behavior over the bus. -//! -//! # Layout -//! -//! This is the implementation half of a two-crate workspace: -//! -//! - [`template_bus`] — the wire contract. Member names, payload types, and the -//! contract version, with no transport and no behavior. A host that only -//! makes calls depends on that crate alone. -//! - `template` — this crate. The behavior, the crate-wide error type, and the -//! `TinyBus` adapter that serves them, built as both an `rlib` and the -//! `cdylib` the loader consumes. -//! -//! Within this crate: -//! -//! - `src/error/` holds the crate-wide [`Error`] enum and the [`Result`] alias -//! returned by every fallible public function. -//! - Each feature area lives in its own module directory with a `mod.rs` -//! module root, an optional `types.rs`, and a `test.rs` holding its unit -//! tests. -//! - Every public item is re-exported from here — including all of -//! [`template_bus`] — so downstream users have a single predictable surface -//! and `template::GreetRequest` is the *same type* as -//! `template_bus::GreetRequest`, not a structural twin. -//! - `tinybus_module` adapts the public behavior to `TinyBus` and exports the -//! module descriptor, embedded manifest, and initialization entrypoint. -//! -//! # Example -//! -//! ``` -//! use template::{greet, Error, GreetRequest}; -//! -//! assert_eq!(greet("Ferris")?, "Hello, Ferris!"); -//! assert_eq!(greet(" ").unwrap_err(), Error::EmptyName); -//! assert_eq!(GreetRequest::new("Ferris").name, "Ferris"); -//! # Ok::<(), template::Error>(()) -//! ``` -//! -//! Replace the `greeting` module with the first real feature area, keep the -//! conventions, and update this documentation to describe the new crate. - -mod error; -mod greeting; -mod tinybus_module; - -pub use error::{Error, Result}; -pub use greeting::greet; - -// The wire contract, re-exported by module rather than by item so every path -// through this crate resolves to the same definitions the contract crate -// publishes. A host may depend on `template-bus` directly and get exactly these -// types; nothing here redefines them. -pub use template_bus; -pub use template_bus::{ - CONTRACT_VERSION, GreetRequest, GreetResponse, INTERFACE, METHODS, OBJECT_PATH, is_compatible, - names, version, -}; diff --git a/crates/template/src/tinybus_module/README.md b/crates/template/src/tinybus_module/README.md deleted file mode 100644 index 2c05772..0000000 --- a/crates/template/src/tinybus_module/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# TinyBus Adapter - -This module is the boundary between ordinary feature code and TinyBus module -ABI v1. `GreetingService` converts the crate's public `greet` function into the typed -`Greet` bus method, while `setup` registers its object and claims the well-known -interface name. Neither the name, the object path, nor the payload types are -spelled here: they come from `template-bus`, so a rename is a compile error in -every consumer instead of an `UnknownMethod` at runtime. - -`tinybus_module::module_export!` emits the descriptor, embedded manifest, and -initialization symbols consumed by the dynamic loader. The manifest method list -must stay aligned with the interface macro's dispatch table and with -`template_bus::names::METHODS`; the unit tests check both relationships. -Integration tests use TinyBus's in-memory transport, and -`crates/template/examples/verify_module.rs` loads a compiled `cdylib` through -the real dynamic loader before a release archive is accepted. - -Generated projects should replace the example interface, object path, and method -declarations together — here and in `crates/template-bus/src/names/`. They must not retain Rust-owned data across the -ABI boundary or bypass the SDK exports with an ad hoc FFI surface. diff --git a/crates/template/src/tinybus_module/mod.rs b/crates/template/src/tinybus_module/mod.rs deleted file mode 100644 index 1c9c2f0..0000000 --- a/crates/template/src/tinybus_module/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! `TinyBus` module entrypoint and bus-facing interface. -//! -//! This adapter keeps the feature implementation independent from `TinyBus` -//! while exposing it as an installable, dynamically loaded integration. The -//! names and payload types it serves come from [`template_bus`], so a host -//! spells them from the contract crate instead of repeating string literals. - -use template_bus::{GreetRequest, GreetResponse, names}; -use tinybus::{Connection, Result as TinyBusResult}; - -struct GreetingService; - -#[tinybus::interface(name = "ai.tinyhumans.template.Greeting")] -impl GreetingService { - async fn greet(&self, request: GreetRequest) -> TinyBusResult { - std::future::ready(crate::greet(&request.name)) - .await - .map(GreetResponse::new) - .map_err(|error| tinybus::Error::failed(error.to_string())) - } -} - -async fn setup(connection: Connection) -> TinyBusResult<()> { - connection - .serve_at(names::OBJECT_PATH.try_into()?, GreetingService) - .await?; - connection.request_name(names::INTERFACE).await?; - Ok(()) -} - -tinybus_module::module_export! { - setup = setup, - worker_threads = 1, - provides = ["ai.tinyhumans.template.Greeting"], - methods = ["Greet"], - signals = [], - requires = [], - optional = [], - lazy = false, -} - -#[cfg(test)] -mod test; diff --git a/crates/template/src/tinybus_module/test.rs b/crates/template/src/tinybus_module/test.rs deleted file mode 100644 index d5fe71a..0000000 --- a/crates/template/src/tinybus_module/test.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Tests for the `TinyBus` module adapter and its declared surface. - -use super::{GreetingService, setup}; -use template_bus::{GreetRequest, GreetResponse, names}; -use tinybus::broker::Broker; -use tinybus::transport::memory::MemoryBus; -use tinybus::{Connection, Interface}; - -#[test] -fn declared_methods_match_the_dispatch_table() { - let methods = GreetingService - .members() - .into_iter() - .map(|member| member.to_string()) - .collect::>(); - - assert_eq!(methods, names::METHODS.to_vec()); -} - -#[test] -fn the_served_interface_name_matches_the_contract() { - assert_eq!(GreetingService.name().to_string(), names::INTERFACE); -} - -#[tokio::test] -async fn module_serves_greetings_over_a_real_bus() -> tinybus::Result<()> { - let bus = MemoryBus::new(); - Broker::new().spawn(bus.clone()); - - let service = Connection::connect(bus.connect().await?).await?; - setup(service.clone()).await?; - - let client = Connection::connect(bus.connect().await?).await?; - let proxy = client.proxy(names::INTERFACE, names::OBJECT_PATH, names::INTERFACE)?; - let reply: GreetResponse = proxy - .call(names::methods::GREET, (GreetRequest::new("Ferris"),)) - .await?; - - assert_eq!(reply, GreetResponse::new("Hello, Ferris!")); - Ok(()) -} - -#[tokio::test] -async fn module_rejects_an_empty_name_over_the_bus() -> tinybus::Result<()> { - let bus = MemoryBus::new(); - Broker::new().spawn(bus.clone()); - - let service = Connection::connect(bus.connect().await?).await?; - setup(service.clone()).await?; - - let client = Connection::connect(bus.connect().await?).await?; - let proxy = client.proxy(names::INTERFACE, names::OBJECT_PATH, names::INTERFACE)?; - let result = proxy - .call::(names::methods::GREET, (GreetRequest::new(" "),)) - .await; - - let Err(error) = result else { - return Err(tinybus::Error::failed( - "whitespace-only names unexpectedly succeeded", - )); - }; - assert!(error.to_string().contains("name must not be empty")); - Ok(()) -} diff --git a/crates/template/tests/public_api.rs b/crates/template/tests/public_api.rs deleted file mode 100644 index 256b71c..0000000 --- a/crates/template/tests/public_api.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! Integration tests for the public crate surface. -//! -//! These tests link against the crate as a downstream consumer would: they can -//! only use what `src/lib.rs` re-exports. Treat them as the regression suite -//! for the crate's public contract — if a change breaks a test here, it is a -//! breaking change for users. - -#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - -use template::{Error, greet}; - -#[test] -fn greeting_is_available_to_consumers() { - assert_eq!(greet("Rust").unwrap(), "Hello, Rust!"); -} - -#[test] -fn errors_are_available_to_consumers() { - assert_eq!(greet("").unwrap_err(), Error::EmptyName); -} diff --git a/crates/template-bus/Cargo.toml b/crates/tinybrowser-bus/Cargo.toml similarity index 65% rename from crates/template-bus/Cargo.toml rename to crates/tinybrowser-bus/Cargo.toml index a30dd85..95a0050 100644 --- a/crates/template-bus/Cargo.toml +++ b/crates/tinybrowser-bus/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "template-bus" +name = "tinybrowser-bus" version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true repository.workspace = true -description = "The TinyBus wire contract for the template module: member names, payload types, and the contract version." -documentation = "https://docs.rs/template-bus" +description = "The TinyBus wire contract for the tinybrowser module: member names, payload types, and the contract version." +documentation = "https://docs.rs/tinybrowser-bus" readme = "README.md" -keywords = ["tinybus", "module", "contract", "template"] -categories = ["development-tools"] +keywords = ["tinybus", "module", "contract", "browser", "automation"] +categories = ["development-tools", "web-programming"] publish = false # Deliberately dependency-light: this is the crate a host links to talk to the diff --git a/crates/tinybrowser-bus/README.md b/crates/tinybrowser-bus/README.md new file mode 100644 index 0000000..603ea8f --- /dev/null +++ b/crates/tinybrowser-bus/README.md @@ -0,0 +1,59 @@ +# tinybrowser-bus + +Every type that crosses the tinybrowser module's `TinyBus` boundary, and the +names of the members that carry them. + +tinybrowser ships as a loadable module so a host does not compile a browser: +`crates/tinybrowser` is built as a `cdylib` and exports one object. A host can +load that binary but cannot `use` anything out of it, so the payload vocabulary +has to be published as an ordinary library. This is it. + +| module | what it holds | +| ---------- | -------------------------------------------------------------- | +| `names` | interface name, object path, one constant per member | +| `session` | opening, listing, and closing the browser a host drives | +| `page` | navigating, extracting a page as text, evaluating JavaScript | +| `snapshot` | the accessibility tree, and the refs that address it | +| `action` | every interaction, and the three ways to name an element | +| `output` | screenshots, and the handle protocol that carries them | +| `errors` | the failure names, and which of them an agent can act on | +| `version` | `CONTRACT_VERSION` and the bind rule a host applies to it | + +Two dependencies, both pure Rust: `serde` and `serde_json`. + +## This crate sits underneath `tinybrowser` + +`tinybrowser` depends on this crate and re-exports all of it, so +`tinybrowser::Action` and `tinybrowser_bus::action::Action` are the *same type*, +not structural twins. Defining a parallel set of payload types for hosts would +mean a conversion at every call site that nothing checks. One definition, here, +at the bottom. + +So: a module author depends on `tinybrowser` and gets behavior and vocabulary. A +host depends on `tinybrowser-bus` and gets vocabulary alone — which matters, +because that host is usually a binary that deliberately does not want a browser +stack in its build. That is the entire reason this split exists. + +## No transport, on purpose + +This crate holds no connection, client, or codec, and does not depend on +`tinybus`. A host already owns its connection — its reconnect policy, its +timeouts, its tracing — and the useful part is the vocabulary, not another +wrapper around it. + +It is also a structural necessity: `tinybus` is vendored as a submodule whose +manifest inherits from its own nested `[workspace.package]`. A crate every +member can depend on has to stay transport-free, and CI asserts it does. + +## Nothing here is `#[non_exhaustive]` + +Both sides construct these types — a host builds the requests, the module builds +the replies — and the module is a different crate from this one. Non-exhaustive +types would leave the implementation unable to build its own replies, and would +turn `..Default::default()` into a compile error for every caller. + +The evolution mechanism is `CONTRACT_VERSION` and the `is_compatible` bind rule +instead, which is the one that works across a dynamically loaded boundary. Every +request type carries `#[serde(default)]`, so an added field is additive; a host +deserializing a reply ignores what it does not know. Neither is something the +Rust attribute could have enforced through a `cdylib` anyway. diff --git a/crates/tinybrowser-bus/src/action/mod.rs b/crates/tinybrowser-bus/src/action/mod.rs new file mode 100644 index 0000000..6e61a18 --- /dev/null +++ b/crates/tinybrowser-bus/src/action/mod.rs @@ -0,0 +1,27 @@ +//! Interactions: the one member a host calls for everything that changes the +//! page, and the vocabulary for saying which element it means. +//! +//! # Why one member and not fifteen +//! +//! Click, fill, hover, and press differ in what they do to an element, not in +//! how they are addressed, deadlined, or reported. Splitting them into separate +//! bus members would duplicate the target-resolution rules fifteen times over +//! and force a host tool that dispatches on a model-chosen verb to carry its own +//! fifteen-arm match anyway. [`Action`] *is* that match, written once, in the +//! crate both sides share. +//! +//! # Refs, selectors, and locators +//! +//! [`Target`] is deliberately three things. A `@e12` ref comes from a +//! [`crate::Snapshot`] and is what an agent should normally use: it names an +//! element the agent has actually seen, and it fails loudly when the page has +//! moved on. A CSS selector is for a host that already knows the page. A +//! [`Locator`] is for the case an agent is best at — "the button called Submit" +//! — where neither of the other two is expressible. + +mod types; + +pub use types::{Action, ActionOutcome, LocateBy, Locator, ScrollDirection, Target, WaitState}; + +#[cfg(test)] +mod test; diff --git a/crates/tinybrowser-bus/src/action/test.rs b/crates/tinybrowser-bus/src/action/test.rs new file mode 100644 index 0000000..bdcfe87 --- /dev/null +++ b/crates/tinybrowser-bus/src/action/test.rs @@ -0,0 +1,172 @@ +//! Tests for the interaction payload types. + +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use super::{Action, ActionOutcome, LocateBy, Locator, ScrollDirection, Target, WaitState}; +use crate::PageState; +use serde_json::json; + +#[test] +fn parse_reads_an_at_prefix_as_a_ref() { + assert_eq!(Target::parse("@e12"), Target::reference("e12")); + assert_eq!(Target::parse(" @e12 "), Target::reference("e12")); +} + +#[test] +fn parse_reads_anything_else_as_a_selector() { + assert_eq!(Target::parse("#submit"), Target::selector("#submit")); + assert_eq!( + Target::parse("button[type=submit]"), + Target::selector("button[type=submit]") + ); +} + +#[test] +fn reference_drops_a_leading_at_however_it_arrives() { + assert_eq!(Target::reference("@e3"), Target::reference("e3")); +} + +#[test] +fn target_is_tagged_by_kind_on_the_wire() { + assert_eq!( + serde_json::to_value(Target::reference("e2")).expect("serializes"), + json!({ "kind": "ref", "value": "e2" }) + ); + assert_eq!( + serde_json::to_value(Target::selector("#a")).expect("serializes"), + json!({ "kind": "selector", "value": "#a" }) + ); +} + +#[test] +fn locator_round_trips_through_its_wire_form() { + let target = Target::locator(Locator::new(LocateBy::Role, "button").with_name("Submit")); + let encoded = serde_json::to_value(&target).expect("serializes"); + + assert_eq!( + encoded, + json!({ + "kind": "locator", + "value": { + "by": "role", + "value": "button", + "name": "Submit", + "exact": false, + "index": 0, + }, + }) + ); + assert_eq!( + serde_json::from_value::(encoded).expect("deserializes"), + target + ); +} + +#[test] +fn locator_fills_its_optional_fields_from_the_default() { + let locator: Locator = + serde_json::from_value(json!({ "by": "test_id", "value": "cart" })).expect("deserializes"); + + assert_eq!(locator.by, LocateBy::TestId); + assert_eq!(locator.index, 0); + assert!(!locator.exact); +} + +#[test] +fn action_is_tagged_by_action_on_the_wire() { + let click = Action::Click { + target: Target::reference("e2"), + new_tab: false, + }; + + assert_eq!( + serde_json::to_value(&click).expect("serializes"), + json!({ + "action": "click", + "target": { "kind": "ref", "value": "e2" }, + "new_tab": false, + }) + ); + assert_eq!( + serde_json::from_value::(json!({ + "action": "click", + "target": { "kind": "ref", "value": "e2" }, + })) + .expect("deserializes"), + click + ); +} + +#[test] +fn unit_actions_are_a_bare_tag() { + assert_eq!( + serde_json::to_value(Action::Reload).expect("serializes"), + json!({ "action": "reload" }) + ); + assert_eq!( + serde_json::from_value::(json!({ "action": "back" })).expect("deserializes"), + Action::Back + ); +} + +#[test] +fn typing_defaults_to_the_focused_element() { + let action: Action = + serde_json::from_value(json!({ "action": "type", "text": "hello" })).expect("deserializes"); + + assert_eq!( + action, + Action::Type { + target: None, + text: "hello".to_string(), + delay_ms: None, + } + ); +} + +#[test] +fn wait_for_defaults_to_waiting_for_visibility() { + let action: Action = serde_json::from_value(json!({ + "action": "wait_for", + "target": { "kind": "selector", "value": ".ready" }, + })) + .expect("deserializes"); + + let Action::WaitFor { state, .. } = action else { + panic!("expected a wait_for action"); + }; + assert_eq!(state, WaitState::Visible); +} + +#[test] +fn scroll_direction_is_snake_case_on_the_wire() { + assert_eq!( + serde_json::to_value(ScrollDirection::Bottom).expect("serializes"), + json!("bottom") + ); +} + +#[test] +fn an_acting_outcome_carries_no_value() { + let outcome = ActionOutcome::acted(PageState::new("https://example.com/")); + + assert!(outcome.value.is_null()); + assert!(outcome.matched.is_none()); +} + +#[test] +fn a_reading_outcome_round_trips_with_its_match() { + let outcome = ActionOutcome::read( + PageState::new("https://example.com/"), + json!("Example Domain"), + ) + .matching("button \"Submit\""); + let encoded = serde_json::to_value(&outcome).expect("serializes"); + + assert_eq!(encoded["value"], json!("Example Domain")); + assert_eq!(encoded["matched"], json!("button \"Submit\"")); + assert_eq!( + serde_json::from_value::(encoded).expect("deserializes"), + outcome + ); +} diff --git a/crates/tinybrowser-bus/src/action/types.rs b/crates/tinybrowser-bus/src/action/types.rs new file mode 100644 index 0000000..45bcc6a --- /dev/null +++ b/crates/tinybrowser-bus/src/action/types.rs @@ -0,0 +1,371 @@ +//! Payload types for interacting with the active page. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// How an element is named. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum Target { + /// A ref from a snapshot, such as `e12`. + /// + /// Refs belong to the snapshot that produced them. Acting on a ref after the + /// page has navigated or re-rendered is refused rather than guessed at — + /// a stale ref that silently resolves to whatever now occupies that position + /// is how an agent ends up clicking the wrong thing and reporting success. + Ref { + /// The ref identity, with or without its leading `@`. + value: String, + }, + /// A CSS selector, matched against the first element it finds. + Selector { + /// The selector text. + value: String, + }, + /// A semantic locator: role, visible text, label, and so on. + Locator { + /// The locator itself. + value: Locator, + }, +} + +impl Target { + /// Reads a target from the string form a host tool receives from a model. + /// + /// A leading `@` means a ref, and anything else is a CSS selector. That rule + /// exists because the tool surface an agent sees takes one `selector` string + /// for both, and `@` is not valid at the start of a CSS selector — so the + /// two vocabularies cannot collide. + /// + /// # Examples + /// + /// ``` + /// # use tinybrowser_bus::Target; + /// assert_eq!(Target::parse("@e12"), Target::reference("e12")); + /// assert_eq!(Target::parse("#submit"), Target::selector("#submit")); + /// ``` + #[must_use] + pub fn parse(target: &str) -> Self { + let trimmed = target.trim(); + match trimmed.strip_prefix('@') { + Some(reference) => Self::reference(reference), + None => Self::selector(trimmed), + } + } + + /// A target naming the snapshot ref `value`, with any leading `@` removed. + #[must_use] + pub fn reference(value: impl AsRef) -> Self { + Self::Ref { + value: value.as_ref().trim_start_matches('@').to_string(), + } + } + + /// A target naming the first element matching the CSS selector `value`. + #[must_use] + pub fn selector(value: impl Into) -> Self { + Self::Selector { + value: value.into(), + } + } + + /// A target naming an element semantically. + #[must_use] + pub fn locator(value: Locator) -> Self { + Self::Locator { value } + } +} + +/// The dimension a [`Locator`] searches on. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum LocateBy { + /// The element's accessibility role, narrowed by its accessible name. + Role, + /// Visible text content. + Text, + /// The text of the element's `