Skip to main content

Flagship Installer Plan

Status: Mostly built, not yet released — this document was written after the architecture, so it’s a record of what exists plus an honest gap list, not a from-scratch proposal. Issue: #2804 Branch: feat/gaia-flagship-agent-2804 Scope: the flagship gaia agent (hub/agents/gaia/python/) + the Go terminal UI (tui/). Does not cover the Electron Agent UI installer — see Desktop Installer Plan for that, and §7 below for why they’re different artifacts with different install strategies.

§0 What this document is

The original ask was “design an installer, write the plan first, then build it.” By the time that research pass finished, it turned out most of it was already built — across roughly 30 commits on this branch, by other agents working the same issue in parallel. hub/agents/gaia/python/packaging/freeze.py, the npm client (hub/agents/gaia/npm/), the two-lane release workflow (.github/workflows/release_agent_gaia.yml), and the TUI’s first-run setup gate (tui/internal/ui/chat/setup.go) all exist, are tested (4,100+ lines of tests across Go/Python/TypeScript), and match the constraints this issue lays out — including ones I hadn’t been told about yet, like refusing port 4001. So this plan does three things instead of one:
  1. Documents the as-built architecture and the reasoning behind it (§1–§5), so the decisions are written down somewhere other than commit messages.
  2. States explicitly where I agree and disagree with the other plans that touch this space — Desktop Installer Plan, Agent Hub Plan, and the hub-publishing guide (§7).
  3. Names the real gaps — signing, an unmeasured sidecar size, docs that drifted out of sync with the code, and the fact that nothing has actually been released yet (§8–§9).
Building this a second time, differently, would have thrown away working, tested code for no reason. The remaining work is finishing what’s here, not replacing it.

§1 The goal, restated

A user downloads one thing, runs it, and has a working gaia agent + terminal UI — no repo clone, no Python installed by hand, no venv. That’s npx @amd-gaia/gaia today, once the release is cut (§9).

§2 Why Python, not C++ — the pivot this branch represents

Issue #2804 was originally scoped as a native C++ agent — see docs/plans/gaia-agent-readiness.md, a scoping review from 2026-08-12 that returned a hard “no-go to start today”: four of six declared C++ dependencies were unstarted (RAG tools, code-index tools, skill loading, skill permissions), one undeclared blocking dependency (embeddings) had no C++ implementation at all, and the security spine the design rested on — refusing skills the C++ runtime can’t honor — didn’t exist in any form. The work on this branch since then pivoted to Python, subclassing the existing ChatAgent (hub/agents/gaia/python/gaia_agent/agent.py → GaiaAgent). That reuses RAG, memory, code-index, and skills wholesale instead of rebuilding them in C++ first. I agree with this pivot — the readiness doc’s blockers were real and would have meant months of prerequisite C++ work before a flagship agent could exist at all. Packaging a Python agent as a standalone binary (this document) is the cost of that choice; it’s a solved problem (§3), not a research problem.

§3 Packaging the Python half: PyInstaller one-file freeze, not a venv bootstrap

Decision (already made, already shipped): hub/agents/gaia/python/packaging/freeze.py freezes the agent’s REST sidecar (server.py) into a single-file PyInstaller executable, one build per target platform (PyInstaller cannot cross-compile).

What was rejected, and why

That same design doc independently reaches the identical SignPath finding §9.2 below does — .signpath/policies/gaia.policy is scoped to artifact_types: [nsis-installer] and would reject a bare executable server-side — which is corroborating, not coincidental: both come from reading the same policy file, not from guessing.

Why the freeze stays small

The freeze’s EXCLUDES list drops torch, transformers, sentence_transformers, scipy, pandas, and the rest of the ML stack. This works only because the agent’s triage/chat/RAG-embedding paths all go over HTTP to Lemonade — nothing in the sidecar’s reachable import graph needs an in-process model. The email agent (the reference this was modeled on) documents its frozen size at ~90 MB with the same exclusion list. Measured, not estimated — updated after §8 below was first drafted: a real --onefile freeze of gaia’s own sidecar was run in this session (uv pip install the freeze venv, then python packaging/freeze.py --onefile, Windows, ~82s build) and produced a 48.9 MB gaia-agent.exe — smaller than the email agent’s ~90 MB, not the ~90–120 MB this section originally guessed. packaging/smoke_test.py against that exact binary passes all three checks (/health → {"status":"ok"}, /version → apiVersion 2.12 / agentVersion 0.1.0, /openapi.json → both /v1/gaia/* paths present) with a 2.6s cold start to first healthy response. This is a Windows-only number — the macOS/Linux freezes were not run here, and PyInstaller’s per-platform packing (compression, DLL vs. dylib/so bundling) means the real number there can differ — so read 48.9 MB as “confirmed on Windows, right order of magnitude elsewhere,” not a cross-platform guarantee. Worth recording the real per-platform numbers in the npm README’s install-size expectations once a release actually cuts all four platforms. If a future change makes the sidecar import anything in EXCLUDES at module load time (not lazily, inside a function), the freeze will silently balloon toward the ~2 GB torch brings — smoke_test.py’s boot check would still pass; only the size would regress. Worth a size assertion in CI if this becomes a real risk (not present today).

§4 The Go TUI: cross-compiled once, consumed twice

The TUI (tui/cmd/gaia) builds cleanly with go build ./cmd/gaia — verified in this session on Windows (see §8). Because it’s pure Go with CGO_ENABLED=0, it cross-compiles all six targets (win-x64, win-arm64, darwin-arm64, darwin-x64, linux-x64, linux-arm64) from a single ubuntu-latest runner in .github/workflows/release_components.yml, publishing them as the terminal-hub hub component. The @amd-gaia/gaia npm package does not rebuild the TUI. It downloads the already-published terminal-hub binary for the user’s platform. This is the “reuse it rather than inventing a parallel path” instruction taken literally: a second build under the gaia agent’s own release lane would be the same bytes at a different version, and the two would eventually drift. release_agent_gaia.yml enforces this at release time — it fetches the pinned terminal-hub version, verifies its SHA-256 against the hub’s own server-side hash (not just its own re-hash), and refuses to publish if terminal-hub isn’t published yet at the pinned version. There is no fallback path that builds a TUI copy of its own. One consequence worth naming: the gaia agent’s release is now gated on a component it doesn’t own. If terminal-hub hasn’t shipped a version yet, gaia’s release fails loudly, naming the missing version — which is exactly what’s happening today (§9).

§5 First-run setup: reused, not reinvented

The TUI’s first-boot gate (tui/internal/ui/chat/setup.go) shells out to gaia init --check --profile gaia (and gaia init --profile gaia --yes if that comes back not-ready) instead of re-deriving GAIA’s setup logic in Go. src/gaia/installer/init_command.py’s INIT_PROFILES["gaia"] is the single source of truth for what “set up” means: the Gemma-4-E4B-it-GGUF chat model, the user.embeddinggemma-300m-GGUF embedder, the [rag] pip extras, and the flagship wheel itself. It is also gaia init’s default, so a bare gaia init and the TUI’s one-key fix do exactly the same thing. The bug this issue specifically flagged — an older, already-installed gaia that predates the --check flag exits 2 (“unrecognized arguments”), and that 2 used to get read as “clean machine, nothing to do” — is already fixed. setup.go pins exit code 1 as the only value meaning “not ready”; anything else (including 2) is treated as “the question wasn’t answered,” which surfaces an error instead of silently skipping setup. There’s a dedicated regression test for this (setup_test.go:267-289).

The Mac / no-Lemonade decision

The constraint as given (“a Mac has no Lemonade NPU/GPU path”) is half right: Mac has no NPU path — the npu init profile is XDNA2-specific, Ryzen AI hardware that doesn’t exist on Apple Silicon. But Lemonade itself does support macOS (the gaia profile has no required_device and no NPU-specific recipe/backend keys; LemonadeInstaller has explicit darwin install/upgrade handling). So the actual decision already made and shipped is:
  • Lemonade is required on every platform, including Mac — for the RAG/memory embedder at minimum, because Anthropic has no embeddings API.
  • The chat model itself is optional, via --use-claude, and so is running setup at all: a Claude launch skips the first-boot gate entirely and never starts the local server. Running an installer that launches Lemonade would defeat the one flag whose purpose is to avoid it, and would hold the first answer behind a multi-minute install. The session says on screen that setup was skipped and names gaia init --skip-chat-model (or /setup) as the way to get the local half.
  • There is no “refuse entirely, Claude-only, no local component” mode. I agree with this — memory and RAG are core to what makes this agent “the flagship,” and neither has a non-local equivalent today. A Claude session simply defers the local half rather than doing without it forever.
Implemented in applyFirstBootGate (tui/internal/ui/chat/setup.go), which returns early in Claude mode; setupCheckArgs/setupRunArgs still branch on claudeMode for the explicit /setup path. The npm README documents Lemonade + the gaia CLI on PATH as requirements for the local-embedding features.

§6 Ports

4001 is refused by name in the npm client (--port 4001 is a hard error, not a warning) — already covers the “never touch port 4001” constraint in this repo’s instructions, independent of anything I added. The sidecar defaults to 8141 on loopback only; nothing here binds a LAN interface.

§7 Where this disagrees with (or extends) the other plans

Desktop Installer Plan — covers the Electron Agent UI installer, a different artifact (a browser-based desktop app, not a terminal agent). I agree with its bootstrap-a-venv approach for that artifact, for the reason in §3: the Agent UI needs the full package surface, so freezing it would mean either an Agent-UI-sized PyInstaller binary (huge — the plan itself estimates the venv bootstrap route at “under 10 minutes, dominated by model download,” implying the app itself is small) or re-deriving the same narrow-slice trick this document uses, which doesn’t work when the whole point is running arbitrary agents. The two installers solving the same problem two different ways is correct here, not drift — flag it if a third distribution channel appears and picks a third approach without a reason. Agent Hub Plan — doesn’t mention binary/sidecar distribution at all (grepped for “binary”, “sidecar”, “npm”, “native” — zero matches). It predates the email agent’s freeze pattern and hasn’t been updated since gaia adopted the same pattern. This is a real doc gap (see §9) — not a design disagreement, just a plan that’s fallen behind the code twice now. docs/guides/hub-publishing.mdx (the guide, current and accurate) — already documents the sidecar pattern correctly: “The email agent goes one step further and ships a frozen native binary plus an npm package… That packaging is currently hand-built per agent; reusable tooling to make any agent an npm sidecar is on the roadmap.” Gaia is now the second hand-built instance of exactly this pattern (freeze.py, gen_binaries_lock.py, publish_to_r2.py, stamp_version.py all exist a second time, adapted per-agent rather than shared). I agree with the guide’s own assessment: two instances is the signal to extract shared tooling, not a third hand-rolled copy. See §9 for the concrete shape that extraction should take.

§8 What I verified vs. what I’m taking on faith

Being explicit here because the instructions ask for it directly, and because most of this system was built by other sessions I can’t fully audit in the time available. Verified in this session, on Windows:
  • go build ./cmd/gaia succeeds cleanly from a fresh clone of this branch (Go 1.26.5). No prior venv, no prior build state.
  • The built binary launches and its --help / --use-claude / --claude-model flags are real and wired (not documentation-only), producing a 25.4 MB binary.
  • The exit-code-1-vs-2 setup bug fix and its regression test exist in the current tree, and pass: go test ./internal/ui/chat/... — 7/7 setup-specific tests plus the rest of the package, all green (setup_test.go:267-289 is the exit-code case).
  • The npm client’s test suite: npm test in hub/agents/gaia/npm — 101 passed, 3 skipped, 0 failed. That’s real coverage of platform resolution, lock parsing, SHA-256 verification (including a tampered-download case that confirms the file is deleted, not just rejected), and lifecycle — against a mocked network layer (see the caveat below).
  • release_agent_gaia.yml and release_components.yml exist, are internally consistent about the two-lane distribution model, and match what docs/guides/gaia.mdx and the npm README.md claim.
  • A fresh editable install of the flagship’s own package — uv pip install -e ".[api]" -e hub/agents/chat/python -e hub/agents/gaia/python pytest — succeeds, produces a working gaia-agent console script with the documented --use-claude/--claude-model/--dev CLI surface, and its full test suite passes: hub/agents/gaia/python/tests — 146 passed. This is complementary to the npm/Go coverage above — it’s the Python side of the “from source” install path in docs/guides/gaia.mdx, not previously exercised in this document. (Caveat on the freeze-venv cost claim above: this install pulled far less than “several GB” — a warm uv cache on this machine from prior GAIA work likely explains the gap, not a correction to the underlying claim about what a genuinely cold cache costs.)
  • The actual PyInstaller freeze of the gaia sidecar. Run to completion in this session: uv pip install --python .venv/Scripts/python.exe -e ".[api]" -e hub/agents/chat/python -e hub/agents/gaia/python pyinstaller (fast here — a warm uv cache from the installs above, not the “several GB, cold” case described below), then python packaging/freeze.py --onefile — 81.6s, produced gaia-agent.exe. python packaging/smoke_test.py packaging/dist/gaia-agent.exe then launched that exact binary as a subprocess (no interpreter available to it) and passed all three checks. Real, measured, not estimated — see §3’s updated size section. This supersedes the “not verified” framing this bullet originally had.
Not verified — taken on the strength of the code + its test suite, not run end-to-end:
  • macOS and Linux. I have no access to either platform from this session. The CI matrix (windows-latest, macos-14 for arm64, ubuntu-latest, macos-26-intel best-effort) is the only evidence either builds; I did not run it. A cold-cache freeze venv on those platforms could still cost the “several GB” this section originally warned about — the warm-cache result above is Windows-specific and machine-specific, not proof the cost claim was wrong in general.
  • A live npx @amd-gaia/gaia run. Can’t — nothing is published yet (§9). The 4,100+ lines of Go/Python/TypeScript unit and integration tests are real coverage of the logic (platform resolution, lock parsing, SHA-256 verification, lifecycle, the exit-code bug), but they mock the network layer; nothing here has proven the end-to-end download-and-launch path against the live hub.

§9 Actual gaps

9.1 Nothing is released yet

binaries.lock.json still carries PENDING-replace-with-real-sha256 placeholders for every platform — both the sidecar lane and the tui lane. The npm fetcher refuses to download against a placeholder (that’s by design — an unverifiable binary should never be fetchable). Until someone with the agent-publish GitHub environment’s approval rights tags agent-pkg-gaia-v0.1.0 (which needs terminal-hub published first — §4), npx @amd-gaia/gaia doesn’t work for anyone. docs/guides/gaia.mdx already says this plainly (“Coming Soon… Use the source path in the meantime”) — that part of the docs is accurate, not a gap.

9.2 Signing — real, unaddressed, and worse here than in the Electron path

Grepped every workflow and installer asset directory for signing references. Findings:
  • The Electron/NSIS/DMG path (.github/workflows/build-installers.yml) has actual signing scaffolding — SignPath for Windows (gated on SIGNPATH_API_TOKEN, explicitly noted as landing in “Phase H,” not yet wired), Apple Developer ID + notarization for macOS (gated on APPLE_ID secrets), and it degrades gracefully to an ad-hoc-signed, unsigned build when secrets are absent — which is what every build produces today, since none of us has the certificates. There’s a real, committed SignPath policy backing this: .signpath/policies/ gaia.policy — OSS Foundation tier, approval.mode: automatic, certificate issued by the SignPath Foundation CA at no cost. But it’s scoped narrowly: artifact_types: [nsis-installer], and trusted_build_systems only accepts requests from build-installers.yml on a refs/tags/v* push. A bare .exe submitted from anywhere else — including gaia-agent.exe or gaia-tui.exe from release_agent_gaia.yml — would be rejected by this policy as written, not silently signed.
  • release_agent_gaia.yml has no signing step of any kind, and couldn’t use the existing policy even if it tried to (see above). The sidecar .exe and the terminal-hub binaries ship as raw executables with no installer wrapper and no code signature — not even the ad-hoc/degrade-gracefully pattern the Electron path has. This is a real gap, not a “handled elsewhere” situation.
What this means for users: Windows SmartScreen will flag gaia-agent.exe and gaia-tui.exe as from an unrecognized publisher on first run — a “Windows protected your PC” interstitial the user has to click through (“More info” → “Run anyway”). macOS Gatekeeper will refuse to open an unsigned/unnotarized binary at all without an explicit right-click → Open, or a xattr -d com.apple.quarantine workaround, which is a materially worse experience than a click-through warning. What I’m not pretending: this artifact is not ready for a general, non-technical audience while this is true. It’s fine for developers and early adopters who understand what SmartScreen/Gatekeeper prompts mean. Fixing it needs, in order of who can actually do it:
  1. Windows is mostly wiring, not procurement — AMD already has a working, free SignPath OSS setup (.signpath/policies/gaia.policy); no certificate purchase or new vendor relationship is needed. What’s missing: either (a) extend that policy’s artifact_types (or add a second .signpath/policies/*.policy) to accept a bare .exe from release_agent_gaia.yml’s tag pattern, or (b) wrap the sidecar/TUI in an NSIS installer so it fits the existing nsis-installer scope. (a) is less work and matches what these binaries actually are.
  2. An Apple Developer ID application certificate + notarization credentials (APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID) — this one is an org-level cost/ownership decision (Apple Developer Program membership), not something a coding session can provision, and nothing in this repo has it yet for either installer path.
  3. Once either is available, release_agent_gaia.yml’s freeze steps need a codesign/ signtool step per platform before the SHA-256-and-publish step — the hash has to be computed on the signed binary, or the lock ships a hash for bytes nobody actually gets (the CI already computes SHA-256 after freeze, so this is an insertion point, not a redesign).
I did not attempt to add unsigned scaffolding for this — the Electron path shows what that looks like, and duplicating unsigned scaffolding into a second workflow adds maintenance surface without solving anything until real certificates exist.

9.3 Docs drift

Two docs are stale relative to the code and are fixed as part of this change:
  • docs/guides/terminal-hub.mdx said “The hub is a Go binary and is not yet distributed — no release carries a gaia-<os>-<arch> asset… Building from source is the only way to get it.” That’s no longer accurate — terminal-hub is a fully-built six-platform release lane in release_components.yml; it just hasn’t been tagged yet (§9.1), which is a different claim. Corrected to say that.
  • docs/guides/hub-publishing.mdx’s “Standalone sidecars” section named only the email agent as the sidecar+npm precedent. Added gaia as the second instance, matching §7’s “extract shared tooling” note.
  • docs/reference/cli.mdx’s gaia init section never mentioned the flagship agent or gaia-tui at all — a reader following the CLI reference alone would have no path from gaia init to actually getting the flagship running. Added a short cross-reference to /guides/gaia and this plan.

9.3.1 gaia-tui --version was broken — fixed as part of this change

Found while re-verifying §8’s binary build: rootCmd.Version was never set in tui/internal/cli/root.go/version.go, so cobra never registered the --version flag at all (gaia-tui --version exited 1, unknown flag: --version, while the version subcommand worked fine). This is unrelated to anything else in this document — a pre-existing bug this session’s own build-and-run verification (§8) happened to surface, not something introduced by the packaging work above. Fixed in tui/internal/cli/version.go: rootCmd.Version is now set from the same build-time-injected version variable build_tui.yml already stamps via -ldflags -X, with a matching version template so --version and version print byte-identical output. The version subcommand’s Run now writes through cmd.OutOrStdout() instead of a bare fmt.Print, which was hiding its own output from anything that redirects cmd.SetOut — including the new test. Verified: rebuilt the TUI (go build ./cmd/gaia, then again with the same -ldflags -X ...version=... build_tui.yml uses), confirmed gaia-tui --version and gaia-tui version now print the same line, added TestVersionFlagIsRegistered/TestVersionFlagPrintsVersion/ TestVersionSubcommandMatchesFlag to tui/internal/cli/root_test.go (alongside the pre-existing TestBinaryName* tests already in that file), and ran the full tui/ suite — every package except ./test (the slower integration suite that spawns real subprocesses) — all green, go vet ./... clean.

9.4 The hub manifest doesn’t need a schema change — the generalization gap is tooling, not schema

Direct answer to “does the hub need a change to express a downloadable executable”: no schema change is the smallest fix, because the mechanism this issue asked for already exists twice (email, now gaia) as a hand-built sibling to the standard Python-wheel hub manifest — a separate npm package with its own binaries.lock.json, outside gaia-agent.yaml entirely. gaia-agent.yaml’s interfaces: block (tui/cli/pipe/api_server) already tells a reader this agent runs as more than a plain importable Python package; that’s sufficient today for the two agents that need it. What is missing, and what hub-publishing.mdx already names as “on the roadmap”: shared tooling so a third agent doesn’t hand-write a third copy of freeze.py/gen_binaries_lock.json-generator/publish_to_r2.py/stamp_version.py. With two real instances now built, the shape of that extraction is visible — a gaia-agent-hub-sidecar-tools (or similar) package providing the freeze harness, lock-file schema + validator, and publish/verify steps as library calls each agent’s packaging/ directory imports and configures (entry module, exclude-list, add-data), rather than forking the whole file. That’s future work, out of scope for this issue, and I’m naming it rather than starting it so it doesn’t get lost.

§10 What’s actually left to do (post-plan)

  1. Fix the doc-drift issues in §9.3, and the --version bug in §9.3.1 — done as part of this change.
  2. Cut the terminal-hub release (tag the core release; release_components.yml publishes it) — not something this session can do; needs a maintainer with tag and agent-publish-environment access.
  3. Cut agent-pkg-gaia-v0.1.0 once terminal-hub is live — same access requirement.
  4. Signing (§9.2), split by how much work remains:
    • Windows: extend .signpath/policies/gaia.policy’s artifact_types (or add a sibling policy) to accept the sidecar/TUI binaries and wire a signing step into release_agent_gaia.yml — an engineering task against infrastructure that already exists, not a procurement one.
    • macOS: still needs an Apple Developer ID + notarization credentials provisioned — that part is genuinely an org-level decision, not something to start from a coding session.
  5. Add the measured sizes to the npm README’s install-size expectations (not currently stated anywhere). §3/§8 now have a real Windows number (48.9 MB); macOS and Linux still need their own real freeze to confirm the same order of magnitude holds — that will happen automatically on the next release attempt’s freeze matrix.