> ## Documentation Index
> Fetch the complete documentation index at: https://amd-gaia.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Email Agent Packaging

> Distribute the email triage agent as an npm-installable local sidecar — a thin JS/TS client plus per-platform Python and C++ binaries — for embedding in third-party web apps

# Email Agent Packaging

<Note>
  **Milestone:** Email Agent & Platform Foundations | **Status:** Planning | **Priority:** High
</Note>

<Warning>
  **Work in Progress** — this plan is the authoritative scope for the packaging milestone. Distribution mechanics (npm layout, signing ownership) are settled below; open decisions are called out explicitly.
</Warning>

***

## Overview

The email triage agent already builds as a Python wheel + sdist (`gaia-agent-email`) and is shareable as a GitHub prerelease asset. That path serves Python integrators and partner evaluation, but it does **not** serve the primary consumer: a **JavaScript/Node host web app that wants to embed the agent as a local service** with a one-command install and no Python/C++ toolchain on the user's machine.

This milestone delivers that path: the email agent distributed through **npm** as a thin **JS/TS client** plus **per-platform, self-contained binaries** (a frozen Python build and a native C++ build), runnable as a **local sidecar** the host app spawns and talks to over a loopback REST API.

### Goals

* A host web app can run `npm install @amd-gaia/agent-email` and, with one **build-time fetch** step, get a working local email agent — **no Python install, no C++ toolchain, no compile step**.
* The npm package stays **lightweight** (JS/TS client + fetch CLI only); the large binaries live in **R2** object storage, the single source of truth.
* **One REST contract** backs both the Python and C++ implementations; the host integrates once and is implementation-agnostic.
* The platform binary is **present on disk at build/sign time** (fetched before the host signs its app), so it's covered by the host's code signature and runs **fully offline at runtime**.
* Each platform fetches **only its own binary**, integrity-verified against a **pinned SHA-256** — fail loudly on mismatch.
* Binaries are **signed/notarized** so a host app that bundles them stays signable.

### Non-goals (this milestone)

* Re-enabling the paused PyPI publish path (tracked separately under #1179).
* An in-process Node native addon (N-API). Captured as a future fast-path in [Deferred](#deferred), not built here.
* Third-party host glue. We ship the agent + client + contract; an external host owns its own integration. **Exception:** the GAIA Agent UI is our *reference consumer* and its migration to the sidecar is in scope as the validation phase ([Phase 6](#phase-6-agent-ui-as-first-consumer-validation)).
* New triage capabilities. This is a **packaging and distribution** milestone, not a feature milestone.

***

## Current State

| Asset                                          | State                                                                                                                                                                                                 |
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gaia-agent-email` wheel + sdist               | ✅ Builds via `build-agent-wheel` composite action                                                                                                                                                     |
| GitHub-prerelease share                        | ✅ `build_agent_package.yml` (`workflow_dispatch`), `agent-pkg-email-v0.1.0` published — kept as the **manual** mirror                                                                                 |
| R2 distribution backend (`workers/agent-hub/`) | ✅ Cloudflare Worker with `POST /publish` (Bearer auth, version-immutability, server-side SHA-256, `agents/<id>/<version>/…` layout, index rebuild) — **reuse as the canonical store**                 |
| CI workflow that pushes binaries to R2         | ❌ Does not exist — `build_agents.yml` uploads only GitHub Actions artifacts ("R2 … handled by other issues"); `build_agent_package.yml` is **manual** → GitHub release — **this milestone**           |
| PyPI publish                                   | ⏸ Paused (#1179) — out of scope here                                                                                                                                                                  |
| REST API surface                               | ✅ Email REST router exists (triage, connectors); needs a published contract                                                                                                                           |
| Agent UI email integration                     | ⚠️ Today the UI backend imports the `gaia-agent-email` **Python wheel** (`amd-gaia[api]` auto-mounts its router); the React UI talks to it via the backend. Phase 6 migrates this to the npm sidecar. |
| npm client                                     | ❌ Does not exist — **this milestone**                                                                                                                                                                 |
| Frozen Python binary (no-interpreter)          | ❌ Does not exist — **this milestone**                                                                                                                                                                 |
| C++ implementation + binary                    | ❌ `hub/agents/cpp/` is an empty placeholder — **this milestone**                                                                                                                                      |
| Build-time fetch CLI + SHA-256 manifest        | ❌ Does not exist — **this milestone**                                                                                                                                                                 |
| Signed/notarized binaries                      | ❌ Does not exist — **this milestone**                                                                                                                                                                 |
| Sidecar lifecycle contract                     | ❌ Not specified — **this milestone**                                                                                                                                                                  |

***

## Target Architecture

### Sidecar model

The agent runs as a **separate local process** the host app spawns, supervises, and talks to over loopback HTTP. This is the default because it is **language-agnostic** (the Python and C++ builds look identical to the host — spawn a binary, hit a port), **crash-isolated** (a segfault in the agent does not take down the host), and allows **independent release cadence**.

```
┌─────────────────────────┐        loopback REST          ┌──────────────────────────┐
│  Host web app (Node)     │  ───────────────────────────▶ │  Email agent sidecar      │
│                          │   POST /v1/email/triage        │  (frozen Python OR C++)   │
│  @amd-gaia/agent-email   │   GET  /health  /version       │  REST server + connectors │
│  (JS/TS client)          │ ◀───────────────────────────  │                           │
│   • spawns binary        │                                └──────────────────────────┘
│   • health/version check │
│   • typed REST calls     │
└─────────────────────────┘
```

### Distribution — thin npm package + build-time R2 fetch

The npm package carries **only** the JS/TS client and a small fetch CLI — **no binaries**. The large per-platform binaries live in **R2** (the existing `workers/agent-hub/` backend), which becomes the single source of truth. The host pulls its platform binary with an **explicit build-time step**, before it signs its app.

```
@amd-gaia/agent-email             ← npm: JS/TS client + `fetch` CLI + SHA-256 manifest. NO binaries.
        │  build-time: npx @amd-gaia/agent-email fetch
        ▼
R2 via Agent Hub Worker           ← canonical, immutable, versioned (existing POST /publish + bucket)
  agents/email/<version>/email-win32-x64.exe
  agents/email/<version>/email-darwin-arm64
  agents/email/<version>/email-darwin-x64
  agents/email/<version>/email-linux-x64
        │  verified against pinned SHA-256, written to the host's resources dir
        ▼
host app bundle (signed)          ← binary on disk at sign time → covered by the host signature
```

The binaries are stored under the **existing Agent Hub R2 layout** (`agents/<id>/<version>/…`) and served by the same Worker that already fronts the bucket. The fetch CLI resolves `${process.platform}-${process.arch}`, downloads that one object, **verifies it against a SHA-256 pinned in the package** (fail loudly on mismatch), and writes it to a host-specified resources directory:

```jsonc theme={null}
// @amd-gaia/agent-email/package.json (the only published binary-bearing artifact is the manifest)
{
  "name": "@amd-gaia/agent-email",
  "version": "0.1.0",
  "bin": { "agent-email": "./cli.js" },     // exposes `npx @amd-gaia/agent-email fetch`
  "files": ["dist/", "binaries.lock.json"]  // SHA-256 + R2 keys per platform, no binaries
}
```

```ts theme={null}
// resolves the *fetched* binary (written by the build-time fetch step), not an npm sub-package
function binaryPath(resourcesDir: string): string {
  const ext = process.platform === "win32" ? ".exe" : "";
  return path.join(resourcesDir, `email-${process.platform}-${process.arch}${ext}`);
}
```

**Build-time, not runtime.** The fetch runs in the host's build pipeline *before* code-signing, so the binary is on disk when the host signs and notarizes — it is covered by the host's signature and needs no network at runtime. A **runtime / first-init download is explicitly rejected**: a binary fetched after the app is signed is not covered by the signature (macOS Gatekeeper quarantine blocks it; Windows SmartScreen flags it) and breaks offline first-run.

**Why this over the alternatives:**

* **Binaries in npm via `optionalDependencies`** (esbuild/swc pattern) — considered, and viable; it's the most reproducible (lockfile-pinned) and lowest-friction for the consumer (`npm install` just works). Rejected as the default here because frozen-Python binaries are large (\~40–100 MB each), and we want **one canonical binary store (R2)** shared across the npm path, the `gaia agent install` Hub path, and the GitHub manual share — rather than duplicating tens of MB into the npm registry per release.
* **`postinstall` download** — breaks under `npm install --ignore-scripts` and behind proxies; rejected. The fetch is an **explicit build step**, not an install hook.
* **Runtime / first-init download** — rejected (signing + offline reasons above).

A **runtime-fetch mode** is offered *only* for the non-bundled server/dev consumer (behind an explicit flag, same SHA-256 verification, fails loudly when offline) — never into a signed app bundle.

### Lifecycle handshake

The host app implements a deterministic startup sequence; the client SDK provides helpers for each step:

1. **Resolve** the platform binary (`binaryPath()` above) — the binary fetched at build time from R2 and SHA-verified.
2. **Spawn** it on an allocated loopback port (`agent --port <p> --host 127.0.0.1`).
3. **Health** — poll `GET /health` until ready or timeout (fail loudly on timeout — no silent "assume ready").
4. **Version-check** — `GET /version` returns `{ apiVersion, agentVersion }`; the client refuses a mismatched `apiVersion` with an actionable error rather than making calls against an incompatible binary.
5. **Ready** — typed REST calls; clean shutdown on host exit.

### One contract, two implementations

A single **OpenAPI spec** is the source of truth for the REST surface. The Python and C++ servers both implement it; the JS/TS client is generated/validated against it. Breaking changes bump a SemVer'd `apiVersion` advertised on `/version`. This is what makes the host integration implementation-agnostic.

***

## Workstreams

### Phase 1 — REST contract & client SDK

**Goal:** lock the wire contract and ship the JS/TS client against the existing Python server.

* Author the **OpenAPI spec** for the email agent REST surface (triage, connectors, health, version). Commit it to the repo as the source of truth.
* Add `GET /version` (`{ apiVersion, agentVersion }`) and confirm `GET /health` semantics on the existing Python server.
* Build `@amd-gaia/agent-email` (TypeScript): typed REST client + lifecycle helpers (spawn, health-poll, version-check, shutdown). Full type defs, no runtime binary yet.
* Unit + contract tests: client validated against the OpenAPI spec; lifecycle helpers tested against a stub server.

**Exit criteria:** a Node script can `import` the client, point it at a manually-started Python server, and run a triage round-trip with types.

### Phase 2 — Frozen Python binary

**Goal:** the Python implementation runs with **no interpreter on the user's machine**.

* Freeze `gaia-agent-email` (PyInstaller or Nuitka) into a single self-contained executable that boots the REST server.
* Per-platform builds: `win32-x64`, `darwin-arm64`, `darwin-x64`, `linux-x64`.
* Smoke test each frozen binary in a **clean environment with no Python**: spawn → health → version → triage.
* Compute and record each binary's **SHA-256** (feeds the Phase 3 pinned manifest); measure per-platform size for R2 storage/transfer.

**Exit criteria:** all four frozen binaries pass the no-Python smoke test and the lifecycle handshake.

### Phase 3 — Fully-automated R2 distribution + thin npm package

**Goal:** a single version tag publishes everything — binaries to R2 and the thin npm package — with **zero manual steps**. `npm install @amd-gaia/agent-email` + one build-time fetch then yields a working, signature-ready agent.

* **Fully-automated, tag-triggered publish workflow** (no `workflow_dispatch` gate): on a namespaced tag, the workflow builds every platform binary → computes SHA-256 → pushes each to R2 via the existing Worker `POST /publish` (Bearer `PUBLISH_TOKENS` from a GH Actions secret) → writes the pinned manifest → publishes the npm package. Re-running a published version is a **no-op** (the Worker's native version-immutability), not an overwrite.
* Reuse the existing pipeline rather than forking: extend `build_agents.yml`'s release-bundle leg (it already produces per-platform binaries + checksums) to call `POST /publish` instead of stopping at GitHub-artifact upload.
* Confirm/extend `POST /publish` to accept **multiple per-platform binaries under one `<id>/<version>`** (today it stores `gaia-agent.yaml` + an artifact; the four OS/arch binaries must coexist in one version without tripping per-version immutability).
* Build the thin `@amd-gaia/agent-email` npm package: JS/TS client + `fetch` CLI + a **`binaries.lock.json`** manifest pinning each platform's R2 key and **SHA-256**.
* `fetch` CLI: resolve platform → download from R2 → **verify SHA-256 (fail loudly on mismatch)** → write to the host-specified resources dir → `chmod +x` on POSIX.
* Document the host integration: run `npx @amd-gaia/agent-email fetch` as a **build step before signing**, and (for Electron-style hosts) keep the fetched binary outside the `asar` (`extraResources`) so it can be spawned.
* **Atomic release:** R2 upload → manifest write → npm publish on one tag; manifest SHAs must match the uploaded R2 objects, and the npm version must reference an R2 version that exists, or the whole release fails.

**Exit criteria:** pushing a version tag publishes binaries to R2 and the npm package end-to-end with no human step; on three real OSes, `npm install` + `npx @amd-gaia/agent-email fetch` retrieves the platform binary, SHA-256 verifies, and the lifecycle handshake succeeds; install works under `--ignore-scripts`.

### Phase 4 — Code signing & notarization

**Goal:** bundled binaries don't break a host app's own signing.

* macOS: sign + **notarize** the `darwin` binaries (unsigned/un-notarized binaries break the host app's notarization).
* Windows: Authenticode-sign the `win32` binary (unsigned trips SmartScreen on the host's signed app).
* **Open decision:** *who holds the signing cert* — we sign and publish signed binaries, or we ship reproducible builds the partner signs under their cert. Resolve before Phase 3 publish.

**Exit criteria:** signed binaries verify on each OS; a test host app bundling them passes notarization/SmartScreen.

### Phase 5 — C++ implementation & binary

**Goal:** a second implementation of the **same contract**, distributed identically.

* Stand up the C++ email agent server under `hub/agents/cpp/email/`, implementing the Phase 1 OpenAPI contract (same routes, same `/version` `apiVersion`).
* Build static / dependency-bundled binaries for the four platforms via the existing C++ build infrastructure (`build_cpp.yml`, `cpp/packaging/`).
* Distribute via the **same R2 + thin-package mechanism** — upload to the same R2 key layout, pin SHA-256 in the manifest; the host install + fetch path is unchanged, only the binary's provenance differs.
* Conformance test: the C++ binary passes the **same contract test suite** as the Python binary (identical request/response behavior).

**Exit criteria:** the C++ binary is a drop-in for the frozen Python binary behind the same client, validated by a shared contract test suite.

### Phase 6 — Agent UI as first consumer (validation)

**Goal:** dogfood the package. The GAIA Agent UI (`@amd-gaia/agent-ui`, Electron) consumes the email agent as the **npm-fetched binary sidecar** instead of importing the `gaia-agent-email` **Python wheel** into its backend. This is the milestone's real acceptance test — if our own app can install, fetch, spawn, and triage through the package, an external host can too.

* Add `@amd-gaia/agent-email` as a dependency of the Electron app; run the build-time `fetch` in the app's build; spawn the sidecar via the client's lifecycle handshake (spawn → health → version → ready).
* Re-point the UI's email features (`ChatView`, `EmailPreScanCard`, `EmailConnectCta`, `useConnectorsSSE` triage calls) at the sidecar instead of the backend's mounted `/v1/email` router.
* **Connectors via shared OS keyring (no token handoff):** the backend keeps owning the OAuth connect flow and keyring writes (connect CTAs unchanged); the sidecar reads the **same OS keyring** through its bundled `gaia.connectors`. This relies on the connectors module's already-documented "two processes share the keyring" behavior — note the process-local token cache + concurrent-refresh caveat, but no new auth protocol.
* **Remove the built-in path:** stop pulling the `gaia-agent-email` wheel into the UI backend's dependency/auto-mount chain, so the sidecar is the only email surface — proving the package fully replaces the in-process agent.
* Keep the Python backend for everything non-email; only email migrates to the sidecar in this phase.

**Exit criteria:** with the `gaia-agent-email` wheel removed from the UI backend, the Agent UI completes a triage + pre-scan flow through the spawned `@amd-gaia/agent-email` sidecar, and an existing Gmail/Outlook connection (granted via the unchanged connect flow) is picked up by the sidecar from the shared keyring.

***

## CI / Release Pipeline

**Deployment and distribution are fully automated — a version tag is the only human action.** No `workflow_dispatch`, no manual upload, no manual npm publish.

* **One tag-triggered workflow** does the whole release: build matrix → `chmod +x` + sign + SHA-256 → push each binary to R2 via the Worker `POST /publish` → write the pinned manifest → `npm publish`. The current manual `build_agent_package.yml` (`workflow_dispatch` → GitHub release) is demoted to a fallback mirror, not the release path.
* **Reuse the existing legs:** extend `build_agents.yml` (C++ binaries + checksums, already tag-aware) and the `build-agent-wheel` action to push to R2 via `POST /publish` rather than stopping at GitHub-artifact upload — the workflow comment "R2 … handled by other issues" is exactly this work.
* **Idempotent by construction:** the Worker rejects republishing an existing `<id>@<version>` (native version-immutability), so a re-run or an unchanged-version tag is a safe no-op — no custom overwrite logic.
* **Atomic release:** R2 upload → manifest write → npm publish on a single tag. Manifest SHA-256s must match the uploaded R2 objects, and the npm version must reference an R2 version that exists, or the whole release fails — no client-without-binary or stale-SHA states.
* **Auth:** the publish step authenticates to the Worker with a Bearer `PUBLISH_TOKENS` secret stored in GitHub Actions (scoped to the `email` author); never embedded in the package.
* **Canonical store = R2, mirror = GitHub:** R2 (`workers/agent-hub/`) is the programmatic source of truth; the `agent-pkg-email-*` GitHub release stays as the manual, no-login mirror.
* **Namespacing:** keep release tags namespaced (`agent-pkg-email-*`) so they never fire the paused PyPI workflows (`publish.yml` / `publish_agents.yml`).
* **Distribution registries:** the existing **`@amd-gaia`** npm scope (already hosts `@amd-gaia/agent-ui`). The thin model needs **one public package, `@amd-gaia/agent-email`** — no per-platform sub-packages.
* **npm auth = trusted publishing (OIDC), no stored token.** The package is configured as an npm trusted publisher; the publish job requests `id-token: write` and runs `npm publish --access public` with provenance — **no `NPM_TOKEN` secret to store or rotate**. This mirrors the repo's PyPI trusted-publishing pattern (`publish_agents.yml`). The publish must run from the exact workflow file registered as the trusted publisher on npm.

***

## Versioning & Contract Governance

`apiVersion` is the host-facing contract version (SemVer), advertised on `GET /version` and defined as a single constant — `SCHEMA_VERSION` in `hub/agents/python/email/gaia_agent_email/contract.py`, aliased as `API_VERSION` in `version.py`. Bumping the contract automatically bumps the API version; they cannot drift.

### `apiVersion` bump policy

| Change type                                                      | Example                                              | Version bump      |
| ---------------------------------------------------------------- | ---------------------------------------------------- | ----------------- |
| Field removed, renamed, or retyped on any request/response model | Remove `is_phishing`, rename `body` → `text`         | **MAJOR**         |
| Enum value removed or renamed                                    | Remove `PROMOTIONAL` from `EmailCategory`            | **MAJOR**         |
| Endpoint removed or its method changed                           | Delete `POST /triage`                                | **MAJOR**         |
| New required field added to a request model                      | Add required `locale` to `EmailTriageRequest`        | **MAJOR**         |
| New optional field added to a request or response                | Add optional `thread_summary` to `EmailTriageResult` | **minor**         |
| New endpoint added                                               | Add `GET /v1/email/threads`                          | **minor**         |
| New enum value added                                             | Add `CALENDAR_INVITE` to `EmailCategory`             | **minor**         |
| Bug fix with no wire-format change                               | Fix a 500 on malformed input                         | none (patch only) |

**Clients must reject a server whose `apiVersion` major is higher than what they support** — fail loudly with an actionable error, never fall back to best-effort parsing against an incompatible schema.  Clients may accept any minor version equal to or greater than their compiled-against minor.

**How to apply a bump:** update `SCHEMA_VERSION` in `contract.py`, regenerate the spec (`python -m gaia_agent_email.export_openapi`), and run `python -m pytest hub/agents/python/email/tests/test_rest_contract.py tests/test_email_openapi_conformance.py` to confirm no drift.

* **Package version** tracks the agent build; the npm package, the R2 binary version (`agents/email/<version>/…`), and the pinned `binaries.lock.json` manifest always publish in lockstep — the manifest references the matching R2 version, never a floating one.
* The **OpenAPI spec (`openapi.email.json`) is the single source of truth** — client, Python server, and C++ server are all validated against it in CI. A change to the contract that isn't reflected in the committed artifact fails CI (`test_rest_contract.py::test_committed_openapi_artifact_is_up_to_date`), and a running server that doesn't conform to the spec fails the conformance suite (`tests/test_email_openapi_conformance.py`).

***

## Testing Strategy

| Layer           | Test                                                                                                                           |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| Contract        | Client + both servers validated against the OpenAPI spec in CI                                                                 |
| Lifecycle       | spawn → health → version → triage → shutdown, against each binary                                                              |
| Clean-env       | Frozen Python binary on a machine with **no Python**; C++ binary with **no toolchain**                                         |
| Per-platform    | Full install + handshake on `win32-x64`, `darwin-arm64`, `darwin-x64`, `linux-x64`                                             |
| Conformance     | Python and C++ binaries pass the **same** contract suite (drop-in equivalence)                                                 |
| Signing         | Notarization (macOS) / SmartScreen (Windows) on a test host app that runs the build-time fetch then bundles + signs the binary |
| Integrity       | `fetch` rejects a tampered/size-mismatched R2 object (SHA-256 fail-loud); succeeds on a match                                  |
| Offline runtime | After a build-time fetch, the agent spawns and serves with **no network**                                                      |
| Install shape   | `npm install` pulls only the thin package; `--ignore-scripts` still works (fetch is an explicit step, not a hook)              |

***

## Risks

* **Runtime download mistaken for build-time fetch** — if a host wires the fetch at runtime/first-init instead of at build time, the binary lands outside its code signature (Gatekeeper/SmartScreen blocks) and breaks offline first-run. *Mitigation:* make `fetch` an explicit, documented build step; the integration guide leads with "fetch before sign"; the runtime-fetch flag is opt-in and documented as server/dev-only.
* **R2 availability / integrity at build time** — a build can't fetch if R2 is down or an object is corrupt. *Mitigation:* immutable versioned keys, SHA-256 pinned in the package (fail loudly on mismatch), and the GitHub release as a manual fallback mirror.
* **Manifest ↔ R2 drift** — a published SHA that doesn't match the uploaded object bricks every consumer. *Mitigation:* the atomic release gates publish on manifest SHAs matching R2 objects.
* **Signing ownership unresolved** — blocks the first signed release. *Mitigation:* resolve the cert-ownership decision (us vs. partner) before Phase 4.
* **Contract drift between Python and C++** — two implementations of one spec can diverge. *Mitigation:* shared OpenAPI source of truth + a single conformance suite both must pass in CI.
* **Electron asar packaging** — binaries inside an asar can't be spawned. *Mitigation:* document the `asarUnpack` requirement prominently in the integration guide.
* **Executable-bit loss** — a binary published without the exec bit fails to spawn on POSIX. *Mitigation:* CI `chmod +x` + a post-pack assertion.

***

## Deferred

* **In-process Node native addon (N-API)** for the C++ build — lowest latency, in-memory data sharing, but couples to the host's exact Node/Electron ABI and makes a crash fatal to the host. Revisit as a v2 fast-path once the sidecar path is proven.
* **PyPI publish** — remains paused under #1179; the npm path does not depend on it.
* **Additional agents** — the npm mechanism is designed to generalize (swap the agent id), but onboarding other agents is out of scope for this milestone.

***

## Exit Criteria (Milestone)

1. `npm install @amd-gaia/agent-email` + `npx @amd-gaia/agent-email fetch` on each of the four target platforms yields a working agent with no Python/C++ prerequisites, pulling only that platform's binary from R2 and SHA-256-verifying it.
2. The JS/TS client runs the full lifecycle handshake and a triage round-trip against the frozen Python binary.
3. Binaries are signed/notarized and verified inside a test host app that fetched at build time; the agent then runs **offline**.
4. The C++ binary passes the same contract suite as the Python binary and is a drop-in behind the same client.
5. The thin npm package installs under `--ignore-scripts`; `fetch` fails loudly on a SHA-256 mismatch or when offline.
6. The OpenAPI contract is published and enforced in CI across client + both servers.
7. **Deployment is fully automated:** pushing a version tag publishes binaries to R2 and the npm package end-to-end with no manual step, and re-running the release is a safe no-op.
8. **Dogfooded:** the GAIA Agent UI runs email triage through the `@amd-gaia/agent-email` sidecar with the `gaia-agent-email` Python wheel removed from its backend, picking up connector tokens from the shared OS keyring.
