# Don't Just Run Locally: A Hands-on Guide to Packaging Anna Executa as a Releasable Binary

**URL:** <https://forum.anna.partners/t/dont-just-run-locally-a-hands-on-guide-to-packaging-anna-executa-as-a-releasable-binary/140>\
**Category:** Developers\
**Created:** [June 19, 2026, 5:37pm UTC](https://forum.anna.partners/t/dont-just-run-locally-a-hands-on-guide-to-packaging-anna-executa-as-a-releasable-binary/140 "2026-06-19T17:37:54Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

**Author:** ![HappyLight](https://avatars.discourse-cdn.com/v4/letter/h/7feea3/32.png) [@HappyLight](https://forum.anna.partners/u/HappyLight)\
**Post date:** [June 19, 2026, 5:37pm UTC](https://forum.anna.partners/t/dont-just-run-locally-a-hands-on-guide-to-packaging-anna-executa-as-a-releasable-binary/140/1 "2026-06-19T17:37:54Z")

</div>

In the previous beginner tutorial ([From Zero to Your First Anna App: A Hands-On Beginner’s Guide](https://forum.anna.partners/t/from-zero-to-your-first-anna-app-a-hands-on-beginners-guide/117)), we walked through the official `anna-app-llm-demo`: starting Anna App, opening the UI, calling the platform LLM, and seeing another interesting path: instead of the frontend calling `anna.llm.complete(...)` directly, it invokes reverse-RPC through an Executa Tool and lets the host complete the LLM call.

However, the previous article mainly answered one question: **how does this App run, and why can it run?**

There is another question closer to real-world distribution that we did not expand on at the time:

**If I have written an Executa Tool, do users also need to install Python, install uv, pull the source code, and then use it?**

Of course not.

For formal distribution, the recommended approach is to package the Executa as a binary file. This way, users do not need to care about the Python environment, manually install dependencies, or understand your source directory structure. Anna Agent will automatically select the binary package for the current system, download it, extract it, start it, and communicate with it through the standard stdio JSON-RPC protocol.

This tutorial fills in the part that the previous article did not cover in detail. We will use this Executa from the official repository as the example:

```Plain
examples/anna-app-llm-demo/executas/llm-via-executa-python

```

Step by step, we will turn it from a locally runnable Python Executa into a binary distribution package that can be placed in a GitHub Release, written into `binary_urls`, and installed by Anna Agent.

If the goal of the previous tutorial was “first get Anna App running”, then the goal of this one is: **let your Executa leave your development machine and become a Tool that can truly be published and installed.**

## Chapter 1: First Understand the Goal: What Exactly Are We Packaging as a Binary?

Before we start typing packaging commands, let’s not rush to install PyInstaller.

Many binary packaging tutorials begin by telling you to run:

```Bash
pyinstaller --onefile ...

```

But if you have not yet figured out “what this command is supposed to package”, “who will download the packaged file”, and “where Anna Agent will learn about this file”, the rest of the workflow easily turns into blind copying.

Copying can certainly get you through one run temporarily. But once you change a `tool_id`, switch platforms, or mistype a file name in GitHub Release, it becomes hard to tell whether the problem is in building, packaging, uploading, or `executa.json`.

So in this first chapter, we will clarify the goal.

### The Object We Are Packaging This Time

This tutorial packages the following Executa from the official example:

```Plain
examples/anna-app-llm-demo/executas/llm-via-executa-python

```

After entering this directory, you will see several key files:

```Plain
llm-via-executa-python/
├── README.md
├── executa.json
├── llm_via_executa_plugin.py
├── pyproject.toml
└── uv.lock

```

The most important ones are these three:

- `llm_via_executa_plugin.py`: the actual Executa Tool implementation. It handles JSON-RPC requests and calls host sampling from inside the tool.

- `pyproject.toml`: the Python project configuration. It declares the project name, version, dependencies, and command-line entry point.

- `executa.json`: the Executa metadata read by Anna development tools. It contains the `tool_id`, the runtime type used during development, and the distribution configuration used for publishing.

In other words, we are not packaging the entire `anna-app-llm-demo`, and we are not packaging the frontend page. This tutorial focuses on one goal only: **package the Python Executa Tool `llm-via-executa-python` as a binary file.**

### How Does It Run During Local Development?

During local development, this Executa is not started by the user double-clicking it, nor is it imported directly by the frontend.

When you run the local development command in the `examples/anna-app-llm-demo` directory, `anna-app dev` scans the project’s `executas/` directory. It finds:

```Plain
executas/llm-via-executa-python/executa.json

```

Then it registers this Python Executa into the local development harness based on that file’s configuration. When the frontend invokes the tool, the host starts and calls it through stdio JSON-RPC.

The rough flow can be understood like this:

```Plain
Anna App UI
  │
  │ anna.tools.invoke(...)
  ▼
Anna host / local dev harness
  │
  │ stdio JSON-RPC
  ▼
llm-via-executa-python
  │
  │ sampling/createMessage
  ▼
Host LLM

```

There is a crucial point here: this Executa is still an independent process. Anna host communicates with it through stdio JSON-RPC, rather than importing it directly as a Python function.

This is also why it can be packaged as a binary. As long as the packaged file can still read stdin, write stdout, and implement the same JSON-RPC protocol after startup, Anna Agent does not care whether it was originally written in Python, Node.js, Go, or another language.

### Why Not Stop at Local Running?

The local running mode is suitable for developers, but not for final distribution.

A developer’s machine can have Python, `uv`, the source directory, and can tolerate installing dependencies on the first run. But when real users install a Tool, they expect a different experience:

```Plain
Click install
Wait for download
Start using it

```

They should not need to know:

- whether this Tool is written in Python;

- which Python version should be installed;

- whether `uv` exists locally;

- whether dependencies are installed completely;

- whether the current shell’s `PATH` is configured correctly.

If we package the Executa as a binary, these problems can be solved ahead of time during publishing. Anna Agent on the user’s machine only needs to do one thing: download the binary package for the current platform, then start the executable inside it.

This is the value of `binary` distribution.

### What Does the Final Packaged Output Look Like?

After packaging, what we ultimately want is not “an executable file placed somewhere locally”, but a set of distribution packages that Anna Agent can download, recognize, extract, and start.

For this example, the final artifacts usually look like this:

```Plain
tool-test-llm-via-executa-12345678-darwin-arm64.tar.gz
tool-test-llm-via-executa-12345678-darwin-x86_64.tar.gz
tool-test-llm-via-executa-12345678-linux-x86_64.tar.gz

```

These file names contain two types of information:

- `tool-test-llm-via-executa-12345678`: the placeholder `tool_id` used by the current example. For real publishing, you should replace it with the official `tool_id` minted on the Anna platform.

- `darwin-arm64`, `darwin-x86_64`, `linux-x86_64`: platform keys that tell Anna Agent which operating system and CPU architecture the package is for.

Note that `.tar.gz` is not the “final program to run” itself, but a distribution package. After Anna Agent downloads it, it first extracts it, then finds the actual entry file to start inside it.

For the simplest PyInstaller `--onefile` scenario, the archive contains at least one executable file:

```Plain
tool-test-llm-via-executa-12345678-darwin-arm64.tar.gz
└── tool-test-llm-via-executa-12345678

```

But this is only the minimal form. A more standard and recommended form is to also place a `manifest.json` in the archive, explicitly declaring the package’s entry file and runtime information:

```Plain
tool-test-llm-via-executa-12345678-darwin-arm64.tar.gz
├── tool-test-llm-via-executa-12345678
└── manifest.json

```

The benefit is that the Agent does not need to guess “which file is the entry point”. Instead, it can directly find the file to start based on the manifest or the `entrypoint` in `binary_urls`.

If your Executa later becomes more than a single file and needs extra dynamic libraries, data files, model files, or sub-tools, the archive may become structured like this:

```Plain
my-tool-darwin-arm64.tar.gz
├── bin/
│ └── my-tool
├── lib/
│ └── ...
├── data/
│ └── ...
└── manifest.json

```

This is multi-file binary distribution. It uses the same installation mechanism as a single-file binary, but the package contains more content and `manifest.json` becomes more important.

This tutorial starts with the easiest-to-understand single-file PyInstaller packaging: first turn the Python Executa into an executable file that can start independently, then put it into `.tar.gz`, and finally make the `binary_urls` in `executa.json` point to these distribution packages. We can cover the details of multi-file packaging separately later.

### Why Is `tool_id` So Important?

You may have noticed that the same string appears everywhere in this example:

```Plain
tool-test-llm-via-executa-12345678

```

It appears in `executa.json`, and also in the project name and script entry in `pyproject.toml`. This value is the placeholder `tool_id` used by the example.

For real publishing, `tool_id` is not something you can name casually. It should come from Anna platform’s mint flow and is the stable identity assigned to this Executa by the platform.

Why be so strict? Because Anna uses this ID to connect several things:

- which Tool the App manifest declares as a dependency;

- which Tool the frontend specifies when calling `anna.tools.invoke(...)`;

- which tool directory the Agent places files into during installation;

- how later version updates know this is a new version of the same Executa.

So before real publishing, you need to replace the placeholder ID in the example with your own official `tool_id`. But in order to learn the packaging flow first, we can continue using the example placeholder in the first few chapters, then discuss replacement after understanding the full process.

### Chapter Summary

So far, we have not run any packaging commands. This is intentional.

Before starting the build, remember these three sentences:

1. **We are not packaging the entire Anna App, but the Python Executa Tool `llm-via-executa-python`.**

2. **The target packaged artifacts are platform-specific `.tar.gz` files containing executables that Anna Agent can start.**

3. **The `binary_urls` in `executa.json` connect the platform, download URL, entrypoint, and related information.**

In the next chapter, we will start the actual preparation work: confirm the local environment, enter the example directory, check the `tool_id` and Python entry point, and first use development mode to confirm this Executa works normally before packaging.

## Chapter 2: Get Your Own Playground First: Fork the Official Example Repository

In the previous beginner tutorial, we directly used `anna-app-llm-demo` from the official example repository. This is great for learning: the code is already written, the directory structure is complete, and the configuration is visible, so you can clone it and run it directly.

But this tutorial is not only about local running. Later, we will do something closer to real publishing: **use GitHub Actions to automatically package Executa binaries and upload the artifacts to GitHub Release.**

At that point, you cannot only look at the official repository.

The reason is simple: you do not have permission to run the release workflow in the official repository. You cannot freely modify its workflow, and you cannot upload your own binary files into its Releases. Even if you can build a binary locally, when you later fill in `binary_urls`, they should point to your own Release download URLs, not URLs in the official example repository.

So before formally entering packaging, we first do one preparation step: **copy the official example repository into your own practice repository.**

### Why Must You Have Your Own Repository?

If you only want to read the code, or only want to try PyInstaller locally, cloning the official repository is enough:

```Plain
git clone https://github.com/whtcjdtc2007/anna-executa-examples.git

```

But if you want to complete this tutorial end to end, especially the later steps:

- modify `executa.json`;

- replace the placeholder `tool_id` in the example;

- run GitHub Actions;

- create a GitHub Release;

- upload `.tar.gz` binary artifacts;

- make `binary_urls` point to your own download URLs;

then you need a repository where you have write permission.

This repository can be a fork, or it can be a new repository that you manually copied. For most readers, the simplest approach is to fork the official example repository.

### Fork the Official Example Repository

Open the official example repository:

```Plain
https://github.com/whtcjdtc2007/anna-executa-examples

```

Click **Fork** in the upper-right corner of the page.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/a8e0946c0b5c6ec10a83c2614120ef6f3bf7e937.jpeg)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/998fcc630dc2febe50f03e5f40f88a396603dfd4.png)

GitHub will create a copy under your account. The URL usually becomes:

```Plain
https://github.com/<your-github-username>/anna-executa-examples

```

The GitHub Actions we run later, the Release we create, and the binary files we upload will all happen in this repository that belongs to you.

**Please note: from this chapter onward, “your repository” in the tutorial refers to the forked repository, not the original official repository**.

### Clone Your Own Repository

After forking, clone your own repository locally:

```Plain
git clone https://github.com/<your-github-username>/anna-executa-examples.git
cd anna-executa-examples

```

Replace `<your-github-username>` with your GitHub username.

If you have already cloned the official repository, it is also recommended to clone your forked repository again this time, so you do not accidentally operate on a remote where you do not have write permission later.

You can confirm where the current repository points with this command:

```Plain
git remote -v

```

You should see output similar to:

```Plain
origin https://github.com/<your-github-username>/anna-executa-examples.git (fetch)
origin https://github.com/<your-github-username>/anna-executa-examples.git (push)

```

If it is still:

```Plain
https://github.com/whtcjdtc2007/anna-executa-examples.git

```

then your current directory is still connected to the official repository. You can continue reading and running locally, but you will not be able to upload build artifacts to your own GitHub Release.

### Enter the Executa Directory We Will Package

Next, enter the main directory for this tutorial:

```Plain
cd examples/anna-app-llm-demo/executas/llm-via-executa-python

```

You should see these files in the directory:

```Plain
README.md
executa.json
llm_via_executa_plugin.py
pyproject.toml
uv.lock

```

Among them:

- `llm_via_executa_plugin.py` is the Python Executa implementation we will package later;

- `pyproject.toml` determines the Python project name, dependencies, and command-line entry point;

- `executa.json` determines this Executa’s metadata in Anna development tools and the publishing flow;

- `uv.lock` locks dependency versions, helping local development and CI use the same dependency environment.

We will not modify them yet in this chapter. For now, just confirm that you are in the correct directory.

### Confirm That GitHub Actions Can Run

Because we will use GitHub Actions to package binaries later, you also need to confirm that Actions is enabled in the forked repository.

Open your GitHub repository page and go to the **Actions** tab.

If GitHub prompts you to enable workflows, click to enable them. Forked repositories sometimes pause Actions by default. This is normal; manually enabling them once is enough.

Later, we will use the repository’s workflow to perform multi-platform builds. **You do not need to run it right now**. Just confirm that the Actions tab can open normally and that you have write permission for this repository.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/c443f29411a42e04711fe52531539ed798b88401.png)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/5473565a63694600af2617c2475937c4d756940c.png)

### Chapter Summary

At this point, we have completed the most important repository preparation before packaging:

- you have your own `anna-executa-examples` repository;

- the local clone’s remote points to your own GitHub account;

- you know that this tutorial packages `llm-via-executa-python`;

- you know that the later Release assets and `binary_urls` should point to your own repository;

- GitHub Actions can already run in your repository.

We did not change any code or start packaging in this chapter. This is intentional.

Before building binaries, we need to confirm one thing first: this Executa is already healthy in source mode. If it cannot run in source mode, packaging will only hide the problem more deeply.

In the next chapter, we will do the pre-packaging “inspection”: start `llm-via-executa-python` locally first and confirm it can respond normally to `describe`, `health`, and tool calls.

## Chapter 3: Find the Real `tool_id` and Organize Local Configuration Before Packaging

In the previous chapter, we forked the official example repository and learned that we will later use our own GitHub Actions and GitHub Release to package binaries.

In this chapter, we will not rush into packaging. We first need to solve a more critical problem:

**What exactly is this Executa’s official `tool_id`?**

This matters because the later binary file name, PyInstaller output name, Tool configuration on the Anna platform, and `entrypoint` in `binary_urls` will all revolve around this `tool_id`.

If you use the wrong value here, even if the binary is packaged successfully later, the platform may not be able to find the correct tool.

### If You Pushed the App Following the Previous Tutorial, the Platform Has Already Created the Tool for You

In the previous beginner tutorial, if you already ran something like:

```Plain
anna-app apps push

```

or pushed `anna-app-llm-demo` to the platform through the Anna App publishing flow, then the platform will automatically create a corresponding Tool based on the bundled Executa configuration in the App.

In other words, you do not need to invent a `tool_id` from scratch.

You need to find it on the platform.

Open the App you just pushed on the Anna platform and enter the relevant advanced configuration page:

```Plain
More -> Advanced -> Executa

```

There you can see the Executa Tool bound to this App. After clicking into it, you will see a Tool configuration page similar to the screenshots.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/a04cac08aa66f4519ca99441468a64c327ed2b31.png)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/f3e789365fbee1520d6a7349205ea40aa6549d01.png)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/e4273d31550235252337963dbf3c5ba9be7d3988.png)

In the example shown in the screenshots, the Tool ID generated by the platform is:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

This is the example `tool_id` used later in this tutorial.

But note: when you follow along, you will definitely see your own ID, perhaps like this:

```Plain
tool-<your-handle>-llm-via-executa-xxxxxxxx

```

Everywhere later that shows:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

should be replaced with the Tool ID you see on your own platform page.

### Why Can’t We Keep Using the Placeholder in the Example?

The official repository’s example defaults to a placeholder ID:

```Plain
tool-test-llm-via-executa-12345678

```

This ID is convenient for a local demo because it is stable, easy to recognize, and not tied to a real account.

But for real packaging and publishing, it is not suitable.

The reason is that the Anna platform identifies a Tool by the `tool_id` actually created by the platform, which is the ID you see on the Tool configuration page. For example, in this tutorial:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

The binaries we package later will also be named with this ID:

```Plain
tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz

```

The benefit is very direct: the Tool ID on the platform, binary file name, `entrypoint`, and CI build artifacts all match.

### Which Places Need to Be Changed?

Enter the Executa directory:

```Plain
cd examples/anna-app-llm-demo/executas/llm-via-executa-python

```

This chapter mainly focuses on two files:

```Plain
executa.json
pyproject.toml

```

First, the key point: **you do not need to configure distribution in `executa.json` right now.**

That means `binary_urls`, GitHub Release URLs, `entrypoint`, platform download URLs, and related content will be configured later directly on the Anna platform. The current local file only needs to keep development and publishing metadata.

#### Modify `executa.json`

Open:

```Plain
examples/anna-app-llm-demo/executas/llm-via-executa-python/executa.json

```

You can first organize the key fields into this shape:

```Plain
{
  "slug": "llm-via-executa",
  "name": "LLM via Executa",
  "version": "0.1.0",
  "executa_type": "tool",
  "description": "Exposes a `complete` tool that wraps host sampling/createMessage via reverse-RPC, letting the LLM Demo app run completions through an Executa instead of calling anna.llm.complete directly.",

  "tool_id": "tool-youming-llm-via-executa-yv49p6ce",
  "type": "python",
  "enabled": true
}

```

The most important line is:

```Plain
"tool_id": "tool-youming-llm-via-executa-yv49p6ce"

```

You need to replace it with the Tool ID you see on your own platform.

Do not worry about these fields for now:

```Plain
"distribution": {
  ...
}

```

If your file already has `distribution`, you can ignore it for now; to make the tutorial clearer, you can also temporarily delete it. When we truly configure binary download URLs later, we will fill them in on the platform’s Tool configuration page rather than hand-writing them here.

This point is important: **later in this tutorial, we will configure binary distribution through the platform UI. You are not required to maintain `binary_urls` in `executa.json`.**

#### Modify `pyproject.toml`

Next, open:

```Plain
pyproject.toml

```

You will see something like:

```Plain
[project]
name = "tool-test-llm-via-executa-12345678"

[project.scripts]
"tool-test-llm-via-executa-12345678" = "llm_via_executa_plugin:main"

```

Replace the placeholder inside with your real Tool ID.

Using the Tool ID from the screenshots in this tutorial as the example, change it to:

```Plain
[project]
name = "tool-youming-llm-via-executa-yv49p6ce"

[project.scripts]
"tool-youming-llm-via-executa-yv49p6ce" = "llm_via_executa_plugin:main"

```

Do not change the right-hand side:

```Plain
llm_via_executa_plugin:main

```

It means the Python startup entry remains:

```Plain
llm_via_executa_plugin.py

```

inside:

```Plain
main()

```

We are only changing the command name and project name to the platform’s real `tool_id`.

#### Do We Need to Change the App Manifest?

In this demo, the App manifest uses a bundled handle:

```Plain
{
  "tool_id": "bundled:llm-via-executa"
}

```

This does not need to be changed to the real `tool_id`.

The reason is that `bundled:llm-via-executa` is the App’s internal handle for referring to this bundled Executa. During publishing, the Anna platform maps this handle to the real Tool ID.

So do not manually change it to:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

Keep it as it is.

#### Do We Need to Change the Tool ID in the Frontend?

The current demo frontend uses the generated:

```Plain
bundle/anna-tool-ids.js

```

to map the bundled handle to the real Tool ID.

You may see something like:

```Plain
window. __ANNA_TOOL_IDS__ = {
  "llm-via-executa": "tool-youming-llm-via-executa-yv49p6ce"
};

```

This file is usually generated by `anna-app apps publish` / the related publishing flow, and it is not recommended to maintain it manually.

So in this chapter, you also do not need to manually edit the frontend code. We only need to ensure that the Executa’s own configuration and Python project entry have been changed to the real Tool ID.

#### Update the Lock File

Because we changed the project name in `pyproject.toml`, it is recommended to synchronize the Python project again:

```Plain
uv sync

```

If `uv` says the lock file needs to be updated, you can also run:

```Plain
uv lock

```

There is no need to manually edit `uv.lock`. Let `uv` update it.

### Run a Local Check Before Packaging

Now you can use the Anna CLI to check whether this Executa can still start normally.

Run this in the current directory:

```Plain
anna-app executa dev --describe

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/72d10075ac82bc0813dd84f177f6db889518f275.jpeg)

This command starts the current Executa and sends a JSON-RPC `describe` request.

If everything is normal, you should see it return manifest information. Focus on confirming:

- the command can start normally;

- there is no Python import error;

- the output includes tools;

- the process does not exit immediately.

Then check health:

```Plain
anna-app executa dev --health

```

If it returns health status normally, then after changing `tool_id` and the Python entry point, source mode is still runnable.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/6ee509dfcadb48cfccdf196efee76694193c78ed.jpeg)

The core goal of this chapter is to confirm:

- `describe` can run;

- `health` can run;

- the Python entry point is not broken;

- the real `tool_id` has been replaced in the local Executa configuration.

### Chapter Summary

In this chapter, we completed the most important pre-packaging step: replacing the example placeholder ID with the real Tool ID created by the platform.

We did several things:

- found the real `tool_id` on the Anna platform through `More -> Advanced -> Executa`;

- used `tool-youming-llm-via-executa-yv49p6ce` as the demonstration ID in this tutorial;

- modified the `tool_id` in `executa.json`;

- modified `[project].name` and `[project.scripts]` in `pyproject.toml`;

- clarified that you do not need to configure `distribution` in `executa.json` right now;

- clarified that `bundled:llm-via-executa` in `manifest.json` does not need to be manually replaced;

- used `anna-app executa dev --describe` and `--health` for a pre-packaging check.

In the next chapter, we can officially start packaging: use PyInstaller to turn this Python Executa into a binary file that can start independently.

## Chapter 4: Start Packaging: Use a Script to Generate a Binary Archive Anna Agent Can Install

Now we have confirmed two things:

- the `tool_id` in `executa.json` has been changed to the ID actually created by the platform;

- this Executa can normally `describe` / `health` in source mode.

Now we can finally start packaging.

The goal of this chapter is not to generate a lonely executable file, but to generate a **binary distribution package that Anna Agent can install**.

In other words, the final file we want looks like this:

```Plain
dist-anna/
└── tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz

```

And inside this `.tar.gz`, there should be a more standard structure:

```Plain
tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz
├── bin/
│ └── tool-youming-llm-via-executa-yv49p6ce
└── manifest.json

```

The `bin/` directory contains the actual binary to start, and `manifest.json` tells Anna Agent where the entry file is, what the version is, and which files need execute permission.

### First Clarify: PyInstaller Cannot Truly Cross-Compile

Before writing the script, let’s clarify a common misconception.

Later, we will indeed support these platforms:

```Plain
darwin-arm64
darwin-x86_64
linux-x86_64

```

But Python’s PyInstaller usually cannot produce binaries for all platforms directly on a single machine.

That means:

- an Apple Silicon Mac produces `darwin-arm64`;

- an Intel Mac produces `darwin-x86_64`;

- a Linux x86\_64 runner produces `linux-x86_64`.

So the script in this chapter does two things:

1. automatically detect the current platform;

2. generate a `.tar.gz` for the corresponding platform using the official platform key.

**Later, in the GitHub Actions chapter, we will let different runners run the same script separately, so we can automatically obtain multi-platform artifacts.**

### Create the Packaging Script

Operate in this directory:

```Plain
cd examples/anna-app-llm-demo/executas/llm-via-executa-python

```

Create a script:

```Plain
package_binary.sh

```

with the following content:

```Plain
#!/usr/bin/env bash
set -euo pipefail

cd "$(dirname "$0")"

EXECUTA_JSON="executa.json"
ENTRY_FILE="llm_via_executa_plugin.py"
OUT_DIR="dist-anna"

if [! -f "$EXECUTA_JSON"]; then
  echo "ERROR: $EXECUTA_JSON not found" >&2
  exit 1
fi

if [! -f "$ENTRY_FILE"]; then
  echo "ERROR: $ENTRY_FILE not found" >&2
  exit 1
fi

if ! command -v uv >/dev/null 2>&1; then
  echo "ERROR: uv is required. Please install uv first." >&2
  exit 1
fi

TOOL_ID="$(python3 - <<'PY'
import json
with open("executa.json", "r", encoding="utf-8") as f:
    data = json.load(f)
print(data["tool_id"])
PY
)"

VERSION="$(python3 - <<'PY'
import json
with open("executa.json", "r", encoding="utf-8") as f:
    data = json.load(f)
print(data.get("version") or "0.0.0")
PY
)"

DISPLAY_NAME="$(python3 - <<'PY'
import json
with open("executa.json", "r", encoding="utf-8") as f:
    data = json.load(f)
print(data.get("name") or data["tool_id"])
PY
)"

DESCRIPTION="$(python3 - <<'PY'
import json
with open("executa.json", "r", encoding="utf-8") as f:
    data = json.load(f)
print(data.get("description") or "")
PY
)"

OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
ARCH="$(uname -m)"

case "$ARCH" in
  x86_64|amd64)
    ARCH="x86_64"
    ;;
  arm64|aarch64)
    ARCH="arm64"
    ;;
esac

case "$OS-$ARCH" in
  darwin-arm64)
    PLATFORM="darwin-arm64"
    ;;
  darwin-x86_64)
    PLATFORM="darwin-x86_64"
    ;;
  linux-x86_64)
    PLATFORM="linux-x86_64"
    ;;
  *)
    echo "ERROR: unsupported platform: $OS-$ARCH" >&2
    echo "This tutorial script currently targets: darwin-arm64, darwin-x86_64, linux-x86_64" >&2
    exit 1
    ;;
esac

echo "Tool ID: $TOOL_ID"
echo "Version: $VERSION"
echo "Platform: $PLATFORM"
echo

rm -rf build dist "$OUT_DIR/staging-$PLATFORM"
mkdir -p "$OUT_DIR/staging-$PLATFORM/bin"

echo "==> Building single-file executable with PyInstaller"

uv run --with pyinstaller python -m PyInstaller \
  --onefile \
  --clean \
  --noupx \
  --name "$TOOL_ID" \
  "$ENTRY_FILE"

BINARY="dist/$TOOL_ID"

if [! -f "$BINARY"]; then
  echo "ERROR: PyInstaller did not produce $BINARY" >&2
  exit 1
fi

if ["$(uname -s)" = "Darwin"]; then
  codesign --force --sign - "$BINARY" 2>/dev/null || true
fi

STAGE="$OUT_DIR/staging-$PLATFORM"
cp "$BINARY" "$STAGE/bin/$TOOL_ID"
chmod 0755 "$STAGE/bin/$TOOL_ID"

echo "==> Writing archive manifest"

python3 - "$STAGE/manifest.json" "$TOOL_ID" "$VERSION" "$DISPLAY_NAME" "$DESCRIPTION" <<'PY'
import json
import sys
from pathlib import Path

manifest_path = Path(sys.argv[1])
tool_id = sys.argv[2]
version = sys.argv[3]
display_name = sys.argv[4]
description = sys.argv[5]

entrypoint = f"bin/{tool_id}"

manifest = {
    "name": tool_id,
    "display_name": display_name,
    "version": version,
    "description": description,
    "runtime": {
        "binary": {
            "entrypoint": {
                "default": entrypoint
            },
            "permissions": {
                entrypoint: "0o755"
            }
        }
    }
}

manifest_path.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
PY

ARCHIVE="$OUT_DIR/$TOOL_ID-$PLATFORM.tar.gz"

echo "==> Creating archive: $ARCHIVE"

(
  cd "$STAGE"
  tar czf "../$TOOL_ID-$PLATFORM.tar.gz" .
)

if command -v shasum >/dev/null 2>&1; then
  SHA256="$(shasum -a 256 "$ARCHIVE" | awk '{print $1}')"
else
  SHA256="$(sha256sum "$ARCHIVE" | awk '{print $1}')"
fi

SIZE="$(wc -c < "$ARCHIVE" | tr -d ' ')"

echo
echo "Built archive:"
echo " $ARCHIVE"
echo
echo "SHA-256:"
echo " $SHA256"
echo
echo "Size:"
echo " $SIZE bytes"
echo
echo "Archive layout:"
tar tzf "$ARCHIVE"
echo
echo "Later, the platform binary asset for this platform will look like:"
echo
cat <<JSON
"$PLATFORM": {
  "url": "https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v$VERSION/$TOOL_ID-$PLATFORM.tar.gz",
  "sha256": "$SHA256",
  "size": $SIZE,
  "entrypoint": "bin/$TOOL_ID",
  "format": "tar.gz"
}
JSON

```

Then give the script execute permission:

```Plain
chmod +x package_binary.sh

```

### What Does This Script Do?

This script looks a bit long, but the logic is not complicated.

It mainly does six things.

First, it reads metadata from `executa.json`:

```Plain
tool_id
version
name
description

```

This way, we do not need to hard-code:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

in the script. If your real `tool_id` is different, as long as you changed `executa.json` in the previous chapter, the script will automatically use your ID.

Second, it detects the current platform:

```Plain
darwin-arm64
darwin-x86_64
linux-x86_64

```

These names are the platform keys used by Anna binary distribution. Later, the same keys will be used when configuring platform download URLs.

Third, it uses PyInstaller to build a single-file binary:

```Plain
uv run --with pyinstaller python -m PyInstaller \
  --onefile \
  --clean \
  --noupx \
  --name "$TOOL_ID" \
  "$ENTRY_FILE"

```

The output file name is the `tool_id`.

For example, this tutorial generates:

```Plain
dist/tool-youming-llm-via-executa-yv49p6ce

```

Fourth, it organizes the output into Anna’s recommended archive layout:

```Plain
dist-anna/staging-darwin-arm64/
├── bin/
│ └── tool-youming-llm-via-executa-yv49p6ce
└── manifest.json

```

The binary is not placed directly at the archive root, but inside `bin/`.

This structure is clearer and closer to the recommended format for official multi-file binaries. If you later need to add `lib/`, `data/`, or other files, you can extend it naturally.

Fifth, it generates `manifest.json` inside the archive:

```Plain
{
  "name": "tool-youming-llm-via-executa-yv49p6ce",
  "display_name": "LLM via Executa",
  "version": "0.1.0",
  "description": "...",
  "runtime": {
    "binary": {
      "entrypoint": {
        "default": "bin/tool-youming-llm-via-executa-yv49p6ce"
      },
      "permissions": {
        "bin/tool-youming-llm-via-executa-yv49p6ce": "0o755"
      }
    }
  }
}

```

The purpose of this file is to tell Anna Agent:

```Plain
Start bin/tool-youming-llm-via-executa-yv49p6ce

```

instead of making the Agent guess which file in the archive is the entry point.

Sixth, it generates `.tar.gz` and outputs:

```Plain
sha256
size
entrypoint
format

```

These values will be used later when configuring binary distribution on the platform.

### Run the Packaging Script

Now run:

```Plain
./package_binary.sh

```

If you run it on an Intel Mac, you will see something like:

```Plain
> ./package_binary.sh **(base)**
Tool ID: tool-youming-llm-via-executa-yv49p6ce
Version: 0.1.0
Platform: darwin-x86_64

==> Building single-file executable with PyInstaller
Installed **6 packages** in 56ms
223 INFO: PyInstaller: 6.21.0, contrib hooks: 2026.6
223 INFO: Python: 3.11.15
239 INFO: Platform: macOS-26.5.1-x86_64-i386-64bit

......

8028 INFO: Build complete! The results are available in: examples/anna-app-llm-demo/executas/llm-via-executa-python/dist
==> Writing archive manifest
==> Creating archive: dist-anna/tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz

Built archive:
  dist-anna/tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz

SHA-256:
  07ca7822e8cc95c995ce8c33c33bd791cf0e4cb8533653fc64a36751e4f8d299

Size:
  9917196 bytes

Archive layout:
./
./bin/
./manifest.json
./bin/tool-youming-llm-via-executa-yv49p6ce

Later, the platform binary asset for this platform will look like:

"darwin-x86_64": {
  "url": "https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v0.1.0/tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz",
  "sha256": "07ca7822e8cc95c995ce8c33c33bd791cf0e4cb8533653fc64a36751e4f8d299",
  "size": 9917196,
  "entrypoint": "bin/tool-youming-llm-via-executa-yv49p6ce",
  "format": "tar.gz"
}

```

If you run it on Linux x86\_64, the artifact becomes:

```Plain
dist-anna/tool-youming-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz

```

If you run it on an Apple Silicon Mac, the artifact becomes:

```Plain
dist-anna/tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz

```

### Check the Archive Structure

The script automatically prints the archive layout at the end. You can also check it manually:

```Plain
tar tzf dist-anna/tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz

```

You should see something like:

```Plain
./
./bin/
./bin/tool-youming-llm-via-executa-yv49p6ce
./manifest.json

```

This means the archive was not produced arbitrarily. It is a distribution package that the Anna binary installer can understand.

### Extract and Test Locally

To confirm that the binary inside the archive can really start, temporarily extract it into a directory:

```Plain
mkdir -p /tmp/llm-via-executa-test
tar xzf dist-anna/tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz -C /tmp/llm-via-executa-test

```

**Remember to replace the file name with the one actually generated for your current platform.**

Then send a JSON-RPC request directly to the binary:

```Plain
printf '%s\n' '{"jsonrpc":"2.0","method":"describe","id":1}' \
  | /tmp/llm-via-executa-test/bin/tool-youming-llm-via-executa-yv49p6ce

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/2281792656bfcbd816cfcb992b421507fa0a0931.jpeg)

If you are currently operating on an Intel Mac, this chapter can only build:

```Plain
darwin-x86_64

```

This is normal.

Do not try to force-build Linux or macOS Apple Silicon binaries locally. PyInstaller’s output is closely tied to the current system, and local cross-platform packaging can easily produce files that cannot run.

**We will hand real multi-platform builds to GitHub Actions.**

In the next chapter, we will put the `package_binary.sh` written in this chapter into a GitHub Actions matrix, letting GitHub automatically run it on different platform runners and produce:

```Plain
tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz

```

### Chapter Summary

In this chapter, we completed the first real binary packaging:

- wrote a `package_binary.sh`;

- automatically read `tool_id` and `version` from `executa.json`;

- used PyInstaller to generate a single-file binary;

- organized the binary into the `bin/` directory;

- automatically generated `manifest.json` at the archive root;

- packaged it as `.tar.gz`;

- output the `sha256`, `size`, `entrypoint`, and `format` needed for later platform configuration;

- extracted it locally and verified that the binary can respond to `describe`.

At this point, we can already generate a standard Anna Executa binary archive for the current platform.

In the next chapter, we will move this workflow to GitHub Actions, letting it automatically build artifacts for multiple platforms and upload them to GitHub Release.

Confirmed, you are right. This chapter needs two changes:

- The commit should not include only `package_binary.sh`, because we also changed `executa.json`, `pyproject.toml`, and possibly `uv.lock` earlier. The tutorial should ask readers to check and directly commit all related changes.

- The runner needs to be changed. I checked GitHub’s official runner documentation according to the `github-action-release` skill: `macos-14` is macOS arm64, `macos-15-intel` is macOS Intel, and `ubuntu-latest` is Linux x64. The previous version using `macos-15 + python_arch: x64` to build `darwin-x86_64` was not stable enough; it should directly use `macos-15-intel`. Source: GitHub hosted runners reference: [https://docs.github.com/en/actions/reference/runners/github-hosted-runners](https://docs.github.com/en/actions/reference/runners/github-hosted-runners)

Below is the recommended replacement content for the core chapter.

## Chapter 5: Hand It to GitHub Actions: Automatically Build Multi-Platform Binaries

In the previous chapter, we built a binary archive for the current platform locally.

But for real publishing, providing only one platform is not enough. At minimum, we want to cover these three common platforms first:

```Plain
darwin-arm64
darwin-x86_64
linux-x86_64

```

That is:

- macOS Apple Silicon;

- macOS Intel;

- Linux x86\_64.

PyInstaller cannot reliably produce binaries for all platforms on a single machine. The most stable approach is: **let the GitHub Actions runner for each platform build the package for that platform.**

In this chapter, we will write a workflow that lets GitHub Actions automatically run the `package_binary.sh` from the previous chapter on different runners, then upload the generated `.tar.gz` and `.sha256` files to GitHub Release.

### First Commit All Previous Changes

Before continuing, confirm that all files you changed earlier have been committed.

Because earlier, we did not only add the packaging script. We may also have modified these files:

```Plain
examples/anna-app-llm-demo/executas/llm-via-executa-python/executa.json
examples/anna-app-llm-demo/executas/llm-via-executa-python/pyproject.toml
examples/anna-app-llm-demo/executas/llm-via-executa-python/uv.lock
examples/anna-app-llm-demo/executas/llm-via-executa-python/package_binary.sh

```

If these changes are not committed, GitHub Actions will not see them in the remote repository. Later packaging may still use the old placeholder `tool_id`.

First check the current changes:

```Plain
git status

```

After confirming these changes are what you want GitHub Actions to use, commit them directly:

```Plain
git add .
git commit -m "Prepare llm-via-executa binary packaging"

```

Then push to your own fork:

```Plain
git push

```

Emphasizing this point:

1. GitHub Actions runs the code in the GitHub repository, not files that are still uncommitted in your local working tree.

2. You need to push to your own fork, not the official repository.

### Add the GitHub Actions Workflow

At the repository root, create:

```Plain
.github/workflows/build-llm-via-executa-binary.yml

```

with the following content:

```Plain
name: Build llm-via-executa binaries

on:
  workflow_dispatch:

permissions:
  contents: write

env:
  EXECUTA_DIR: examples/anna-app-llm-demo/executas/llm-via-executa-python

jobs:
  meta:
    runs-on: ubuntu-latest
    outputs:
      tool_id: ${{ steps.meta.outputs.tool_id }}
      version: ${{ steps.meta.outputs.version }}
      slug: ${{ steps.meta.outputs.slug }}
      tag: ${{ steps.meta.outputs.tag }}
    steps:
      - uses: actions/checkout@v4

      - id: meta
        run: |
          python3 - <<'PY' >> "$GITHUB_OUTPUT"
          import json
          from pathlib import Path

          p = Path("examples/anna-app-llm-demo/executas/llm-via-executa-python/executa.json")
          data = json.loads(p.read_text("utf-8"))

          tool_id = data["tool_id"]
          version = data.get("version") or "0.0.0"
          slug = data.get("slug") or "llm-via-executa"
          tag = f"{slug}-v{version}"

          print(f"tool_id={tool_id}")
          print(f"version={version}")
          print(f"slug={slug}")
          print(f"tag={tag}")
          PY

  build:
    needs: meta
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: darwin-arm64
            os: macos-14
          - platform: darwin-x86_64
            os: macos-15-intel
          - platform: linux-x86_64
            os: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - uses: astral-sh/setup-uv@v5

      - name: Build binary archive
        working-directory: ${{ env.EXECUTA_DIR }}
        env:
          TOOL_ID: ${{ needs.meta.outputs.tool_id }}
          PLATFORM: ${{ matrix.platform }}
        run: |
          chmod +x package_binary.sh
          ./package_binary.sh

          expected="dist-anna/$TOOL_ID-$PLATFORM.tar.gz"
          test -f "$expected"

      - name: Write sha256 file
        working-directory: ${{ env.EXECUTA_DIR }}
        env:
          TOOL_ID: ${{ needs.meta.outputs.tool_id }}
          PLATFORM: ${{ matrix.platform }}
        run: |
          archive="dist-anna/$TOOL_ID-$PLATFORM.tar.gz"

          if command -v shasum >/dev/null 2>&1; then
            shasum -a 256 "$archive" > "$archive.sha256"
          else
            sha256sum "$archive" > "$archive.sha256"
          fi

          cat "$archive.sha256"

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ needs.meta.outputs.tool_id }}-${{ matrix.platform }}
          path: |
            ${{ env.EXECUTA_DIR }}/dist-anna/${{ needs.meta.outputs.tool_id }}-${{ matrix.platform }}.tar.gz
            ${{ env.EXECUTA_DIR }}/dist-anna/${{ needs.meta.outputs.tool_id }}-${{ matrix.platform }}.tar.gz.sha256

  release:
    needs: [meta, build]
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-assets
          merge-multiple: true

      - name: List release assets
        run: |
          find release-assets -type f -maxdepth 1 -print | sort

      - name: Create or update GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ needs.meta.outputs.tag }}
          name: ${{ needs.meta.outputs.slug }} v${{ needs.meta.outputs.version }}
          make_latest: false
          fail_on_unmatched_files: true
          files: release-assets/*

```

This workflow is divided into three jobs:

- `meta`: reads `executa.json` to get `tool_id`, `version`, `slug`, and the release tag.

- `build`: calls `package_binary.sh` separately on three runners.

- `release`: aggregates all build artifacts and uploads them to GitHub Release.

### Commit the Workflow

After adding the workflow, commit all changes again:

```Plain
git status
git add .
git commit -m "Add llm-via-executa release workflow"
git push

```

If `git status` shows unrelated files you do not want to commit, handle them before `git add .`.

But for this tutorial path, the earlier changes to `executa.json`, `pyproject.toml`, `uv.lock`, `package_binary.sh`, and the workflow should all enter the same remote repository state.

### Manually Run the Workflow

Open your GitHub repository page and go to:

```Plain
Actions -> Build llm-via-executa binaries

```

Click:

```Plain
Run workflow

```

Choose the branch containing your latest commit, then run it.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/02746f7c26052d12479639d3d5bde1cba49c2fe3.jpeg)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/161ffd3d9012a86256387fb740566b8336834aed.png)

Note: if you modified the workflow, do not click `Re-run jobs` in an old failed record. That may still use the old workflow. You should return to the workflow page and click `Run workflow` again, and confirm the branch points to the version containing your latest commit.

### What Do You Get After It Succeeds?

After the workflow succeeds, enter the GitHub Release page. You should see a tag:

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/83c07b683960f1c35d6486e77a9a3180f88c2ff4.jpeg)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/3e33f862cefdc250a10f550a945414b9d93de03c.jpeg)

```Plain
llm-via-executa-v0.1.0

```

Inside, there will be these assets:

```Plain
tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz.sha256

tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz.sha256

tool-youming-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz
tool-youming-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz.sha256

```

These files are the binary download assets that we will fill into the Anna platform Tool binary distribution in the next chapter.

### Chapter Summary

In this chapter, we moved the local packaging flow to GitHub Actions.

We completed several things:

- confirmed that all files changed earlier need to be committed, including `executa.json`, `pyproject.toml`, `uv.lock`, `package_binary.sh`, and the new workflow;

- used `git add .` and `git commit` to push local changes to your own fork, ensuring GitHub Actions can read the latest configuration;

- selected three build environments according to GitHub’s official runner labels:

- added the `build-llm-via-executa-binary.yml` workflow;

- let the workflow automatically read `tool_id`, `version`, and `slug` from `executa.json`;

- used a matrix to run the same `package_binary.sh` on different runners;

- generated `.tar.gz` and `.sha256` for each platform;

- finally, used a separate `release` job to aggregate all build artifacts and upload them to GitHub Release.

At this point, we can do more than package locally. We now have a remotely reproducible build flow. Every time you update Executa code later, you only need to commit the changes and rerun this workflow to get a new set of multi-platform binary Release assets.

In the next chapter, we will return to the Anna platform and configure these GitHub Release download URLs in the Tool’s binary distribution, so Anna Agent can truly install and start this Executa through the binary path.

## Chapter 6: Configure Binary Distribution on the Anna Platform and Reinstall on the Local Agent

In the previous chapter, we built binary archives for three platforms through GitHub Actions and uploaded them to GitHub Release.

Right now, these `.tar.gz` files are only “download files placed on GitHub”. Anna Agent does not yet know where to download them.

In this chapter, we will return to the Anna platform, switch the distribution mode of the `LLM via Executa` Tool to **Binary** , and fill in the GitHub Release download URLs generated earlier.

After that, we will reinstall this Tool on the local Agent and confirm that it no longer starts in Local mode, but downloads and runs through Binary mode.

### First Check Whether the Local Agent Still Has the Local Version Installed

If you followed the previous tutorial all the way through, your local Agent has probably already installed this Tool.

But it was likely installed through Local mode before, meaning it was started directly from your local development directory instead of downloading a binary from GitHub Release.

Let’s check first.

In the lower-left corner of the Anna page, go to:

```Plain
More -> Agents

```

After entering the local Agents management page, find your local Agent.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/43cb3bc421d007886b45a2dff39a25aad5c5646b.png)

Click:

```Plain
Details

```

on the Agent card.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/244837097845ddf7a6674269fec6f38d2dbc6d6e.png)

This opens the list of plugins currently installed on this Agent.

Find:

```Plain
LLM via Executa

```

in the list.

If the text below it says:

```Plain
Local

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/fc75ded5eb7267cb77dadf8963fbad119433dde7.png)

then the current Agent still has the local development version installed, not the binary distribution version.

In this case, **first click the trash icon on the right to delete this Local version. After clicking, this tool will enter the Not Installed state.**

This deletes the local plugin instance installed on the current Agent. It does not delete the Tool definition on the platform. The `LLM via Executa` Tool on the platform still exists, and later we will let the Agent reinstall it through Binary mode.

If you already see it displayed as:

```Plain
Binary

```

you can leave it for now. But if after configuring Binary later you find that the Agent did not redownload it, you can come back, delete the old installation, and reinstall.

### Edit the Tool Configuration on the Platform

Next, enter the Executa page and find:

```Plain
LLM via Executa

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/cf752d122025360f7dcd260186d61bf22137e227.jpeg)

Open the edit page.

> **Note:** In **Visibility — controls Hub publishing & AnnaApp packaging** , the Tool must be set to `public` or `app_bundled`, not `private`. This Tool is bound to the AnnaApp and needs to be shared with other users together with the App. If it remains `private`, other users will not be able to access the Tool, and the App will not be able to invoke it properly on their side.
> 
> ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/3c7e538dda3c083c6bf810b9633f6c449fd7f129.png)  
> There is also a small bug observed in practice: every time you reopen the Edit page, Visibility may default back to `private`. Before saving, always check this field manually and change it back to `public` or `app_bundled` if needed.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/cd24a614ebc1d36bb7335c0c14935c2aed0f07a0.png)

In the Distribution area, set:

```Plain
Distribution Type

```

to:

```Plain
Binary

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/e7c6f12f6d1808bfd9da1964ce2327d27fc63a24.png)

**This step means: this Tool no longer requires the Agent to start it from a local path or source environment. Instead, it is installed through binary download URLs.**

The page may have a single-platform download URL input:

```Plain
Download URL (Single Platform)

```

This field is suitable for simple cases where there is only one platform package. We have now built multiple platforms, so focus on the section below:

```Plain
Multi-platform Binary Download URLs

```

### Fill in the Release Download URLs for the Three Platforms

Click:

```Plain
Add Platform

```

Add the following three platforms:

```Plain
macOS ARM64 (Apple Silicon)
macOS x86_64 (Intel)
Linux x86_64

```

Then fill in the corresponding download URLs from the GitHub Release in the previous chapter.

Assume your GitHub username is:

```Plain
<your-github-username>

```

your Tool ID is:

```Plain
tool-<handle>-llm-via-executa-yv49p6ce

```

and the version is:

```Plain
0.1.0

```

Then the three download URLs are roughly:

```Plain
https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v0.1.0/tool-<handle>-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz

```

```Plain
https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v0.1.0/tool-<handle>-llm-via-executa-yv49p6ce-darwin-x86_64.tar.gz

```

```Plain
https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v0.1.0/tool-<handle>-llm-via-executa-yv49p6ce-linux-x86_64.tar.gz

```

Please replace:

```Plain
<your-github-username>

```

with your GitHub username.

Also replace:

```Plain
tool-<handle>-llm-via-executa-yv49p6ce

```

with your own Tool ID.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/5b2c24c6266a9268ebefef96d3ea9ef003cbc07c.png)

### Let the Local Agent Reinstall This Tool

After saving the Tool configuration, go back to:

```Plain
More -> Agents

```

Find your local Agent.

Click:

```Plain
Install Essentials

```

The Agent will pull the Tool configuration from the platform again and download the binary package for the corresponding platform according to the current Binary distribution mode.

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/648dbca02c687699901476b7b6f359f964e1028d.png)

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/1b2785c1b5e937cb57a30a82f5f5bb45acef9cad.png)

This process may take a while. After the download completes, click:

```Plain
Details

```

again.

Find:

```Plain
LLM via Executa

```

This time, the text below it should show:

```Plain
Binary

```

and the status should be:

```Plain
Running

```

 ![image](https://canada1.discourse-cdn.com/flex033/uploads/anna/original/1X/0346ac3150630854aea597a743b617fe795ba16e.png)

This means the local Agent is no longer starting this Tool from the local source directory. It has downloaded the binary package for the corresponding platform from GitHub Release and successfully started it.

### FAQ: What If Details Still Shows Not Installed?

After configuration, under normal conditions we expect to see this in the Agent Details page:

```Plain
LLM via Executa
Binary
Running

```

But sometimes you may still see:

```Plain
Not Installed

```

This means the Agent did not successfully install this Tool.

Do not rush to repeatedly click `Install Essentials`. Problems like this are usually not because the button was not clicked, but because the binary distribution configuration, archive structure, or plugin protocol itself has an issue. You can troubleshoot in the following directions.

#### Check Whether `manifest.json` in the Archive Is Correct

First, check whether the `.tar.gz` you uploaded to GitHub Release really contains `manifest.json`.

After downloading the archive for the corresponding platform, inspect it like this:

```Plain
tar tzf tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz

```

You should see something like:

```Plain
./bin/
./bin/tool-youming-llm-via-executa-yv49p6ce
./manifest.json

```

Then extract it and inspect `manifest.json`:

```Plain
mkdir -p /tmp/llm-via-executa-check
tar xzf tool-youming-llm-via-executa-yv49p6ce-darwin-arm64.tar.gz -C /tmp/llm-via-executa-check
cat /tmp/llm-via-executa-check/manifest.json

```

Focus on this field:

```Plain
{
  "runtime": {
    "binary": {
      "entrypoint": {
        "default": "bin/tool-youming-llm-via-executa-yv49p6ce"
      }
    }
  }
}

```

`runtime.binary.entrypoint.default` must point to a binary file path that truly exists inside the archive.

If your archive structure is:

```Plain
bin/tool-youming-llm-via-executa-yv49p6ce

```

then the entrypoint should be:

```Plain
bin/tool-youming-llm-via-executa-yv49p6ce

```

If it is written as:

```Plain
tool-youming-llm-via-executa-yv49p6ce

```

or as a nonexistent path, the Agent will not find the entry after extraction and naturally cannot install successfully.

#### Check Whether the Binary File Can Actually Run

Sometimes the archive structure looks correct, but the binary inside cannot run.

After extraction, execute it directly:

```Plain
/tmp/llm-via-executa-check/bin/tool-youming-llm-via-executa-yv49p6ce

```

If it errors immediately, or says a dynamic library is missing, a Python module is missing, or permission is insufficient, then the problem is in the packaged artifact itself.

The better check is to directly send it a JSON-RPC request:

```Plain
printf '%s\n' '{"jsonrpc":"2.0","method":"describe","id":1}' \
  | /tmp/llm-via-executa-check/bin/tool-youming-llm-via-executa-yv49p6ce

```

A valid Anna Executa Tool should at least respond normally to `describe`.

If this fails, then although the binary exists, it is not a usable Executa Tool. Common reasons include:

- PyInstaller did not package dependencies;

- the Python entry point is wrong;

- the program does not read stdin after startup;

- the program does not output according to the JSON-RPC over stdio protocol;

- the program prints non-JSON content to stdout at startup, polluting the protocol output;

- the program exits immediately after startup.

Anna Agent is only responsible for starting the process and communicating with it through stdio JSON-RPC. It will not fix a binary that does not conform to the protocol by itself.

#### Check Whether the Platform Matches

Agent selects the download URL based on its own platform.

For example, if your Agent is an Apple Silicon Mac, it needs:

```Plain
darwin-arm64

```

If your Agent is an Intel Mac, it needs:

```Plain
darwin-x86_64

```

If your Agent is Linux x86\_64, it needs:

```Plain
linux-x86_64

```

If you only uploaded `darwin-arm64`, but the current Agent is `darwin-x86_64`, installation will fail.

You can go back to the Agent page to view the current machine’s platform information. Then confirm that the Tool’s Binary configuration has the download URL for that platform.

Also confirm that you did not swap the platform and URL. For example:

```Plain
macOS ARM64

```

should not be filled with:

```Plain
...-darwin-x86_64.tar.gz

```

but should be filled with:

```Plain
...-darwin-arm64.tar.gz

```

#### Check Whether the GitHub Release Link Is Correct

The Binary URL must be directly downloadable by the Agent.

Copy the URL you filled into the platform and open it in a browser to confirm it can directly download the `.tar.gz` file.

You can also test it with a command:

```Plain
curl -L -I "https://github.com/<your-github-username>/anna-executa-examples/releases/download/llm-via-executa-v0.1.0/<asset-name>.tar.gz"

```

If it returns 404, the link is wrong. Common mistakes include:

- the GitHub username is wrong;

- the repository name is wrong;

- the Release tag is wrong;

- the asset file name is wrong;

- the version number is inconsistent;

- you copied the URL from the official repository instead of your own fork;

- the Release has not successfully generated the asset for the corresponding platform.

If the repository is private, also confirm that the environment where the Agent runs has permission to download this file. To reduce pitfalls during the tutorial stage, it is recommended to first use a public fork or a publicly accessible Release asset.

### Chapter Summary

In this chapter, we completed the real binary distribution configuration:

- checked and deleted the old Local installation in Agent Details;

- entered the Executa page and edited `LLM via Executa`;

- switched Distribution Type to `Binary`;

- filled in GitHub Release download URLs for `macOS ARM64`, `macOS x86_64`, and `Linux x86_64`;

- returned to the Agents page and clicked `Install Essentials`;

- entered Details again and confirmed that `LLM via Executa` shows `Binary` and status `Running`.

At this point, this Tool can already be downloaded and started by Anna Agent through the binary path. In other words, we have gone from “a Python Executa that can run locally” to “a platform-distributable Binary Executa”.
