# Vendoring chattolib This plugin ships a **vendored copy of chattolib and all its dependencies** in `vendor/`. Nothing has to be installed separately — the plugin brings its own dependency tree. ## Why Vendoring? - **No dependencies to install**: `hermes plugin install` works immediately - **No lazy-import complexity**: no `ensure()` or `lazy_import` mechanisms - **Isolated**: the vendored copy cannot conflict with a system-installed one - **Offline-friendly**: works in air-gapped environments ## Layout Two of the vendored packages contain compiled extension modules and are therefore platform-specific: `protobuf` (its `google/_upb` runtime) and `websockets` (its `speedups` mask helper). Everything else is pure Python. The tree is split accordingly, so the shared part is stored once: ``` vendor/ ├── __init__.py ├── common/ # pure-Python packages (5.8 MB), all platforms │ ├── chattolib/ │ ├── httpx/ httpcore/ h11/ anyio/ idna/ certifi/ │ └── typing_extensions.py └── platform/ # compiled extensions, 2.3–3.9 MB per platform ├── linux-x86_64/ # google/ websockets/ ├── linux-aarch64/ └── macos-arm64/ ``` Total: 14.4 MB on disk (~9.6 MB tracked by git, the `.so` files compress well). `vendor_path.py` derives the platform tag from `platform.system()` / `platform.machine()` at import time and puts both directories on `sys.path`. Both `adapter.py` and `platform_config.py` call `setup_vendor_path()` before importing anything from `chattolib`. ### sys.path order — and why it differs per directory | Directory | Position | Why | | --- | --- | --- | | `platform/` | **prepended** | The compiled extensions must match the vendored chattolib. A host copy of `protobuf` at a different version would break the generated `_pb` code. | | `common` | **appended** | The Hermes agent ships most of this itself — `httpx` (same pin, 0.28.1), `certifi`, `anyio`, `httpcore`, `h11`, `idna`, `typing_extensions`. A plugin must not shadow the host's pinned versions; Hermes pins `certifi` exactly, and prepending would substitute our CA bundle process-wide. | So the host's pure-Python packages win, and our copies act as a fallback — which keeps out-of-process use (the cron sender, `hermes_standalone_sender_fn`) working in an environment without Hermes. Note that Hermes' pins drift from ours (`anyio` 4.12.1 vs 4.15.1, `idna` 3.18 vs 3.20, `certifi` 2026.5.20 vs 2026.7.22) — the append order is exactly what keeps those differences harmless. The packages Hermes does *not* provide — `chattolib` and the compiled `protobuf`/`websockets` — always come from `vendor/`. The rest (`httpx`, `httpcore`, `h11`, `anyio`, `idna`, `certifi`, `typing_extensions`, ~4.2 MB) Hermes *does* provide, so it is fair to ask why they are vendored at all. Because of the cron sender: `hermes_standalone_sender_fn` delivers scheduled messages out of process, in no Hermes venv, and there `vendor/common` is the only thing on `sys.path`. The append order means those copies are inert wherever Hermes runs and load-bearing where it does not. chattolib requires `httpx>=0.27`, `protobuf>=5.28` and (via the `[realtime]` extra) `websockets>=13.0`. Two of those floors are worth knowing about: `protobuf>=5.28` is wrong in practice — chattolib b13 ships gencode 7.35.1, and protobuf ≥ 5.27 refuses at import time to load gencode newer than its own runtime, so a host protobuf older than 7.35.1 raises `VersionError` rather than degrading. And if a future Hermes release pins `httpx` below 0.27, the appended fallback no longer helps — the host copy would be found first and be too old. If the current platform was not vendored, the import fails with an explicit message naming the available platforms — rather than a cryptic `dlopen` error about an invalid Mach-O/ELF file. ## Updating or rebuilding ```bash # Default platform set: linux-x86_64, linux-aarch64, macos-arm64 ./vendor_chattolib.sh # A single platform PLATFORMS="linux-x86_64" ./vendor_chattolib.sh # Add another one (see the table below for valid tags) PLATFORMS="linux-x86_64 linux-aarch64 macos-arm64 windows-amd64" ./vendor_chattolib.sh # Pin a specific chattolib version (keep the [realtime] extra — without it # websockets is not vendored and the event stream cannot start) CHATTOLIB_SPEC="chattolib[realtime]==0.5.0b13" ./vendor_chattolib.sh ``` The script passes `--only-binary :all: --no-binary chattolib`. Everything must come as a prebuilt wheel, because a source distribution of `pyqwest`, `protobuf` or `websockets` could not be built for a *foreign* platform — uv would build it for the host and silently drop host-architecture binaries into `vendor/platform//`. chattolib itself is the documented exception: it is pure Python, so building it from an sdist yields identical, platform-independent files on any host. That exception is load-bearing, not cosmetic — upstream stopped publishing wheels after 0.5.0b4, so without it the resolver falls back to b4 and a refresh silently changes nothing. The script wipes `vendor/`, installs the dependency tree once per platform via `uv pip install --python-platform …`, splits the result with `vendor_split.py`, and verifies that `import chattolib` works on the machine you ran it on. Afterwards: `git add vendor/`. ### Supported platform tags | Tag | uv `--python-platform` | vendored by default | | -------------------- | ------------------------------ | ------------------- | | `linux-x86_64` | `x86_64-unknown-linux-gnu` | ✅ | | `linux-aarch64` | `aarch64-unknown-linux-gnu` | ✅ | | `macos-arm64` | `aarch64-apple-darwin` | ✅ | | `linux-x86_64-musl` | `x86_64-unknown-linux-musl` | — | | `linux-aarch64-musl` | `aarch64-unknown-linux-musl` | — | | `macos-x86_64` | `x86_64-apple-darwin` | — | | `windows-amd64` | `x86_64-pc-windows-msvc` | — | All seven are available as binary wheels upstream; only the default three are committed, to keep the repository at a reasonable size (14.4 MB for `vendor/`). ### Python versions The script resolves wheels for **Python 3.11** (`PYTHON_VERSION`), which is the project minimum. What that buys differs per package, and the difference matters: | Package | Tag at 3.11 | Consequence | | --- | --- | --- | | `protobuf` | `abi3` (`_message.abi3.so`) | Stable ABI, forward compatible — one build covers 3.11 through 3.15. | | `websockets` | `cp311` (`speedups.cpython-311-…so`) | Version-specific. | So on Python 3.12+ the protobuf runtime still loads, and `chattolib` imports fine — but `websockets.speedups` is *not* importable and websockets silently falls back to its pure-Python `apply_mask` (it wraps the import in `try: … except ImportError:`). Verified on 3.12 and 3.14: chattolib and its realtime module import, `apply_mask` resolves to `websockets.utils`. That is a performance fallback, not a breakage — masking is a small part of frame handling. It is the price for keeping one build instead of one per minor version, and the reason `PYTHON_VERSION` must stay at the project minimum: raising it would pull `cp312-…`-style wheels and make the whole tree version-specific. **If you raise `PYTHON_VERSION`, check the resulting `.so` names.** `*.abi3.so` for protobuf; anything under `websockets/` is tied to that exact CPython minor version regardless. ## Development without vendoring ```bash pip install chattolib ``` A system-installed chattolib is only used if `vendor/` is absent — the vendored copy is deliberately prepended to `sys.path` and therefore wins. Keep the installed version in step with `vendor/common/chattolib-*.dist-info/`, or tests will pass against one chattolib while the plugin ships another. ## License The vendored chattolib is licensed under **MPL-2.0 + Apache-2.0** (see `vendor/common/chattolib-*.dist-info/`). Upstream source: https://github.com/chattocorp/chatto