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

# Shared bundle builds

<Info>
  Commands, package names, and image names on this page come from the open-source project that Mibyan Desktop is built on, and can differ from the Mibyan Desktop installer. For the supported Mibyan install and update path, see [Install and update](/products/desktop-guide/install-and-update).
</Info>

Mibyan separates dependency preparation, product builds, and distribution
packaging. The compiler and agent-assembly interfaces live in
[`scripts/build/README.md`](https://github.com/NousResearch/hermes-agent/blob/main/scripts/build/README.md). These are current
interfaces, not proof that every distribution passed native acceptance.

## Providers, products, and distributions

| Layer | Responsibility | Implementation |
| - | - | - |
| Dependency providers | Prepare tools, Python environments, JavaScript dependencies, and native bindings | PM (`pm.build_environment`), `scripts/build/node-deps.mjs`, Nix, and Termux |
| Product builders | Compile icons/TUI/web/desktop UI or assemble a runnable agent from prepared inputs | `scripts/generate_icons.py` and `scripts/build/` |
| Distribution adapters | Select products and package them for their target | `scripts/bundles/`, `Dockerfile`, `nix/`, and `scripts/termux/` |

Providers retain their package managers and locks. Product builders do not
install missing dependencies or download inputs. The Node dependency provider
uses one locked workspace union. Nix supplies its own npm and Python inputs.
Termux supplies bionic tools and wheels. Shared recipes do not imply identical
bytes, dependency selections, or Python environments across targets.

| Product | Implementation | Inputs |
| - | - | - |
| Icons | `scripts/generate_icons.py` | Artwork and a Python with the runtime dependencies |
| TUI | `scripts/build/tui.mjs` | Prepared TUI workspace |
| Dashboard | `scripts/build/web.mjs` | Prepared web workspace and generated icons |
| Desktop UI | `scripts/build/desktop.mjs` | Prepared desktop workspace, icons, stamp, and native bindings |
| Runnable agent | `scripts/build/agent.py` | Code, interpreter, dependencies, independent PM runtime, resources, and selected frontends |

The desktop UI compiler does not depend on the dashboard. A bundled desktop
selects an agent with TUI and dashboard products. A light desktop does not
select an agent payload.

## Build and packaging entrypoints

Source updates and source UI launches use the same dependency provider and
product builders. `mibyan_cli/source_build.py` selects the workspace union,
prepares it once, then invokes the shared recipes. An update builds TUI and
web, plus the local desktop app when one was present before the update.
Desktop packaging still belongs to the local desktop adapter.

The current-checkout retry, pulled-checkout, and ZIP update paths all use
`update_cmd_maint._prepare_updated_checkout`: one PM dependency sync, then a
fresh process on the selected interpreter for the frontend builds. Build
failure aborts completion; an existing stale product is not a successful
update. There is no updater-specific npm cache, fallback install, extra
refresh, or memory-provider reinstall. PM owns the complete Python union.

Build a desktop distribution from a clean checkout at its release tag. Start
with a host Python that can run the entrypoint and Git; preparation acquires the
pinned Python, Node and npm through PM. Native compilers/SDKs remain host inputs:

```sh theme={null}
python -m scripts.bundles.desktop --tag=vX.Y.Z
```

Without `--prepared`, `desktop.py` requires exactly one of `--tag` or `--commit`. Commit builds
require the full commit SHA and matching checkout HEAD. `--variant` accepts
`bundled`, `store`, or `light`, with `bundled` as the default. `--repo` selects
the checkout. Arguments after `--` go to the prepared Electron Builder wrapper;
they cannot replace the admitted target, tools, output or packaging configuration.

The driver first prepares the complete dependency set: managed tools, the locked
Node workspace union, icon environment, Electron-native bindings, packaging
utilities, and (except for light) the application and independent PM environments.
Only then does it compile products, assemble the payload, and package Electron.
MSIX metadata, signing, notarization, and package formats remain adapter work.

### Split desktop preparation and consumption

The one-command build and CI use the same preparation operation. To stop before
product compilation and packaging:

```sh theme={null}
python scripts/bundles/desktop.py --tag vX.Y.Z --variant bundled --prepare-only \
  --work "$PWD/.build/desktop-job" --cache "$PWD/.cache/desktop-inputs"
python scripts/bundles/desktop.py --prepared "$PWD/.build/desktop-job/prepared.json"
```

`--work` and `--cache` are separate, build-owned directories. Preparation claims
the work directory itself; do not create it beforehand. It writes `prepared.json`
last, after every provider succeeds. That file contains this job's absolute paths
and source identity, not a portable cache receipt. Reprepare after moving a
checkout, changing source or locks, or losing an input. Repeated preparation may
reuse admitted dependency bytes while rebuilding path-bound environments.

`--prepared` does not accept new tag/commit/work/cache arguments. It validates
the clean checkout and prepared inputs, then fails on stale or missing inputs
instead of installing them. Bundled and Store builds may consume the same
preparation for a stable tag; light and commit builds cannot switch to Store.
Product builds still run each time. Native staging exposes `prepare_native` and
`finish_native` to this composition; `mibyan pm bundle` remains a complete
native staging command, not the desktop preparation interface.

Windows and macOS release jobs use **restore → prepare → save → build**. The
cache action derives provider paths and transports candidates; it neither
creates the preparation workdir nor declares a cache hit valid. Saves run after
successful preparation, before product compilation or signing. Failed
preparation leaves completed provider data locally but does not save an overall
snapshot. The general `setup-pm` action remains available for other workflows.

Native wheel caches use compact 128-bit partitions under `python/runtime`, keyed
by the target and observed compiler/SDK inputs. Missing identities still get a
fresh partition. uv builds source distributions inside this cache, so partition
names must leave room for nested compiler outputs below Windows `MAX_PATH`.
Keep a custom `--cache` root short as well; enabling OS long paths does not make
every native compiler long-path aware. This layout retains the shared cache
transport and all target-compatible extras, including Silk.

The strict boundary forbids dependency acquisition during consumption, not all
network access: signing, timestamping, notarization and publication retain their
online responsibilities. A network-denied unsigned native build is still needed
to prove the boundary on each release target.

Stage a native agent with both frontend products, without Electron packaging:

```sh theme={null}
python -m scripts.bundles.stage --out /work/agent-payload --ref HEAD
```

This command prepares and builds TUI/web products if none are supplied. It
also accepts `--tui` and `--web` together as prepared product roots. It rejects
a selection with only one of those arguments. The standalone products have
explicit input/output contracts in the builder README.

`--ref` selects the agent source snapshot. Automatic frontend compilation uses
a temporary snapshot of that same revision. Explicit frontend products must
come from the selected revision.

`mibyan pm bundle --out /work/agent-payload --ref HEAD` stages native tools,
the application environment, and the agent. That PM command does not build
frontends. `npm run payload --workspace apps/desktop` uses the stage driver
that includes them. Neither command creates an Electron installer.

### Split PM Bundle preparation and assembly

PM Bundle uses the same isolated tool bootstrap and native preparation slice,
without installing desktop workspaces, icons, Electron bindings or packagers:

```sh theme={null}
python -S -B scripts/bundles/native_build.py --source "$PWD" \
  --work "$PWD/.build/payload-job" --cache "$PWD/.cache/payload-inputs" \
  --out "$PWD/build/agent-payload" --ref HEAD --prepare-only
python -S -B scripts/bundles/native_build.py \
  --prepared "$PWD/build/agent-payload.prepared.json"
```

Use a clean checkout at the selected revision. `--ref` defaults to `HEAD`;
`--commit` accepts an exact full SHA instead. Work, cache and output must be
separate; preparation claims a previously absent work directory. Without
`--prepare-only`, the driver also assembles. `--prepared` takes no new selection
or path arguments and validates the native receipt before assembly; it never
bootstraps or repairs dependencies. The receipt and lock are output siblings
(`agent-payload.prepared.json`, `agent-payload.prepare.lock`), not shipped files.
Assembly deliberately supplies no frontend products, as with `mibyan pm bundle`.

The `payload-test` producer on the existing cache action selects only `tools`,
`python/runtime` and `native`. It excludes source `node_modules`, npm caches,
icon environments and packagers. PM's managed tool bootstrap still includes Node
and npm, but does not run desktop `npm ci`. Native compiler identity and Windows
ARM64 prerequisites use the shared owner once. Cache hits always undergo provider
admission; native SDK/compiler prerequisites still belong to the host. PR jobs
only restore; saves require successful preparation on the default-branch trusted
lane with no alternate source ref. No signing or publication is added.

For Termux, tools and the wheelhouse are prerequisites:

```sh theme={null}
python scripts/termux/build.py --repo /work/source --payload /work/termux-payload \
  --out /work/packages --tag vX.Y.Z
```

The Termux driver accepts exactly one of `--tag` or `--commit`. It prepares
the TUI workspace, calls the shared TUI builder, and passes that product to
`build_deb.sh`. It does not add the dashboard or replace bionic preparation.

The CLI contracts are in `scripts/bundles/desktop.py`,
`scripts/bundles/stage.py`, `pm/cli.py`, and `scripts/termux/build.py`.

## Shared agent and launcher contract

`AgentInputs` supplies explicit paths and a placement mode:

* `contained` keeps runtime inputs inside the native payload.
* `fixed` retains the Docker or Termux installation prefix.
* `references` keeps installed code and dependencies in separate Nix store paths.

The assembler derives console commands from `[project.scripts]`. It copies
source-layout resources and selected frontends, or links explicit reference
inputs. It writes the completion manifest after structural assembly. Nix also
receives a command/environment map for its native wrappers.

The launcher implementation lives in `scripts/build/launchers.py`.
`launcher_wrapper.py` and `mint_launchers.py` live in that same directory.
`scripts/bundles/payload.py` owns git snapshots, PM tool facts, PM runtime
sealing, and portable link handling. It no longer owns frontend placement or
launcher generation.

The desktop stamp carries the declared launch paths. Electron consumes that
contract without payload adoption or repair. Non-bundled builds carry no
placeholder agent payload. Store packages declare `microsoft-store` as their
update mechanism. Sideload bundles declare `app-installer`.

Windows launcher minting runs the payload interpreter. POSIX launchers use
explicit interpreter, source, and dependency paths. Structural assembly does
not replace target-native launch tests. A manifest alone does not prove that
a relocated or signed artifact runs.

## Independent PM runtime

PM dependencies come from `pm/pyproject.toml` and `pm/uv.lock`, not the
application dependency graph. The application environment and `pm-runtime`
are separate inputs to agent assembly.

Native and Docker preparation use `pm.runtime_stage.stage_runtime`. Termux
uses that function with its offline wheelhouse. Nix uses the independent
`nix/pm-runtime.nix` derivation. Native sealing records the payload interpreter
and PM-only site directory in `pm-runtime.json`.

Resident PM workers run the declared interpreter with `-I -S -B`. They add
only the declared PM dependency directory before the worker script. They do
not borrow application dependencies or add PM packages to the caller's
interpreter. Docker and Nix stamps point to their declared PM runtime.
These contracts live in `pm/runtime.py:55–88,171–182` and
`scripts/bundles/payload.py:58–90`.

Ordinary native staging does not scan user plugin trees. It runs provisioning
in a child with temporary home and PM roots plus an explicit persistent build
cache. Windows ARM64 prerequisite preparation runs before HOME isolation and
uses the same provider as source setup and CI. Live PM retains plugin
admission, generation selection, and transaction state. Both paths use
`pm.environment.PythonEnvironment` for explicit uv environment construction.

## Distribution boundaries and output paths

| Distribution | Selected products | Current output layout |
| - | - | - |
| Desktop bundled/Store | Icons, TUI, web, agent, desktop UI | Products: `apps/desktop/build/products/`. Agent: `apps/desktop/build/agent-payload/`. UI: `apps/desktop/dist/`. Packages: `apps/desktop/release/` |
| Desktop light | Icons and desktop UI | `apps/desktop/dist/` and `apps/desktop/release/`, without an agent payload |
| Docker | Icons, TUI, web, agent | Agent at `/opt/mibyan`, dependencies at `.venv`, PM at `pm-runtime`, generated commands at `libexec` |
| Nix TUI | TUI | `$out/lib/mibyan-tui/{dist/entry.js,package.json}` |
| Nix web | Web | `$out/index.html` and assets |
| Nix agent | Agent with TUI/web references | `$out/bin`, `$out/share/mibyan-agent`, `$out/ui-tui`, `manifest.json`, and `command-map.json` |
| Nix desktop | Icons and desktop UI, with the Nix agent | `$out/share/mibyan-desktop` and `$out/bin/mibyan-desktop` |
| Termux | TUI and agent | `OUT/mibyan-agent_<version>_aarch64.deb`, installed at `$PREFIX/lib/mibyan-agent/` |

Native payloads contain `mibyan-agent`, `tools`, `venv`, `pm-runtime`, `bin`,
`uv-cache`, and their manifests/facts. Copied frontend assets live at
`mibyan-agent/mibyan_cli/tui_dist/` and `mibyan-agent/mibyan_cli/web_dist/`.
The TUI asset directory contains `entry.js` and module-mode package metadata.

Docker keeps its existing TUI product at `/opt/mibyan/ui-tui` and web output
at `/opt/mibyan/mibyan_cli/web_dist`. The assembler also plants its shared TUI
asset layout beneath `mibyan_cli/tui_dist`. Venv command symlinks preserve the
paths used by s6 and the privilege-drop shim.

The Docker frontend stage owns npm dependencies and compilation. The runtime
stage copies only the frontend products and the locked TypeScript package for
runtime linting. Node/npm and Photon's separately installed sidecar dependencies
remain runtime inputs. Python dependency preparation precedes the application
source copy. Actual cache-hit and layer-size claims require build evidence.

Nix retains `importNpmLock`, uv2nix, platform overrides, and store references.
Its builders run during derivation builds, not evaluation. Per-product source
filters keep frontend, Python, and resource inputs separate. Nix wrappers
consume the assembler's command map and retain their PATH and extra-Python
collision policies.

**Nix wheel policy:** `nix/python.nix:128–135` sets `mibyan_NIX_BUILD=1` only
for the Mibyan derivation. `setup.py:34–72` rejects general Mibyan wheel/sdist
builds. The shared assembler uses the installed Nix code without another copy.
Other providers use source-layout code and metadata, not a public Mibyan wheel.

Termux retains bionic wheel compilation, offline installation, native library
paths, and its fixed prefix. Its installed root contains `app`, `tools`,
`runtime-libs`, `venv`, `pm-runtime`, `bin`, and manifests/facts. APT maintainer
hooks manage the declared CLI symlinks under `$PREFIX/bin` and refuse foreign
conflicts. They do not compile dependencies during installation.

`stage_apt_repo.py` owns repository metadata and signatures. Stable and canary
suites are `mibyan-stable` and `mibyan-canary`. Package files publish before
signed metadata. [Stable release admission](/desktop/developer-guide/stable-releases) coordinates
acceptance and publication across distributions.

## Public artifact handoffs

The desktop release workflow downloads and installs each supported bundled
format on a fresh native runner, then runs the same composer/provider/reply
check as install-e2e. Windows universal assembly stages bytes before the smoke;
its canary feed is published separately only after both native smoke matrices
pass. macOS feed publication and stable candidate acceptance are likewise gated.
See [install and chat acceptance](https://github.com/NousResearch/hermes-agent/blob/main/tests/install/README.md#post-build-artifact-smoke)
for covered formats, checkpoint evidence and the Store/Linux/no-upload limits.

`scripts.releases.handoff fetch --public-base URL` downloads a staged artifact
without R2 credentials. Supply either `--tag TAG --commit SHA` or
`--commit-build SHA`, plus the producer `--name`, destination `--root`, and
optional `--include` selectors. This uses the same receipt identity, path,
size and SHA-256 checks as authenticated handoffs. Tagged and commit-only
archives remain separate; the command does not resolve a mutable latest feed.

Public reads require HTTPS, except for loopback fixture servers. They reject
URL credentials, unsafe paths and redirects. An incomplete or corrupt download
cannot replace an existing verified destination. Receipt files are saved only
after all selected artifacts verify. Selection of one desktop format must still
reject missing or ambiguous matches before installation.

## Cache ownership

PM binary download archives and the native uv wheel cache have different
consumers. They are not interchangeable cleanup targets.

* PM retains completed downloads until package verification and publication.
  Successful installs remove their exact fetch archives. Failures retain
  downloads for retry. Cleanup leaves unrelated partials alone.
* Native staging prunes obsolete entries only in its build-owned tool store.
  It does not prune the user's machine-wide store.
* Native staging retains `uv-cache/` for offline mutable-environment rebuilds.
  It copies extracted wheels and index/revision metadata, but excludes redundant
  wheel ZIPs and cached sdist `src/` trees (including Rust `target/` outputs)
  before copying. The provider cache is unchanged; native signing reaches the
  extracted binaries without scanning build-only artifacts.
* PM-runtime and application builds share the provider's persistent uv cache.
  CI restores/saves that cache, not the temporary build HOME. Failed builds
  retain completed wheels. General PM staging uses its v2 cache namespace;
  desktop preparation uses its own input snapshot namespace.
  Plain `uv cache prune` removes dangling entries without discarding offline
  wheel inputs. It does not remove all historical versions or enforce a size cap.
* Source builds render icons on their runtime interpreter. Desktop preparation
  and product staging, which have none, prepare the locked runtime dependencies
  (without the application) under the job workdir; desktop's wheel cache is
  `CACHE/python/build`. That environment does not enter the shipped runtime.
* Frontend `node_modules` is a provider input, not a frontend product.
  Docker's runtime TypeScript and Photon selections are separate exceptions.

The native cache behavior is in `scripts/bundles/native.py`.
Removing all caches from a payload can break offline environment reconstruction.

Desktop transport selects PM tool entries, Python download/wheel caches, npm's
content-addressed downloads, prepared workspace dependencies, and native/package
tool inputs through `scripts/ci/desktop_build_cache.py`. It does not select the
workdir, virtual environments, live user home, signing tokens, or products.
The signature-result cache is separate. Native wheel partitions depend on
observed compiler/SDK/OpenSSL identity; incomplete identity deliberately forgoes
warm reuse. This is not a fully pinned host SDK.

Cache keys are lookup hints, not authorization. Native builders have separate
release and commit execution jobs with literal `cache-mode: write` and
`cache-mode: read`, respectively. GitHub enforces these permissions on scoped
cache tokens, so commit builds cannot save caches even from their own scripts.
Both branches share their matrix, environment, and steps through YAML anchors.
The original `build-win32` and `build-darwin` IDs aggregate the branches: after
successful validation and input archiving, exactly the selected branch must
succeed and the other must be skipped. A failed, cancelled, or unexpectedly
skipped selected build fails the aggregate; it cannot permit publication.
Existing publication dependencies keep using those IDs. A skipped save or a
different key prefix alone cannot prevent a script from poisoning a writer
namespace.
Default-branch canary dispatch retains default-branch cache scope; changing a
checkout SHA does not change the workflow ref's cache scope. See GitHub's
[cache access contract](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#controlling-cache-access-with-cache-mode).

## Pinned binary inputs

`python -m scripts.ci.archive_inputs` preserves every HTTPS artifact in
`pm/lock.json`, across all targets, plus the Termux runtime-library and license
pins. `pm/artifact-mirror.json` owns the public mirror location. Object keys
are `upstream/sha256/HASH`, independent of filenames and release tags.

CI requests R2 first. Only 404 permits an upstream download. It verifies the
existing SHA256 before an immutable `If-None-Match: *` upload, then downloads
and verifies the stored object. Corrupt bytes, denied access, and failed
uploads stop the build. Concurrent writers may reuse identical bytes but
cannot replace an existing object.

The all-target workflow runs on pin changes on main, manual dispatch, and as
an admitted release prerequisite. It uses runner Python before the pinned
toolchain is available. Desktop jobs depend on that prerequisite and then let PM
fetch verified inputs without archival write credentials; they do not need a
second cache-seeding lane. Other protected workflows can opt into archival
through `setup-pm`'s `archive-inputs` input. Targeted archival can seed disposable
fetch entries with `--target` and `--store`; Termux also passes `--payload` for
runtime libraries. Cache hits do not skip preservation. Desktop R2 credentials
are scoped to upload steps. Untrusted PR jobs receive no publication credentials.

Installed PM clients, bootstrap installers, and Nix pin consumers use the
primary URL followed by the public mirror if the download is unavailable.
The pinned hash remains binding; no credentials or uploads are needed by
clients. PM keeps per-source resume state and reports attempted URLs.
For Termux files already removed upstream, the CI publisher can recover the
exact bytes from the community Internet Archive after an upstream 404/410.
It never repins to the latest package.

The archive must have no expiration lifecycle rule. Release pruning does
not cover its prefix. This covers PM binary pins and Termux runtime inputs,
not unpinned apt packages, OCI images, language-package registries, or native
Electron/SDK archives independently downloaded by their build tools.

## Verification boundary

The checks below distinguish product tests from distribution acceptance.
Signed installers and Android device execution require their native runners.

The macOS dmgbuild acquisition owner is part of packaging preparation, not the
strict build. Native DMG creation, detach diagnostics, signing and notarization
must be exercised after tool-provider changes. Windows x64/ARM64 and
macOS x64/ARM64 cold/warm and signed-artifact acceptance remain separate from
Linux helper tests. Linux desktop release lanes remain disabled.

Focused helper tests cannot establish all of these requirements:

* Offline frontend compilation from prepared immutable inputs.
* Standalone TUI interaction and dashboard backend/assets behavior.
* Native Electron bindings and final signed launchers on each target.
* PM isolation after native payload relocation.
* Retained-cache offline reconstruction of a mutable application environment.
* Docker non-root CLI/TUI/web/browser behavior and every image layer's contents.
* Actual Nix package execution through reference-based wrappers.
* Fresh network-disabled Termux package installation and Android device behavior.

Use the existing acceptance checks in `tests/install/BUNDLED_UPDATES.md`,
`tests/docker/`, Nix checks, and `scripts/termux/check_deb.sh` plus
`validate_installed.py`. Container acceptance does not substitute for Android
device acceptance. No build-pass claim follows from this document.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.