# 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

Three of the vendored packages contain compiled extension modules and are
therefore platform-specific: `pyqwest` (~15 MB, the HTTP core used by
`connectrpc`), `protobuf` and `protobuf-py-ext`. Everything else is pure
Python. The tree is split accordingly, so the shared part is stored once:

```
vendor/
├── __init__.py
├── common/                    # pure-Python packages (~8 MB), all platforms
│   ├── chattolib/
│   ├── connectrpc/
│   ├── httpx/ httpcore/ h11/ anyio/ idna/ certifi/
│   └── opentelemetry/ protobuf/ typing_extensions.py
└── platform/                  # compiled extensions, ~16 MB per platform
    ├── linux-x86_64/          #   pyqwest/, protobuf_ext/, google/
    ├── linux-aarch64/
    └── macos-arm64/
```

`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/<tag>` | **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.

The packages Hermes does *not* provide — `chattolib`, `connectrpc`, `pyqwest`,
`protobuf`, `protobuf-py`, `protobuf-py-ext` — always come from `vendor/`. They
are also the bulk of it: `pyqwest` alone is ~15 MB per platform, so dropping
the packages Hermes already ships would save only ~2.7 MB of ~59 MB and buy a
dependency on Hermes' exact pins. Not worth it.

chattolib requires `httpx>=0.27` and `protobuf>=5.28`; connectrpc requires
`protobuf-py==0.1.1` and `pyqwest>=0.5.1`. 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
CHATTOLIB_SPEC="chattolib==0.4.20" ./vendor_chattolib.sh
```

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 (~59 MB for `vendor/`).

### Python versions

The script resolves wheels for **Python 3.11** (`PYTHON_VERSION`), which is the
project minimum. At that version the three binary packages still provide
`cp310-abi3` wheels, and the stable ABI is forward compatible — so one build
covers 3.11 through 3.14. Resolving for 3.12+ would instead pull
version-specific wheels (`cp312-…`) that only work on that exact version.

**If you raise `PYTHON_VERSION`, check that the resulting `.so` files are still
named `*.abi3.so`.** If they are not, the vendored tree silently becomes
Python-version-specific.

## 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.

## 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
