AbstractGateway — Desktop tray icon¶
abstractgateway serve can show a small icon in the macOS menu bar, the
Windows system tray or a Linux panel. It exists so that anyone running a
gateway on their own computer — including people who never open a terminal —
can see what it is doing and act on it in one click:
- Open Console — the web console (
/console), already signed in: the tray mints a one-time sign-in link locally (the same code asabstractgateway claim; see Security) so an expired browser session never ends on a token prompt. One door: every other console entry point in the menu is a deep link to a tab of it. - Apps — the six AbstractFramework apps, one short line each: Open X (running, whoever started it, or installed: started first), Install X… (installs through the gateway, with progress notifications: the same install as the console's Install button, so Code's terminal app comes with it; nothing opens by itself, the menu then offers Open X), Launch Assistant (the desktop app), otherwise the app's name, greyed. See Apps below.
- Workflows — what is running now, then the last 24 hours. Under a
one-line tally (
Now: 2 running · 1 waiting — last 24 hours: 3 done), the active runs come first — running (🟢), then waiting for a person or an event (🟡) — each with the time elapsed so far, whatever day it started; then the runs that finished in the last 24 hours (✅ completed, ❌ failed, ⚪️ cancelled), newest first, with how long each took. Every row shows the step count and opens a small submenu: when it started, and Open in Observer (that run's page in Observer, signed in; a stopped Observer the gateway manages is started first; greyed when Observer is not installed). Open Runs in Console at the bottom is the full list. A row is a run someone started — a conversation turn or an automation's occurrence — never a sub-run or an automation's controller; a run whose sub-run is working counts as running. The list is host-wide (GET /api/gateway/host/runs), not per-principal: memory, GPU and loaded models on this menu describe the machine, and the run list has to describe the same machine. Catalog-published workflows are shown under the name you know them by (their run id encodes scope and tenant in base64) and are not mistaken for the gateway's own bookkeeping runs. Summoned entities' data planes are not listed (the payload names them inskipped_entity_planes). - Pause / Resume Workflows — the one high-level control over all of that: stop new workflow steps from running to free the machine or to look at what is going on (the gateway keeps answering; work queues until you resume).
- Models — what is in memory, what can be loaded, and the way to either: eject a loaded model, or preload an installed one. See Models below.
- Show Activity Window — two live graphs (memory, GPU) and the model list
in a small window (needs
tkinter; see below). - Start AbstractGateway at login — a check item that shows whether this gateway WOULD start at your next login, and switches it. See Start at login.
- Check for Updates / Restart / Quit / About / Help. About shows the
gateway's version, console address and data folder, the AbstractFramework
project details (website, source, documentation, where to report an issue
or give feedback, contact) and the versions of the framework packages this
gateway runs (from
GET /api/gateway/about).
The menu as shipped (macOS, a machine with 151 installed models; […] is a
greyed information line, ▸ a submenu, ☐/☑ the check item):
[AbstractGateway — Running]
[Ready · 1 model loaded · 249 MB]
[http://127.0.0.1:8080 · localhost only]
Open Console
Apps ▸ Open Observer | Open Continuum | Install Code… | … | Launch Assistant | Manage Apps in Console…
Copy Address ▸ http://127.0.0.1:8080 | http://192.168.1.23:8080 (Wi-Fi) | http://mymac.local:8080
Workflows ▸
Pause Workflows
[Memory 82.8 GB of 128 GB (65%)]
[GPU 0% busy]
Models ▸
[Loaded: 1 · 249 MB · 45.2 GB free]
✓ Qwen1.5-0.5B-Chat-4bit · 249 MB · MLX ▸
Eject — frees 249 MB
Load a Model ▸
[Your defaults]
☑ Text: Qwen1.5-0.5B-Chat-4bit · 260 MB · MLX · loaded
MLX (11) ▸ recognised models first, then A–Z
LM Studio (13) ▸
Ollama (1) ▸
Hugging Face (126) ▸ Recognised models (3) ▸ | A – F (30) ▸ | F – Q (30) ▸ | …
Download Models in Console…
Manage Models in Console…
Check for Updates…
Restart AbstractGateway…
☐ Start AbstractGateway at login
Network ▸ [Now: localhost only · 127.0.0.1:8080] | ● Localhost only | ○ Local network | ○ Internet…
Help ▸
Quit AbstractGateway…
The icon itself is a live gauge: the outer ring fills with system memory in
use (blue), the inner ring with GPU load (amber). A green dot in the centre
means a workflow step is executing right now; pause bars mean paused; a red
ring with ! means the gateway is not answering.
Everything works offline except Documentation, Report a Problem and
Check for Updates, which open a website or check for a newer release (see
Restart and update). The documentation
is online; the console's docs assistant answers from the llms.txt shipped
with the gateway.
Install¶
The tray is an optional extra so servers never pull GUI libraries:
pip install "abstractgateway[tray]" # pystray + Pillow
| Platform | What else is needed | Notes |
|---|---|---|
| macOS | nothing (pystray installs the pyobjc Cocoa bindings) |
Retina-crisp icon; native alerts. |
| Windows | nothing | The icon may start in the tray overflow (the ^ chevron); drag it out to pin it. |
| Linux | the GTK/AppIndicator bindings: sudo apt install python3-gi gir1.2-ayatanaappindicator3-0.1 (Debian/Ubuntu) |
GNOME needs the AppIndicator and KStatusNotifierItem extension; KDE, XFCE, Cinnamon work out of the box. Pure Wayland sessions without a status-notifier host have no tray. |
The Activity window uses tkinter from the Python standard library. Some
Python builds ship without it (Homebrew: brew install python-tk; Debian:
sudo apt install python3-tk; pyenv builds need the Tk headers at build
time). When it is missing, the item is absent; the console's Resources tab
shows the same graphs.
When the icon appears¶
At abstractgateway serve time the gateway decides, and says why on stderr:
While the gateway runs, the icon is there. There is no setting to turn it off and no Hide item in its menu, because the icon is how people who never open a terminal reach their gateway. It is absent only for one of these reasons:
| Situation | Outcome |
|---|---|
abstractgateway[tray] not installed |
not started; the install hint is printed |
| No display (SSH session, container, Windows service, macOS daemon, CI) | not started (reason headless) |
serve --reload (development) |
not started (the app runs in uvicorn's reloader child) |
A runner-only process (abstractgateway runner) |
not started; the tray belongs to the process that serves the console |
serve --no-tray (a test or scratch gateway next to your usual one) |
not started for this run (reason no_tray_flag) |
| Otherwise | started; Desktop tray: started (pid …) |
The console's Resources → Gateway card reports which of these applies, as
plain text. If the helper itself crashed, POST /api/gateway/host/tray/show
(admin) retries it without restarting the gateway.
Everything the tray shows is also in the console's Resources tab: the Gateway card (pause/resume, update, restart, quit), the memory and GPU meters, and the model table with unload buttons. A paused gateway shows a banner on every console tab with a Resume button.
Models¶
Models ▸ leads with what is in memory — a header (Loaded: N · size ·
free memory), then one row per loaded model. Each loaded row is a submenu
whose one action is Eject — frees N GB: a click on a check-marked row
never unloads by surprise. Eject asks first; when work is running on the
gateway it says so, because eject stops the calls running on that model first
and the next request that needs it loads it again (the gateway's eject
semantics). A locked model ("kept in memory") asks again before it goes.
Load a Model ▸ preloads an installed model (POST
/api/gateway/models/load; the gateway pins it resident):
- Your defaults — the models your capability routes name (Text, Image, Voice, Speech to text, Music), loaded under that route's task. A default that is not downloaded is shown greyed with its status, never offered as a click that fails.
- One submenu per engine — MLX, LM Studio, Ollama, Hugging Face — with
every model the engines hold on this machine (
GET /models/installed), size on disk included. Models the catalog recognises come first, then A–Z. A list longer than 30 is split into ranges (A – F (30) ▸); nothing is ever left out. A model larger than the free memory says so, and loading it asks first. Embedding models are listed greyed ("load on use"): the residency API has no embedding task.
Every load shows Loading X while it runs (a notification and a greyed row), then Loaded X after N s — or a dialog with the gateway's full reason when it fails.
Installed models are grouped by engine rather than by capability: the engine is always known and decides how a model loads, while many installed artifacts carry no capability metadata. Capabilities appear where they are known: the routes you configured, under Your defaults.
When no model is listed but the gateway process still holds more than 256 MB
of accelerator memory (every model library in the gateway: MLX, llama.cpp,
transformers, embeddings), the header adds gateway holds N GB and the menu,
the status line and the Activity window say "Gateway still holds N GB (no
model listed)" instead of "No models loaded", followed by how the figure was
measured ("Measured by metal device counter", "cuda device counter" or "sum of
MLX + llama.cpp") and what holds it ("Held by [mlx] qwen/27b × 2 holders", or
"Not attributed to any model"): eject the held model in the Console, or
restart the gateway to free it. With models loaded, the menu still says how
the gateway's memory is measured.
After a default-model switch, the Models menu and the Activity window also list the ejects the gateway still owes or that failed: "Will eject X when the in-flight call ends", "X: eject failed: reason", "X kept in memory: reason", "X ejected".
The model lists refresh every 5 minutes and after each load or eject; the Activity window and the console's Models tab show the same data live.
Apps¶
The Apps submenu lists Observer, Continuum, Code, Entity, Flow (the
stack order and ports of scripts/start-local.sh: 3001-3005) and Assistant,
one short line each and never a reason: the full reason lives in the
console's Apps tab. Presence is detected, never imported — the gateway does
not depend on its apps:
| App | Detected by | Line |
|---|---|---|
| Observer, Continuum, Code, Entity, Flow | the gateway (GET /api/gateway/apps): its own installs, and apps started outside it (the dev stack, npx, a service) found on their usual port |
Open X when running, whoever started it (a one-time signed-in handover); Open X when installed and stopped (starts it, then opens it); Install X… when it can be installed (the browser app and, for Code when a ready-made download exists for this computer, its terminal app, as one job; a notification when it is done, then Open X); otherwise X, greyed |
| the same, installed globally | the app's command on PATH (abstractobserver, abstractflow-editor, abstractcode-web, abstractcontinuum, abstractentity) or the package under npm root -g |
Open X — started by the tray with the gateway URL passed in and stopped when the tray exits; sign in inside the app (install it here instead for the one-click sign-in) |
| Assistant | the same detection as the console's Assistant card (apps_desktop.detect_assistant): AbstractAssistant.app in /Applications or ~/Applications, the abstractassistant command (this Python's scripts folder, or PATH), or the package in this Python (importlib.util.find_spec, without importing it) |
Launch Assistant when found (also while it runs): the installed package, else the app bundle — the one the console's card describes; when the other one runs, the line under it says so ("Another Assistant is running: /Applications/AbstractAssistant.app 0.5.0 — quit it to use 0.13.0"). The gateway opens it (POST /api/gateway/apps/assistant/launch, the console's Open), connected and signed in to this gateway; Install Assistant… when the gateway can install it into its own Python (the console's Install); otherwise Assistant, greyed |
| Code's terminal version (the only app with one today) | the gateway's presence check, reported as interfaces[kind="tui"] on the app row: its terminal-apps folder (uv's tool bin dir, shared with the installer; <data dir>/apps/bin/ for a gateway that is not a uv tool install), abstractcode on PATH, or ~/.cargo/bin |
Open Code in Terminal — a new terminal window on this machine, signed in through a one-time code (POST /api/gateway/apps/code/launch-tui, the same route as the console's button). Shown only when the gateway reports it installed; greyed when the gateway would refuse (the console says why) |
When an app cannot be installed from here, ONE line near the bottom says so: "Installs are off for this gateway · Console → Apps" (the gateway's install setting refuses this caller), or "Installs unavailable now · Console → Apps" (for example the npm registry is unreachable). The tray is on the gateway machine, so with the default setting its installs are allowed whatever address the gateway listens on. Manage Apps in Console… at the bottom opens the console's Apps tab (updates, logs, stop, and the full reasons).
The tray talks to its gateway over loopback (http://127.0.0.1:<port>) for
every network mode; the network address in the menu's header and in
Copy Address is for other devices.
A folder called abstractassistant in the gateway's working directory (a
source checkout) resolves as a namespace package; it is not an install and
is ignored. Apps started by the tray get an environment with every token,
secret, password and key removed, like the apps the gateway runs itself.
Start at login¶
Start AbstractGateway at login is checked only when THIS gateway (its data folder) would really start at your next login. Toggling it registers or removes the same per-user login item as the CLI and the installers:
| OS | Mechanism (per user, no admin) | "On" means |
|---|---|---|
| macOS | LaunchAgent ~/Library/LaunchAgents/ai.abstractframework.gateway.plist (RunAtLoad) |
the plist parses, the program it starts exists, and launchctl print-disabled does not list it as disabled |
| Linux (systemd) | user unit ~/.config/systemd/user/abstractgateway.service |
the program exists and systemctl --user is-enabled says enabled |
| Linux (no systemd user manager) | XDG autostart entry ~/.config/autostart/abstractgateway.desktop (graphical login) |
the program exists and the entry is not switched off (X-GNOME-Autostart-enabled=false, Hidden=true) |
| Windows (experimental) | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\AbstractGateway → pythonw -m abstractgateway.os_service launch … |
the program exists and Task Manager's Startup apps has not disabled it |
The item reads — needs repair when a registration exists but would not
start (the gateway was moved or reinstalled elsewhere, the file is unreadable,
the unit is disabled); the line under it says why, and a click repairs it. It
reads (another gateway is registered) when the login item belongs to
another data folder; a click asks before replacing it. A registration that
pins --host/--port on its command line also reads — needs repair
("pinned to 127.0.0.1:N by the login item …"): it starts, but the Network
choice cannot apply to it. The click rewrites it to plain serve and keeps
the stored network mode.
Turning it on registers for the next login: it never starts a second copy of the gateway that is already running. Turning it off only unregisters: the gateway keeps running now. The same switch from a terminal:
abstractgateway service status # on | off | broken | other, and why
abstractgateway service enable # this gateway at next login; the Network setting binds it
abstractgateway service disable # the running gateway keeps running
abstractgateway service install # enable + start now + wait for health (installers)
abstractgateway service uninstall # disable + stop it
Network and addresses¶
The line under the status header is the address to share and the network
mode, e.g. http://127.0.0.1:8080 · localhost only (· restart required
while a change waits for a restart). Network ▸ shows what runs now
(Now: local network · 0.0.0.0:8080) and three choices — Localhost only,
Local network, Internet… — whose mark is what is set. Choosing one
saves it (POST /api/gateway/network); when it needs a restart, Restart to
apply appears (and the headers say restart required). Internet… first
shows what exposing the gateway means, with the gateway's own warnings, and
posts only after you acknowledge it. A refusal (for example sign-in not set up
for that mode) opens a dialog with the reason and the fix. Copy Address ▸
lists every address the gateway answers on (loopback, each network interface,
the machine's .local name) and copies the one you click.
A gateway without the network settings route says so in the Network submenu, and Copy Address still offers the address the tray talks to.
Menus without submenus¶
pystray draws submenus and check marks on macOS, Windows and Linux
(AppIndicator). Some Linux panels drop submenus: set "flat_menu": true in
<data_dir>/tray/prefs.json and restart the gateway — every row is kept,
prefixed with its path (Models › Load a Model › MLX › …). pystray's plain
X11 backend (xorg) has no menu at all: clicking the icon opens the console,
and a notification at start names the CLI equivalents (abstractgateway
service …, apps …, models …).
Pause¶
Pausing is process-wide and persists across restarts: a laptop paused to get its GPU back does not silently resume after a reboot or an update. While paused:
- no new workflow step starts — runs, schedules and bridge-started work are accepted and wait;
- a step already inside an LLM or tool call finishes first (the menu says "Finishing N runs at the next step");
- the console, the API and connected apps keep answering; cancelling a run still works;
- summoned entities' own-time loops are not affected (they are separate processes with their own lifecycle controls);
GET /api/healthcarries"paused": truewhilestatusstays"healthy"— a supervisor must never recycle a paused gateway.
Pause reaches inside a tick: AbstractRuntime's Runtime.tick(step_gate=…)
consults the gateway's gate at every step boundary. Where the runtime does
not offer that gate, the pause takes effect at tick boundaries (up to
tick_max_steps steps later); GET /host/runner reports
step_gate_supported and the menu says so.
In the split layout (serve --no-runner + abstractgateway runner) the
pause is written to <data_dir>/gateway_paused.json and the runner process
picks it up within about two seconds.
Restart and update¶
Restart asks uvicorn for its normal graceful shutdown (runner drain,
entity close), then relaunches the same command in the same environment
(python -m abstractgateway …). On macOS and Linux the process keeps its PID
and terminal; on Windows a new process is spawned on the same console. Restart
is refused (HTTP 409, greyed out in the tray) under serve --reload, when the
server was not started by abstractgateway serve, or while an update is
being installed.
Check for Updates (the tray), Check now (web console, Resources >
Gateway > Version) and u in the terminal console's F3 panel all ask the
gateway the same question and show its answer word for word: the version line,
a hint, and Update to … with a confirmation that says exactly what runs.
The check takes at most a few seconds, runs at most once per hour unless you ask
again, and offline is a normal answer, not an error. Updating is for admins.
What the check compares and what Update runs depends on how the gateway was installed:
| Install | Compared with | Update runs | One-click? |
|---|---|---|---|
| AbstractFramework installer (the one-line install, the macOS double-click installer) | the newest AbstractFramework release | the AbstractFramework installer (install.sh), see below |
yes on macOS and Linux; Windows shows the PowerShell line |
pip in a virtual environment |
the newest abstractgateway on PyPI |
python -m pip install --upgrade "abstractgateway[<your extras>]" |
yes |
uv venv / uv pip |
PyPI | uv pip install --python … --upgrade … |
yes |
pipx |
PyPI | pipx upgrade abstractgateway |
yes |
uv tool (installed by hand, no version pin) |
PyPI | uv tool upgrade abstractgateway |
yes |
uv tool pinned to one version (==X) |
PyPI | — reinstall without the pin (the hint gives the command) | no |
editable checkout (pip install -e .) |
PyPI | — update with git pull |
no |
| Docker image | PyPI | — pull the newer image | no |
| system Python (PEP 668 "externally managed") | PyPI | — use pipx or a venv | no |
An AbstractFramework installer install¶
The gateway recognises an installer install by the installer's record in its
data folder (bootstrap.env), which also names the AbstractFramework release
installed. Update then runs the same installer as the one-line install,
so updating from the tray, either console or by running the line again in a
terminal does the same thing:
- The release. The check reads the AbstractFramework repository's
mainbranch at one commit: its install manifest (docs/installers/install-manifest.json, the release version and the gateway version it pins) and itsscripts/install.sh. The line readsAbstractFramework 0.6.1 · gateway 0.7.1 · AbstractFramework 0.6.2 available. An install made with--pinrecords no release; it is offered the release only when the release's gateway is newer than the one installed. - What runs. The confirmation names the script's address
(
https://abstractframework.ai/install.sh), the commit and the script's sha256, and the command:/bin/sh install.sh --yes --no-start --no-open --no-modify-path --data-dir <data folder>. The gateway runs exactly the file it checked, written to a new file of its own for that run; the start must name the sha256 the confirmation showed, and if a newer check fetched a different script in between, Update refuses and asks you to check again. One update runs at a time. - How it runs. Nothing is asked and nothing is opened. Start at login stays
as it is. The installer keeps your profile, port and the choices it remembers,
moves every package to the release's tested versions, and does not stop the
running gateway. Its output is the update log (console: Update log under the
version line; the tray shows its last lines if it fails). The installer's own
log is the newest
install-*.login<data folder>/logs/. - The result. What moved (
AbstractFramework 0.6.1 -> 0.6.2, abstractgateway 0.7.1 -> 0.7.2, …) and Restart to finish; Already up to date when the installer changed nothing (no restart is offered); or the installer's exit code with the last lines of its log. The running gateway keeps working in every case. - Windows keeps a running program's files locked, so the gateway cannot update itself in place. The hint shows the line to paste in PowerShell; the installer stops the gateway, updates everything and starts it again:
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"
The newest gateway outside a release is a command-line choice: run the installer
with --pin latest (see the AbstractFramework install guide).
Other installs¶
The installed extras (apple, gpu, embeddings) are detected and kept.
The upgrade runs in the background (its log is under the console's version
line); Already up to date is reported when the package manager installed
nothing newer.
In every case the running process keeps serving the old code until it restarts: the tray offers Restart Now, the console's Gateway card Restart gateway….
Security¶
The tray talks to the gateway over loopback HTTP with a per-process
ephemeral admin token handed over on the helper's stdin — never on the
command line, never in the environment, never on disk. The token is accepted
only from a loopback socket peer (127.0.0.0/8, ::1) and dies with the
process. Audit-log entries made through it carry
source: loopback-ephemeral:desktop-tray.
Signed-in links. Open Console does not ask the gateway to sign anyone
in — there is no such endpoint, by design. The tray writes a one-time claim
code under <data_dir>/auth/claims/ (only someone who can write the data
folder can), valid 2 minutes, and opens /console#claim=<code>; the console
redeems it once, from a loopback browser only. A gateway running with a static
token (no user accounts) cannot redeem claims, so the plain URL opens and a
notification says why. Copy Console Link copies the plain URL: a sign-in
link does not belong on a clipboard. App links use the gateway's one-time
handover (POST /api/gateway/apps/{id}/open, 2 minutes, single use).
FORWARDED_ALLOW_IPS=* (uvicorn's proxy setting) would let any client rewrite
its peer address; the gateway warns at boot when it sees that with a tray
running. Use a concrete proxy IP.
Troubleshooting¶
See troubleshooting.md for a missing icon. Other cases:
- The helper starts then disappears: read
<data_dir>/logs/tray.log;GET /api/gateway/host/trayreportsexit_codeand the readiness failure. After two crashes in a row it is not restarted automatically;POST /api/gateway/host/tray/show(admin) starts it again. - The icon says "Not responding": the gateway is restarting, stopped or unresponsive. Force Quit in that state sends the gateway process SIGTERM and, after five seconds, kills it.
Licensing note¶
pystray is LGPL-3.0 and python-xlib (Linux) is LGPL-2.1. AbstractGateway
imports them dynamically as ordinary dependencies, which is compatible with
its MIT license. A frozen single-file build (PyInstaller and the like) would
have to honour the LGPL relinking terms.