# AbstractGateway — llms-full

This file is a single-document snapshot of the local Markdown files (`*.md`) linked in `llms.txt`, intended for LLM/agent ingestion.
It is generated by `scripts/generate-llms-full.py` in the same order the links first appear in `llms.txt` (de-duplicated).
Relative links are normalized to repo-root paths.

---

## docs/first-run.md

# AbstractGateway — First run

This page covers the shortest path from `pip install abstractgateway` (or
`uv tool install abstractgateway`) to a signed-in console on your own machine:
no environment variables, no token to copy. It also covers starting the gateway
at login. For server deployments, see [deployment.md](docs/deployment.md); for
every setting, see [configuration.md](docs/configuration.md).

## 1. Start the gateway

```bash
abstractgateway serve
```

With no auth configured, `serve`:

- binds **127.0.0.1** (port 8080; use `--port` to pick another);
- turns on **user auth** and creates the admin user `default/admin`;
- keeps its data in the per-user data folder for your OS (see
  [Where the data lives](#where-the-data-lives));
- prints a short banner:

```text
Gateway data dir: /Users/you/Library/Application Support/AbstractGateway (os_default: ...)
Gateway auth: user auth enabled automatically (bound to loopback 127.0.0.1, no auth posture in this gateway's environment): ...
Gateway admin token file: .../auth/bootstrap-admin-token
First run: open http://127.0.0.1:8080/console#claim=agclaim_...
           (one-time link, valid 10 minutes, works from this machine only; ...)
```

Open the `First run` link in a browser on the same machine. The console signs
you in as the admin and opens the **first-run guide**.

The admin token is printed just above the link, and kept in
`<data dir>/auth/bootstrap-admin-token` (file mode `0600`). Start with
`abstractgateway serve --no-print-token` to keep it out of the output.

## 2. The first-run guide

The guide has five steps. Every step is optional:

| Step | What it shows |
|---|---|
| Welcome | This machine (memory, GPU), the data folder and why it was chosen, the sign-in mode, whether the gateway starts at login |
| Local engines | The engines found on this machine (Ollama, LM Studio, MLX, llama.cpp, ...), whether each is installed and running, and an **Install** button. The confirmation shows the exact command before anything runs |
| Default model | The text model currently configured, **Use recommended defaults** (the same action as the Multimodal tab's *Apply recommended*), a **Download** button for each recommended model that is missing, and the Models tab's catalog cards with **Fits this computer** on (one card per model, every 4-bit and 8-bit build, filters, **Open in the Models tab**). A downloaded text model's **Use as default** makes it the default text model |
| Apps | "Apps that work with this gateway": one card per browser app (Observer, Continuum, Code, Entity, Flow Editor), then the desktop **Assistant**: name, status, one line of description and one row of buttons at the same height on every card: **Install** when it is not installed (on Code it installs the terminal app too, when a ready-made download exists for this computer; nothing opens by itself), then **Open** (Open also starts a stopped app), plus **Open in Terminal** on Code when its terminal app is installed. The Assistant opens on the gateway's computer only. Stop, Show log, Update, versions and commands appear with **Technical details** (see [console.md](docs/console.md#apps-tab)) |
| Done | How to reopen the console, the login-service status, one line saying the console also exists as a terminal app, and (with **Technical details**) the CLI equivalents and the terminal console's install and open commands |

**What "recommended" means on this computer.** AbstractCore picks the
recommended text model; the gateway shows that pick and holds no list of its
own. On a Mac with Apple silicon the pick is an MLX build chosen by the
computer's memory:

| Memory | Recommended text model |
|---|---|
| less than 24 GB | `mlx-community/Qwen3.5-9B-MLX-4bit` (Qwen3.5 9B) |
| 24 GB up to, but not including, 128 GB | `mlx-community/Qwen3.8-27B-4bit` (Qwen3.8 27B) |
| 128 GB or more | `mlx-community/Qwen3.8-Flash-Next-4bit` (Qwen3.8 Flash-Next) |

LM Studio and Ollama builds stay in the catalog and can be downloaded, but on
a Mac they are not the recommendation. Other computers keep the LM Studio
build `qwen/qwen3.5-9b@4bit`. When AbstractCore's memory estimate says the
recommended model may not fit, the **Chat and text** card says so with the
estimate; the recommendation does not quietly switch to another model.

A typical path from a fresh install to a working local model:

1. **Local engines.** If no engine is installed, click **Install** on Ollama
   or LM Studio. The confirmation shows what will run and that it runs on
   this machine (on a Mac: download Ollama's signed app and place it in
   Applications; see [engines.md](docs/engines.md)). The install runs in the background with progress; when it
   finishes the row shows the version.
2. **Default model.** The catalog opens on models that fit this machine's
   memory, one card per model with its 4-bit and 8-bit builds. Click
   **Download** on the build you want; its progress bar shows on the row.
3. When the download finishes, the row reads *Downloaded*. Click **Use as
   default**: the gateway's text route now uses it.

The same steps are available later in the **Engines** and **Models** tabs (see
[console.md](docs/console.md#models-and-engines-tabs)) and from the command line
(`abstractgateway engines install ollama`, `abstractgateway models download
ollama qwen3:8b`). Installing an engine needs an admin and the
[`allow_engine_install`](docs/configuration.md#allow_engine_install) setting, which
is on by default for a gateway that listens on this machine only, and for
someone at the gateway machine whatever it listens on.

The guide opens by itself once per data folder. **Finish** or **Skip setup**
records that it ran (`POST /api/gateway/host/first-run`); clicking outside the
dialog closes it without recording anything. The **Setup** button (⚑, top
right, admins only) reopens it at any time.

## 3. Get a new sign-in link

A link works once and expires after 10 minutes. To get another one:

```bash
abstractgateway claim            # prints the link
abstractgateway claim --open     # prints it and opens your browser
abstractgateway-config claim-url # same command, from the config helper
```

The command finds the running gateway's port from the data folder (the gateway
writes `<data dir>/run/gateway-serve.json` while it runs). Use `--port` or
`--url` to target another gateway, `--data-dir` for another data folder, and
`--json` for machine-readable output. It exits with code `2` when the running
gateway does not use user auth (a static `ABSTRACTGATEWAY_AUTH_TOKEN`
deployment), because that gateway would refuse the link.

How the link is protected:

- only someone who can write the data folder can mint one (the CLI writes it
  under `<data dir>/auth/claims/`, stored as a SHA-256 digest only);
- `POST /api/gateway/session/claim` redeems it **only** from a loopback socket
  peer, and refuses any request carrying proxy headers (`X-Forwarded-For`,
  `X-Forwarded-Host`, `X-Real-IP`, `Forwarded`);
- the console removes the code from the address bar before sending it;
- the result is the same browser session as a normal sign-in (session cookie +
  CSRF cookie). The response's `claim.created_by` says who minted the link
  (`serve`, `cli` or `tray`), so the console can treat a tray sign-in
  differently from a first run.

## 4. Start the gateway at login (optional)

```bash
abstractgateway service install      # install and start
abstractgateway service status       # on | off | broken | other, and why
abstractgateway service enable       # start THIS gateway at the next login (starts nothing now)
abstractgateway service disable      # stop starting it at login (the running gateway keeps running)
abstractgateway service uninstall    # stop and remove (your data is kept)
```

The desktop tray's **Start AbstractGateway at login** item is the same switch
(`enable`/`disable`, shared module `abstractgateway.autostart`). `status`
reports `broken` when a registration exists that would not start — the
program it points at is gone (a moved or reinstalled gateway), the file is
unreadable, the unit is not enabled, or launchd / Task Manager / the desktop
switched it off — and `other` when it belongs to another data folder. It also
reports `broken` with **needs repair** when a registration pins
`--host/--port` on its command line, so the Network setting cannot apply:
"pinned to 127.0.0.1:N by the login item — run `abstractgateway service
enable` again to let the Network setting apply". `service enable` rewrites it.

| OS | What is installed | Logs |
|---|---|---|
| macOS | LaunchAgent `~/Library/LaunchAgents/ai.abstractframework.gateway.plist` (`RunAtLoad`, restarted if it crashes), loaded with `launchctl bootstrap gui/<uid>` | `~/Library/Logs/AbstractGateway/` |
| Linux | systemd user unit `~/.config/systemd/user/abstractgateway.service` (`Restart=on-failure`), enabled with `systemctl --user enable --now` | `journalctl --user -u abstractgateway.service` |
| Linux without a systemd user manager | XDG autostart entry `~/.config/autostart/abstractgateway.desktop` (starts at graphical login) | `<data dir>/logs/gateway.log` |
| Windows (experimental) | per-user Run entry `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\AbstractGateway` that starts the gateway with `pythonw.exe` (no console window, no admin) | `<data dir>\logs\gateway.log` |

Details:

- The service runs the absolute path of the `abstractgateway` you installed
  and sets `PATH` itself (including `~/.local/bin`, `~/.lmstudio/bin`,
  `/opt/homebrew/bin`, `/usr/local/bin`), because service managers do not read
  your shell profile.
- The login item runs plain `abstractgateway serve`: the host and port come
  from the [Network setting](docs/configuration.md#network-exposure-localhost--local-network--internet)
  (`abstractgateway network set localhost|lan|internet [--port N]`, the tray's
  Network menu, the console) at every start. `install`/`enable` store it
  first: a stored mode and port are kept; otherwise the mode is `localhost`
  (127.0.0.1) and the port is `--port` when given, else the running
  gateway's (`enable`), else a previous install's, else the first free port
  from 8080 upwards. `--host 127.0.0.1|0.0.0.0` and `--port` are written into
  that setting and printed. `--pin-command-line` puts `--host/--port` on the
  command line instead (for technical setups; the Network setting then does
  not apply). The registration is recorded in `<data dir>/service.json`.
- After starting, `install` waits up to 60 seconds for `/api/health`
  (`--wait-s`, `--no-wait`) and, on a first run, prints a sign-in link
  (`--no-claim` to skip).
- `--dry-run` prints the files and commands without changing anything.
  `--no-start` registers the service without starting it now.
- On Linux, a user unit runs while you are logged in. To keep it running after
  logout or start it at boot, run `loginctl enable-linger "$USER"` once.

## Where the data lives

When `ABSTRACTGATEWAY_DATA_DIR` is not set, the gateway uses, in order:

1. `./runtime`, if it already exists in the working directory (repository
   checkouts and the AbstractFramework workspace scripts);
2. the per-user data folder for your OS:
   - macOS: `~/Library/Application Support/AbstractGateway`
   - Linux: `$XDG_DATA_HOME/abstractgateway` (default `~/.local/share/abstractgateway`)
   - Windows: `%LOCALAPPDATA%\AbstractGateway`

`abstractgateway-config status` prints the folder and the reason it was
chosen. `serve --data-dir <dir>` or `ABSTRACTGATEWAY_DATA_DIR` choose it
explicitly.

## Exposing the gateway beyond this machine

Choose who can reach the gateway with the network setting:

```bash
abstractgateway network set lan                            # this machine + your local network
abstractgateway network set internet --acknowledge-internet
abstractgateway network restart                            # apply it now
```

The console's **Network** tab and the tray's **Network** menu change the same
setting. User accounts stay on in every mode; the gateway does not terminate
TLS, so put a reverse proxy or a tunnel in front for `internet`. See
[configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet)
and [security.md](docs/security.md#network-exposure).

An explicit `serve --host 0.0.0.0` (or any non-loopback address) refuses to
start unless auth is configured (`ABSTRACTGATEWAY_USER_AUTH=1` or
`ABSTRACTGATEWAY_AUTH_TOKEN`).

## Checking the setup from scripts

`abstractgateway-config status --json` includes these keys (schema
`gateway_config_status_v1`):

| Key | Meaning |
|---|---|
| `data_dir`, `data_dir_source`, `data_dir_reason` | The data folder, and `env`, `legacy_cwd_runtime` or `os_default` |
| `auth_mode`, `auth` | `users`, `token`, `users+token`, `open` or `loopback_auto` (nothing configured: `serve` on loopback enables user auth) |
| `service` | The login service: `installed`, `mechanism`, `unit_path`, `port`, `url` |
| `claim_pending`, `claims` | Whether an unused, unexpired sign-in link exists |
| `first_run` | Whether the first-run guide was completed |
| `serve` | The running gateway for this data folder (`url`, `port`, `pid`, `alive`, its `auth`), or `null` |

`GET /api/gateway/host/state` carries the same facts in its `gateway` block.

## Related docs

- [getting-started.md](docs/getting-started.md): runs, bundles, stores
- [configuration.md](docs/configuration.md): every environment variable and CLI flag
- [console.md](docs/console.md): the web console and the terminal console
- [security.md](docs/security.md): auth, origins, limits
- [tray.md](docs/tray.md): the desktop tray icon
- [troubleshooting.md](docs/troubleshooting.md): sign-in links, login service, network modes

---

## docs/configuration.md

# AbstractGateway — Configuration

AbstractGateway is configured in three places:

- **Runtime settings**, stored in the data dir and changed from the web
  console, the terminal console, the tray or the CLI (`abstractgateway
  network`, `abstractgateway apps config`, `abstractgateway config
  get|set|unset`): network exposure, reverse proxy, browser apps, engine
  installs, backlog folder, exec runner, process manager, stop kill switch.
- **AbstractCore configuration** for capability defaults, provider keys and
  other values AbstractCore owns (see [Two entry points, one store](#two-entry-points-one-store)).
- **Launch flags and environment variables** for deployment choices such as
  the data dir, auth mode, stores and limits.

This page is the reference for all three.

## Install extras (recommended)

The base install (`pip install abstractgateway`) is the remote-light server
profile: HTTP/SSE, durable stores, `AbstractRuntime`,
Runtime-owned provider/tool and multimodal support, AbstractAgent, AbstractFlow
compatibility (runs bundles produced by AbstractFlow; does not require the
`abstractflow` package), and AbstractMemory/LanceDB KG support. Local
sentence-transformer embeddings and hardware-local inference engines are
opt-in, so the base Linux install does not pull PyTorch/CUDA packages.

Remote embeddings are part of this base light profile. Configure
`embedding.text` for OpenAI, OpenRouter, Portkey, LM Studio, vLLM, another
OpenAI-compatible embeddings endpoint, or a remote AbstractCore server. The
`abstractgateway[embeddings]` extra is only for local HuggingFace/
sentence-transformer embeddings on the Gateway host.

Optional extras (see `pyproject.toml`):
- `abstractgateway[embeddings]`: local sentence-transformer embeddings for semantic KG queries
- `abstractgateway[apple]`: full native macOS Python profile with Apple-local engines and all non-NVIDIA framework capabilities; this is for native macOS, not Docker
- `abstractgateway[gpu]`: full native/container GPU profile with local GPU engines and all relevant framework capabilities; the NVIDIA Docker image uses this profile
- `abstractgateway[tray]`: the desktop menu bar / system tray icon shown by `abstractgateway serve` (pystray + Pillow; see [tray.md](docs/tray.md))
- `abstractgateway[docs]`: MkDocs site tooling
- `abstractgateway[dev]`: local dev/test deps

Default dependency floors (see `pyproject.toml`):
- `AbstractRuntime>=0.4.35`
- `abstractcore>=2.15.2`
- `abstractagent>=0.3.13`
- `AbstractMemory[lancedb]>=0.3.0`

Gateway's KG resolver targets AbstractMemory's TripleStore API. It does not use
the newer memory-agent API directly.

## Configuration helper

Gateway has a first-class configuration helper:

```bash
abstractgateway-config status
abstractgateway-config init --env-file .env
abstractgateway-config bootstrap-admin --print-token
abstractgateway-config claim-url [--open]
abstractgateway config status --json
```

`claim-url` (also `abstractgateway claim`) prints a one-time console sign-in
link for this machine; see [first-run.md](docs/first-run.md). `status --json`
also reports `data_dir_source`, `auth_mode`, `service`, `claim_pending`,
`first_run` and `serve` (schema `gateway_config_status_v1`, documented in
[first-run.md](docs/first-run.md#checking-the-setup-from-scripts)).

It reports Gateway auth/data/store/runtime defaults, Core-server handoff
configuration, memory-store selection, and package readiness. `init` writes a
private env file for server/operator deployments. Gateway Console (`/console`)
is the preferred place to configure provider connections, provider API keys,
endpoint base URLs, users, and Gateway/user defaults. Provider URLs and keys
belong to the Providers tab; the Multimodal Capabilities tab only chooses an
available provider and a discovered model.
`bootstrap-admin` is the non-interactive setup path used by Docker images:
when user auth is enabled, it ensures `default/admin` exists, stores only the
token hash in `auth/users.json`, and can write the raw bootstrap token to
`auth/bootstrap-admin-token` for first login.

## Core environment variables

### Paths + workflow source

- `ABSTRACTGATEWAY_DATA_DIR`: durable data directory. When unset: `./runtime`
  if it already exists in the working directory, else the per-user data folder
  (macOS `~/Library/Application Support/AbstractGateway`, Linux
  `$XDG_DATA_HOME/abstractgateway` or `~/.local/share/abstractgateway`,
  Windows `%LOCALAPPDATA%\AbstractGateway`). `serve --data-dir` sets it for
  one process. `abstractgateway-config status` prints the folder and why it
  was chosen.
  Evidence: `src/abstractgateway/host_paths.py`
- `ABSTRACTGATEWAY_FLOWS_DIR`: workflows directory. When unset, Gateway uses the
  packaged shipped bundle directory, which carries `basic-agent`,
  `coding-agent`, `deep-research`, `co-scientist`, and more
  ([shipped-workflows.md](docs/shipped-workflows.md)). If the shipped bundles are
  unavailable, Gateway fails clearly instead of starting with an empty default
  registry. Setting this replaces the shipped registry with your own directory.
  Evidence: `src/abstractgateway/config.py`
- `ABSTRACTGATEWAY_WORKFLOW_SOURCE`: `bundle` (the default and only
  supported source)
  Evidence: `src/abstractgateway/service.py` (`create_default_gateway_service`)

### Authentication and user routing

**Default on loopback.** When no auth setting is present (none of
`ABSTRACTGATEWAY_AUTH_TOKEN[S]`, `ABSTRACTGATEWAY_USER_AUTH`,
`ABSTRACTGATEWAY_MULTI_USER`, `ABSTRACTGATEWAY_AUTH_MODE`,
`ABSTRACTGATEWAY_SECURITY`, `ABSTRACTGATEWAY_PROTECT_WRITE`), `serve` binds
`127.0.0.1` and enables user auth automatically. A non-loopback bind in that
state refuses to start. When any auth setting is present, `serve` keeps the
`0.0.0.0` default bind and your settings apply unchanged.

The normal browser-console/browser-app path uses Gateway user auth:

- `ABSTRACTGATEWAY_USER_AUTH=1` or `ABSTRACTGATEWAY_AUTH_MODE=users`: enable
  file-backed user principals and per-principal runtime routing
- `abstractgateway serve`: when user auth is enabled, ensures `default/admin`
  exists and writes the first-login token to
  `<ABSTRACTGATEWAY_DATA_DIR>/auth/bootstrap-admin-token` (mode `0600`). The
  token is printed on a loopback bind and hidden on other binds;
  `serve --print-token` / `--no-print-token` override that
  (`ABSTRACTGATEWAY_BOOTSTRAP_PRINT_TOKEN=1` is accepted as an alias of
  `--print-token`). Until
  the first-run guide is completed, a one-time sign-in link
  (`/console#claim=...`, 10 minutes, single use, loopback only) is printed
  instead
- `POST /api/gateway/session/claim`: redeems a one-time link code for an admin
  browser session; accepted only from a loopback peer without proxy headers.
  The response carries `claimed: true`, `first_run` (the guide state) and
  `claim: {created_by}`, which says who minted the link: `serve` (first run),
  `cli` (`claim-url` / `abstractgateway claim`) or `tray` (tray sign-in).
  `GET /api/gateway/host/first-run` / `POST` (admin) read and record the
  first-run guide state

Server/operator token mode uses a shared Gateway bearer token:

- `ABSTRACTGATEWAY_AUTH_TOKEN`: single Gateway admin token
- `ABSTRACTGATEWAY_AUTH_TOKENS`: comma-separated Gateway admin tokens

That shared bearer token maps to `local-admin` and is not accepted by browser
sign-in flows such as `/console` or AbstractFlow. User-auth mode resolves
Gateway user bearer tokens to principals and routes each principal to a separate
service/data plane:

- `ABSTRACTGATEWAY_USER_AUTH_AUTO=1`: compatibility mode that also enables
  user auth when the registry file already exists
- `ABSTRACTGATEWAY_USERS_FILE`: optional user registry path; default:
  `<ABSTRACTGATEWAY_DATA_DIR>/auth/users.json`
- `ABSTRACTGATEWAY_SESSIONS_FILE`: optional browser session registry path;
  default: `<ABSTRACTGATEWAY_DATA_DIR>/auth/sessions.json`
- `ABSTRACTGATEWAY_SESSION_TTL_S`: default browser session lifetime
- `ABSTRACTGATEWAY_REMEMBER_SESSION_TTL_S`: browser session lifetime when a
  browser app requests "remember me"
- `ABSTRACTGATEWAY_ADMIN_USES_DEFAULT_RUNTIME`: keep the default
  `default/admin` admin principal on the Gateway's base data plane when its
  `runtime_id` is `default` or `admin` (default: enabled)
- `GET /api/gateway/me`: returns the resolved principal and routing mode
- `/api/gateway/admin/users`: admin-only user list/create/read/update/delete
- `/api/gateway/admin/runtime-reservations`: admin-only retained runtime
  list/transfer/purge lifecycle
- `/console`: built-in same-origin Gateway Console for session sign-in with
  Gateway user + token, account/runtime summary, admin user management, optional
  account email metadata, token rotation, retained runtime transfer/purge, and
  multimodal capability defaults selected from available providers

User records include `tenant_id`, `user_id`, roles/scopes, enabled state, and a
`runtime_id`. The registry stores password-grade bearer-token hashes only.
Generated or rotated user tokens are returned once from the admin response.
Gateway rejects duplicate `runtime_id` values within the same tenant when users
are created or updated, preserving `1 user = 1 runtime` for independent hosted
users. Deleting a user reserves its retained runtime id. Admins must explicitly
purge retained runtime data before the id can be reused by another user, or
transfer the retained runtime to an existing same-tenant user.

When user auth is active, `src/abstractgateway/service.py` keeps normal users
isolated in a per-principal service directory:

```text
<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant_id>/<runtime_id>/runtime
<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant_id>/<runtime_id>/flows
```

The bootstrap `default/admin` admin principal is a local-setup compatibility
exception by default: with `ABSTRACTGATEWAY_ADMIN_USES_DEFAULT_RUNTIME=1`, it
uses the base Gateway data plane and bundle registry. That keeps the admin
connected to the default runtime and shipped `basic-agent` bundle while regular
users remain on `1 user = 1 runtime` routing.

Browser apps should exchange a Gateway user token for an opaque Gateway browser
session through `/api/gateway/session/login`; the raw bearer token should not be
kept in browser storage, and the login response body does not expose the session
id or CSRF token. Session-authenticated writes carry
`X-AbstractGateway-Session` plus `X-AbstractGateway-CSRF`, and
`/api/gateway/session/logout` revokes the session. Apps such as AbstractFlow,
AbstractCode, AbstractAssistant, and AbstractObserver should authenticate as the
current user/session in hosted mode. They should not share one app-server
Gateway token for all users.

<a id="network-exposure-localhost--local-network--internet"></a>
## Network exposure (localhost / local network / internet)

One setting decides who can reach the gateway. The console, the console TUI
(Connection screen), the tray and `abstractgateway network` all edit the same
runtime-config key (`network`); there is no environment variable for it.

| Mode | Bind | Requires |
|---|---|---|
| `localhost` ("Localhost only") | `127.0.0.1` | nothing: only this machine can connect |
| `lan` ("Local network") | `0.0.0.0` (IPv4) | user auth (accounts + console sign-in) |
| `internet` ("Internet…") | `0.0.0.0` (IPv4) | user auth **and** an explicit acknowledgement |

- **Applied at the next start.** A listening socket cannot move: after a change
  the status says `restart_required: true` with `configured` vs `effective`
  until the gateway restarts (`POST /api/gateway/network/restart`, the tray's
  *Restart AbstractGateway…*, `abstractgateway network restart`, or stop and
  start `serve`).
- **`serve --host/--port` win** over the setting and are reported as
  `effective.overridden_by_cli: true`. A restart replays the same command
  line, so it cannot apply the setting: the status says so
  (`restart.applies: false` + `restart.reason`) and the restart route refuses.
- **The login service lets the setting apply.** The LaunchAgent,
  systemd unit, XDG entry and Windows Run entry written by
  `abstractgateway service install|enable` start plain `serve`: no `--host`, no
  `--port`. Install/enable first **seed** the setting through the same change
  door as `network set` (same auth refusals; a refusal registers nothing):
  nothing stored yet → `localhost` on the chosen port; a stored mode/port →
  kept. `service install|enable --host H
  --port P` are written **into the setting** (`127.0.0.1` → `localhost`,
  `0.0.0.0` → `lan`, or the stored `internet`; a specific address is refused),
  never onto the command line. `--pin-command-line` is the technical escape
  hatch: `serve --host H --port P` on the command line, the setting
  untouched and overridden (`overridden_by_cli`, `service status` names it).
- **A registration that pins `--host/--port` reads "needs repair".** `service
  status` (and the tray's *Start at login — needs repair*) says
  "pinned to 127.0.0.1:N by the login item — run `abstractgateway service enable`
  again to let the Network setting apply". `service enable` (or the tray click)
  rewrites the registration in place, keeping a stored mode. The gateway
  running at that moment keeps its command line until it restarts:
  `abstractgateway service install` restarts it from the new registration, or
  log out and back in.
- **Auth is checked before anything is stored.** `lan`/`internet` are refused
  (HTTP 409, nothing written, `refused_reason` + `fix`) when the gateway was
  started with authentication switched off (`ABSTRACTGATEWAY_SECURITY=0` /
  `ABSTRACTGATEWAY_PROTECT_WRITE=0` in its launch environment), with read
  protection off (`ABSTRACTGATEWAY_PROTECT_READ=0`: unauthenticated reads would
  be answered as the admin, `reason_code: auth_disabled`), or with a posture
  without accounts (a shared token only, or `ABSTRACTGATEWAY_USER_AUTH=0`).
  The `fix` describes that state; a plain start (`abstractgateway serve`, or
  the login item `abstractgateway service enable` registers) has none of
  them: when no auth posture is configured at all (the first-run default),
  `serve` turns user auth on for the network mode and says so on stderr
  (`auth.source: network_setting`).
- **`internet` needs `acknowledge_internet: true`** (CLI
  `--acknowledge-internet`; the TUI and tray ask with a confirm). The gateway
  does not terminate TLS: put a TLS reverse proxy or a tunnel in front
  (Caddy, nginx, Cloudflare Tunnel, Tailscale Funnel, ngrok). Port forwarding
  and firewalls are yours to configure; the gateway changes neither.
- **Browser origins.** In a network mode from the setting, `serve` also allows
  the gateway's own discovered LAN origins (e.g. `http://192.168.1.23:8080`,
  `http://mymac.local:8080`) next to the loopback defaults, so the console can
  sign in from another machine. An address that appears later (new Wi-Fi)
  needs a restart. Your public origin (behind a proxy or tunnel) is a setting:
  see [Reverse proxy](#reverse-proxy-allowed-origins-and-trust-proxy) below.
- **A setting that cannot apply falls back loudly.** If the stored mode's auth
  requirement stops being met (the environment changed), `serve` binds
  `127.0.0.1`, prints `[ERROR] Network exposure 'lan' cannot be applied: … Fix: …`
  and the status carries `effective.blocked_reason`.

### Reverse proxy: allowed origins and trust proxy

Two settings a deployment behind a reverse proxy or a tunnel needs, stored in
the same `network` setting and changed through the same door
(`POST /api/gateway/network`, admin-only, audit-logged). **Both apply to the
next request: no restart.** The security middleware re-reads them per request
(one `stat()` of the settings file; parsed again only when it changed), so a
change from the console, the TUI or the CLI (another process) is live at once.

| Setting | Meaning | Default |
|---|---|---|
| `allowed_origins` | Browser origins whose pages may call the gateway, **added** to the always-allowed `http://localhost:*`, `http://127.0.0.1:*` (and, in a network mode, the gateway's own LAN origins). | none |
| `trust_proxy` | Take the client address from `X-Forwarded-For` (sign-in lockouts, audit log). Only when your own proxy sits in front of every request: otherwise any client chooses the address the gateway sees. | off |

**Validation** (one place, the gateway; every door shows its sentence
verbatim). An origin is `scheme://host[:port]`: `http` or `https`, no path, no
trailing slash, no query, no user info; IPv6 in brackets. It is stored the way
a browser sends it: scheme and host lowercased, the default port dropped
(`https://Gateway.Example.com:443` → `https://gateway.example.com`). `*` (every
origin), a leading `*.` label and a `:*` port are accepted only as typed and
are flagged with a warning. A list with any invalid entry is refused whole
(HTTP 400 `reason_code: invalid_origins`, `errors[{value, error}]`, nothing
written), e.g. `1 origin is not valid (nothing was saved): https://x.example/: no
trailing slash: an origin is scheme://host[:port] (write https://x.example)`.
An empty list clears the setting back to the default.

**The environment override.** `ABSTRACTGATEWAY_ALLOWED_ORIGINS` /
`ABSTRACTGATEWAY_TRUST_PROXY` in the environment a gateway was started with
still decide (a deployment pin, the security carve-out in `env_registry.py`),
and every surface says so: the payload carries `source: "env"` and
`overridden_by_env: true` with `env_name`/`env_value` and a `note` ("This
gateway was started with … in its environment: …"); saving is still allowed
and answers `changed.<field>.applies: "overridden_by_env"` ("Saved, but not in
effect"). The value in the settings applies once the gateway starts without
the variable. The origins `serve` itself exports for a network mode are never
counted as an override.

Status payload (`GET /api/gateway/network`, `reverse_proxy`):

```json
"reverse_proxy": {
  "allowed_origins": {"value": ["https://gateway.example.com"], "source": "setting", "overridden_by_env": false,
                      "effective": ["http://localhost:*", "http://127.0.0.1:*", "https://gateway.example.com"],
                      "builtin": ["http://localhost:*", "http://127.0.0.1:*"], "self_origins": [],
                      "applies": "live", "warnings": []},
  "trust_proxy": {"value": true, "source": "setting", "overridden_by_env": false, "effective": true,
                  "applies": "live", "warning": "Trust proxy is on: …"}
}
```

`source` is `setting` (stored), `env` (the start-time override) or `default`.
From the CLI (another process) the running gateway's environment is read from
its run record (`<data>/run/gateway-network.json`, `proxy_env`), never from the
CLI's own shell.

**Three ways, same semantics** (a headless server over SSH needs only the
last two):

| | Web console | Console TUI | CLI |
|---|---|---|---|
| Where | Network → *Advanced: reverse proxy* | Connection screen, below the addresses | `abstractgateway network …` |
| Add/replace origins | type an origin, *Add origin* (Enter); × on a chip removes it | *browser origins* line: comma-separated list, Enter saves, empty clears | `set --allowed-origins https://a,https://b` (`""` clears) |
| Trust proxy | *Trust the proxy's client address* switch | checkbox (Space) | `set --trust-proxy on\|off` |
| See values + source | pills: *Saved setting* / *Default* / *Set by the environment* | `[saved setting]` / `[default]` / `[environment override]` + the override line | `network show` (`--json` = the payload) |
| Refusal | the gateway's sentence under the input | notice `✗ reverse proxy refused: <sentence>` | `refused: <sentence>` on stderr, exit 1 |

The console and the TUI send `{allowed_origins}` / `{trust_proxy}` to
`POST /api/gateway/network`; the CLI writes the same store through the same
function (`network_exposure.apply_network_change`). The mode is untouched by a
reverse-proxy-only change (`mode` is optional).

### Addresses

`GET /api/gateway/network` lists every address a client can use, discovered on
each call: loopback; each up interface's IPv4/IPv6 (loopback, link-local and
down interfaces skipped; macOS names from `networksetup`, e.g. "Wi-Fi"; VPN
`utun`/CGNAT addresses labelled "VPN"); the Bonjour name `<LocalHostName>.local`
when it resolves. Discovery uses `psutil` when importable, else `ifconfig -a`
(macOS/BSD) or `ip -o addr show up` (Linux), else the hostname's own
resolution. Each row says whether the gateway listens there now
(`reachable`). The WAN address (`kind: public`) is looked up only on request
(`?lookup_public=1`, admin, `internet` mode only; one HTTPS GET to
`api.ipify.org`), never on a poll. `copy_hint` is the URL to copy first (the
LAN IPv4 when listening on the network, else loopback).

### API (`gateway_network_v1`)

- `GET /api/gateway/network[?lookup_public=1]`: any authenticated principal
  (`writable` says whether the caller may change it).
- `POST /api/gateway/network {mode?, port?, acknowledge_internet?, allowed_origins?, trust_proxy?}`:
  admin, any subset (at least one). 200
  `{ok, configured, effective, restart_required, restart, auth, reverse_proxy, changed, warnings, copy_hint}`
  where `changed{field: {from, to, applies: live|restart|overridden_by_env}}`;
  409 `{ok:false, reason_code: user_auth_required|auth_disabled|acknowledgement_required, refused_reason, fix?, warnings}`;
  400 invalid mode/port/`trust_proxy`, or `invalid_origins` with `errors[]`;
  422 unknown field or a non-boolean `trust_proxy`. Every attempt is one
  audit-log line (`audit_log.jsonl`) carrying `setting_change` (the fields
  changed, from/to, or the refusal).
- `POST /api/gateway/network/restart {force?}`: admin. 409 with
  `refused_reason` when a restart cannot apply the setting (CLI override, auth
  not met, nothing pending, process cannot relaunch itself).

A trimmed `GET` in `lan` mode, running and applied:

```json
{
  "schema": "gateway_network_v1",
  "configured": {"mode": "lan", "label": "Local network", "port": 8080, "bind_host": "0.0.0.0", "source": "stored"},
  "effective": {"mode": "lan", "bind_host": "0.0.0.0", "port": 8080, "overridden_by_cli": false,
                "host_source": "setting", "port_source": "setting", "running": true},
  "restart_required": false,
  "restart": {"available": true, "applies": true, "needed": false},
  "auth": {"user_auth": true, "token_auth": false, "ok_for_mode": true, "source": "env"},
  "modes": [{"id": "localhost", "allowed": true, "selected": false},
            {"id": "lan", "allowed": true, "selected": true},
            {"id": "internet", "allowed": true, "requires_acknowledgement": true}],
  "addresses": [
    {"kind": "loopback", "url": "http://127.0.0.1:8080", "reachable": true},
    {"kind": "lan", "url": "http://192.168.1.23:8080", "interface": "en0", "interface_label": "Wi-Fi", "reachable": true},
    {"kind": "hostname", "url": "http://mymac.local:8080", "reachable": true}
  ],
  "copy_hint": "http://192.168.1.23:8080",
  "warnings": ["Traffic is plain HTTP: …"]
}
```

### CLI

```bash
abstractgateway network status|show [--json] [--data-dir DIR]
abstractgateway network set [localhost|lan|internet] [--port N] [--acknowledge-internet]
                            [--allowed-origins ORIGIN[,ORIGIN...]] [--trust-proxy on|off]
abstractgateway network addresses [--copy] [--public] [--json]
abstractgateway network restart [--url URL] [--token T] [--force]
```

`status`, `set` and `addresses` work on the data dir directly (a running
gateway's bind and auth posture are read from `<data>/run/gateway-network.json`);
`restart` asks the running gateway. See [security.md](docs/security.md#network-exposure)
for what each mode changes for someone on your network.

## Two entry points, one store

AbstractCore (low level) and AbstractGateway (high level) are the two entry
points to the framework, and they share configuration. Where AbstractCore holds
a value, that value is the single source of truth: the Gateway reads and writes
it through AbstractCore, keeps no copy of it, and surfaces it alongside the
configuration the Gateway itself owns.

A fresh install starts with recommended defaults so generation works out of the
box — text on `lmstudio/qwen/qwen3.5-9b`, voice on `supertonic/supertonic-3`,
image on `mlx-gen/AbstractFramework/flux.2-klein-4b-8bit`. They appear in the
capability-defaults grid like any configured route and can be changed or
cleared from either entry point; a value supplied by an application or a run
always wins. The seed applies only when no AbstractCore configuration file
exists yet, so a store you already have is never modified.

**Which side owns what.**

| Domain | Authority | Where it is stored | Gateway surface |
| --- | --- | --- | --- |
| Capability route provider/model/base URL (text, image, video, voice, sound, music, 3D, embeddings) | AbstractCore | `capability_defaults.routes` in `abstractcore.json` | `GET/PUT/DELETE /api/gateway/config/capability-defaults[/{kind}/{modality}[/{task}]]`, console **Capability defaults** |
| Reasoning effort for text generation | AbstractCore | `reasoning` on the `output.text` route (stored as `input.text`) | the same routes and console panel |
| MTP default policy | AbstractCore | `options.speculation` on that text route | web/TUI **MTP** selector; application/run overrides remain independent |
| Plugin/provider route options (voice, profile, language) | AbstractCore | `options` on the route | the same routes and console panel |
| Provider API keys | AbstractCore | `api_keys` in `abstractcore.json` | console **Provider connections** (values are never returned) |
| Mail connection (IMAP/SMTP host, port, username, folder) | AbstractCore | `email` in `abstractcore.json` | the email bridge and inbox routes read it; `ABSTRACT_EMAIL_*` variables override it |
| Maintenance-triage LLM settings | AbstractCore | `maintenance` in `abstractcore.json` | the maintenance triage assistant; `ABSTRACT_TRIAGE_LLM_*` variables override it |
| Endpoint profiles (custom base URLs, per-profile keys, allowed models) | shared namespace | `provider_profiles` in `abstractcore.json` and `provider_endpoint_profiles` under the Gateway data dir | `/api/gateway/config/provider-endpoint-profiles` |
| Gateway auth, users, sessions, principals | Gateway | Gateway data dir | `/api/gateway/session/*`, `/api/gateway/users/*` |
| Bundles, workflow catalog, workspaces, run policy and retention | Gateway | Gateway data dir | the corresponding `/api/gateway/*` routes |
| Integrations (Agora, Telegram, process manager) | Gateway | Gateway data dir and environment | the corresponding `/api/gateway/*` routes |

Inside the Gateway, every read and write of an AbstractCore-owned value goes
through one module, `abstractgateway/core_config.py`. It is the only place that
talks to AbstractCore's configuration, which is what keeps "no Gateway copy"
true as the code grows.

Endpoint profiles are the one shared namespace: both sides can define
`endpoint:<id>` virtual providers, AbstractCore in its `provider_profiles`
section and the Gateway in its own store. A profile AbstractCore holds wins on
an id collision, and a Gateway profile resolves when AbstractCore has none — so
`abstractcore config set-default output.text --provider endpoint:<id>` and a
Gateway-defined profile of the same name always resolve to AbstractCore's
definition. Use distinct ids across the two unless you intend that.

### Capability defaults

The Gateway is a full CRUD surface over AbstractCore's per-modality
provider/model defaults (configure, surface, live-refresh) and keeps **zero**
local storage. Every read hits Core's manager and every write goes through
Core's setter, so configuring a default here configures Core's default, for text
and for every media modality: image, video, voice (TTS), voice input (STT),
sound, music, 3D.

**Where it is stored.** A JSON file under key `capability_defaults.routes`:
`~/.abstractcore/config/abstractcore.json` normally, or the Gateway-scoped
`<data_dir>/config/abstractcore.json` in hosted user-auth mode (the payload
reports both as `config_file` / `gateway_config_file` / `principal_config_file`).
`GET /api/gateway/config/capability-defaults` names the file it read.

| Route | What it defaults |
| --- | --- |
| `output.text` (stored as `input.text`) | text generation |
| `output.image[.text_to_image\|.image_to_image\|.image_upscale]` | image generation / edit / upscale |
| `output.video[.text_to_video\|.image_to_video]` | video generation |
| `output.voice` / `input.voice` | TTS **and voice cloning** / STT |
| `output.music` / `output.sound` | music / sound-effect generation |
| `output.scene3d[.text_to_scene3d\|.image_to_scene3d]` | 3D scene generation |
| `input.image` / `input.video` / `input.sound` / `input.music` | understanding (covered by `input.text` when that model is multimodal) |

The task→route mapping is stated once, in AbstractCore's capability-defaults
module, and every layer reads it from there. A `.task` suffix is only valid for
the tasks Core persists; `tts`, `stt`, `music_generation` and
`sound_generation` resolve at the modality cell.

CRUD: `GET /api/gateway/config/capability-defaults` (full grid — configured,
derived and unset rows, each naming its source),
`PUT`/`DELETE /api/gateway/config/capability-defaults/{kind}/{modality}` and
`.../{kind}/{modality}/{task}`. Every write re-applies the affected default to
the **live** runtime (`refresh_capability_defaults`), so the next run uses it
without a restart.

A `PUT` is a partial update: `provider`, `model`, `base_url`, `reasoning` and
`options` are all optional, a field you omit keeps its stored value, and `""`
clears a field. That is what lets the console edit a provider without discarding
a reasoning effort set through `abstractcore config set-default`, and the other
way round.

**The reasoning effort.** The text-generation route carries an optional
`reasoning` field beside its provider and model — the host's default reasoning
effort for reasoning-capable models. Set it in the console's capability-defaults
panel or through the route:

```bash
curl -X PUT "$GW/api/gateway/config/capability-defaults/output/text" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"provider":"lmstudio","model":"qwen3-30b","reasoning":"high"}'
```

It applies to any call that names no effort of its own. An explicit `thinking`
on a run, a Flow LLM/Agent node, or an entity's substrate wins over it,
`thinking=false` included; with no configured effort and no explicit value, no
reasoning parameter is sent at all.

**MTP defaults and overrides.** Fresh Core configurations seed native MTP at depth
2 for compatible models; existing stores are preserved. Gateway's web and terminal
capability-default editors change this Core-owned policy, not a separate Gateway
setting. Choose Off or a draft depth; clearing the policy does not reseed it.
Other route options are preserved by the dedicated selector. A default is a
policy, not proof that the selected backend or loaded model can execute it.

Flow, Assistant and Code default to inheritance. A run may supply `speculation`
on `/runs/start` or `/runs/schedule`, or `_runtime.speculation` in its input:
`false` disables MTP and a native-MTP object selects a depth. Explicit node/call
settings override inherited run settings. The sandbox selector uses execution
capabilities for the selected provider/model and reports the response's actual
MTP outcome. Selecting a depth never downloads a head or silently reloads a model.
Prepared models can change depth or switch Off without unloading; an unprepared
instance reports that provisioning/reloading is needed. Depth 2 is a starting
default, not a workload-independent speed guarantee.

**If the other entry point writes.** `abstractcore config set-default <route>
--provider … --model …` (and AbstractCore's console-TUI, which runs that
command) edits the same file with no way to notify a running Gateway. The host
therefore fingerprints the config files — `(path, mtime, size)`, one `stat`, no
parse — and re-publishes the defaults to the live runtime on the next
`start_run` when a file has moved. So both entry points are effective on the
next run, not at the next Gateway write or a restart. With a split AbstractCore
server (`ABSTRACTCORE_SERVER_BASE_URL`) there is no local file to watch, and the
write routes' push remains the freshness mechanism. Partial updates work the
same across that boundary: the AbstractCore server's own
`PUT /v1/config/capability-defaults/...` routes keep the fields a request omits
and clear the ones it sends empty, so the reasoning effort survives a
provider-only save whether AbstractCore runs in-process or as a server.

Cascade, per modality (highest wins): explicit request pins (a flow node's
`image_provider`, `tts_provider`, …) > flow defaults > this console default >
flow-scanned bootstrap (**text only**). A media node that names a provider is
never clobbered; a default only fills an absent/Auto one. See
`abstractgateway/provider_defaults.py` for the full contract.

Config beats env. The `output.image` default outranks `ABSTRACTVISION_BACKEND` /
`ABSTRACTCORE_VISION_BACKEND`, exactly as `output.voice` / `input.voice` outrank
`ABSTRACTVOICE_*`, and the voice contract's `active_model` reports the
configured route's model before any `ABSTRACTVOICE_*_MODEL` export. An
environment variable that loses to a configured value is logged once per
distinct (variable, config, env) triple, so a stale export stays visible.
Environment variables remain a labeled `#FALLBACK` for deployments that
configure nothing.

Voice-model environment variables such as `ABSTRACTGATEWAY_VOICE_TTS_MODEL` and
`ABSTRACTVOICE_OPENAI_TTS_MODEL` add entries to the **discovery catalog** — the
list of models a picker can offer. They do not select a default; the
`output.voice` route does.

### Model weights

Capability defaults say which model each route uses. These endpoints say
whether that model's weights are on the execution host, and fetch them when
they are not. They report the same four states as `abstractcore models status`
and both console-TUIs: `installed`, `not downloaded`, `unknown`, `remote`.

| Endpoint | What it does |
|---|---|
| `GET /api/gateway/models/availability` | The capability grid annotated with weight availability, plus the recommended fresh-install set — its raw counts (`total`, `installed`, `absent`, `would_download`) and `gaps`, the subset whose route has nothing else serving it. The text entry of `recommended` also carries AbstractCore's reasons: `catalog_id`, `basis` (`apple_silicon_tiers` or `portable_default`), `tier`, `fit_verdict`, `fits` and `warning` (a sentence when the model may not fit this computer). Read-only; never downloads. |
| `POST /api/gateway/models/download` | `{"provider": "...", "artifact": "..."}` or `{"recommended": true}`, with optional `"dry_run": true`. Returns a job id immediately. |
| `GET /api/gateway/models/download/{job}` | One job's progress: status, percent, byte counts and the provider tool's own recent output. |
| `GET /api/gateway/models/downloads` | Every download job this Gateway process knows about. |

The web console renders this as a **Weights** column on the capability-defaults
table, with a per-row download button and a fresh-install banner. In both
console-TUIs the verb is `w` on the Routes screen.

**The recommendation is advice for an empty route, not a standing debt.** The
banner speaks only about `gaps` — a recommended model that is absent *and*
whose route has nothing serving it. Route text generation at a model of your
own and the starter kit's LM Studio build stops being reported as missing:
nothing on this host needs it. A route whose *own* model is not downloaded is
still reported, on that row, in the Weights column. "Apply recommended" is a
standing action in the section head (`a` on the TUI Routes screen), available
whether or not the banner has anything to say.

**The artifact is not the model id.** A route stores the id the provider
*serves* (`qwen/qwen3.5-9b`); the download names the exact weights,
quantization included (`qwen/qwen3.5-9b@4bit`). The availability payload
carries `download_artifact` on any row where these differ — post that, not the
row's `model`.

**Single-flight.** A second request for an artifact already downloading joins
the running job instead of starting a second copy of the provider's tool; the
returned job's `joined` counter says so.

**Jobs run in AbstractCore's host job registry.** Downloads, deletes and
engine installs are jobs of one kind (`host_job_v1`), readable at
`GET /api/gateway/jobs/{id}` as well as through
`GET /api/gateway/models/download/{job}` (which keeps its `{ok, job}` envelope
and reports a queued job as `running`). AbstractCore keeps a snapshot of each
job on disk, so jobs started by `abstractcore models download` on the same
machine appear too. A job the gateway no longer knows returns 404; that is not
a lost download — the provider tool owns the bytes. Re-read
`/api/gateway/models/availability` to learn whether the weights landed.

**A default whose weights are missing does not stop the Gateway.** The host
loads, bundles register, and the failure surfaces when a run actually needs that
model — naming the capability route that configured the pair, how to change it,
and how to download it.

### Models and engines

The **Models** and **Engines** tabs of the web console, the terminal console
and the `abstractgateway models …` / `abstractgateway engines …` commands show
the same things AbstractCore shows (`abstractcore models …`, `abstractcore
engines …`): the host's hardware, the local inference engines, a model catalog
with a "fits this machine" verdict per download, the models already installed,
and the jobs that download, delete or install. The gateway does not detect
engines or size models itself; it serves AbstractCore's answers
so both entry points always agree.

| Endpoint | What it does |
|---|---|
| `GET /api/gateway/host/profile` | This host: OS, accelerator, RAM/VRAM, how much memory a model may use, free disk per model store. |
| `GET /api/gateway/engines?probe=1` | Ollama, LM Studio, MLX, llama.cpp, vLLM, Hugging Face: supported here, installed, version, running, and the exact install command. `install_allowed` says whether installs are enabled on this gateway. |
| `POST /api/gateway/engines/{id}/install` | `{"dry_run": true}` shows the command; `{"dry_run": false}` runs it on the gateway host as a job. Admin only, and only when `allow_engine_install` is on. |
| `GET /api/gateway/models/catalog?q=&engine=&fits=1&hub=1` | Downloadable models with presence and a fit verdict (`fits`, `tight`, `too_large`, `partial_offload`, `unknown`). |
| `GET /api/gateway/models/installed?provider=` | Every installed model per engine, with sizes and what would block a delete. |
| `POST /api/gateway/models/download` | Download one model as a job (see [Model weights](#model-weights)). Admin only. |
| `POST /api/gateway/models/delete` | `{"provider", "artifact", "dry_run", "force"}`: delete one model as a job. Admin only; refuses a loaded or shared model unless `force`. |
| `GET /api/gateway/jobs`, `GET /api/gateway/jobs/{id}`, `POST /api/gateway/jobs/{id}/cancel` | Download, delete and install jobs, newest first; cancel is admin only. |

Every job carries a `cli_equivalent` you can run by hand, for example
`abstractgateway models download ollama qwen3:8b` or
`abstractgateway engines install ollama --yes`. Payloads and refusals are
listed in [api.md](docs/api.md#models-and-engines).

<a id="allow_engine_install"></a>
#### `allow_engine_install`

Installing an engine runs its vendor installer (for example
`brew install ollama`) **on the machine that runs the gateway**, which for a
remote gateway is not the machine of the person clicking. So installs are
controlled by the runtime-config setting `allow_engine_install`:

| Gateway bound to | Default for someone at the gateway machine | Default for another computer |
|---|---|---|
| a loopback address (`127.0.0.1`, `::1`, `localhost`), which is what a bare `abstractgateway serve` and `abstractgateway service install` use | on | on |
| any other address (`0.0.0.0`, a LAN IP, a host name), or started without `abstractgateway serve` | on | off |

"Someone at the gateway machine" is a request whose socket peer is loopback
or one of this host's own interface addresses (a browser on the gateway
machine that uses its LAN address counts), with no proxy header
(`Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host`, `X-Real-IP`): see
[security.md](docs/security.md). The same rule gates app installs (Apps page,
tray). `install_policy` reports `caller_on_this_machine` and, when that rule
decided, `source: "default_same_machine"`.

An admin changes it with
`POST /api/gateway/admin/runtime-config {"allow_engine_install": true}`
(`false` turns it off, `null` returns to the default). The current value and
where it came from are in `GET /api/gateway/admin/runtime-config` and in
`install_policy` on `GET /api/gateway/engines`. There is no environment
variable for it. A dry run ("show the command") is always allowed, and every
install is admin-only and recorded in the audit log.

### Browser apps settings (`apps.*`)

The browser apps (Apps page: Flow, Code, Observer, Continuum, Entity) read five
runtime settings. Precedence is **stored > env > default**: a saved value
always wins; the `ABSTRACTGATEWAY_APPS_*` environment variable named in the
table is the fallback, and an env value that a saved one shadows is reported
as `env_shadowed`.

| Key | Label | Default | Value | Environment fallback |
|---|---|---|---|---|
| `apps.node` | Node.js for apps | `auto` | `auto` (Node.js 18+ on this computer, else the gateway's own) · `managed` · `system` · an absolute path to `node` | `ABSTRACTGATEWAY_APPS_NODE` |
| `apps.ports` | Ports for apps | (empty) | a port or `low-high`; empty = each app's usual port, else the next free one in 3100-3199 | `ABSTRACTGATEWAY_APPS_PORTS` |
| `apps.host` | Where apps listen | `127.0.0.1` | an IP or host name; `0.0.0.0` opens the apps to every network this computer is on | `ABSTRACTGATEWAY_APPS_HOST` |
| `apps.npm_registry` | npm registry | `https://registry.npmjs.org` | an http(s) URL (a mirror) | `ABSTRACTGATEWAY_APPS_NPM_REGISTRY` |
| `apps.pypi_url` | Node.js download index | `https://pypi.org/pypi` | an http(s) URL (a mirror) | `ABSTRACTGATEWAY_APPS_PYPI_URL` |

`GET /api/gateway/admin/runtime-config` returns them under `apps` as
`{name: {key, label, help, placeholder, default, env_name, value, source,
note?, env_shadowed?, invalid_stored?, invalid_env?}}` (the registry
`runtime_config.APPS_SETTINGS`: a new knob is one row, and the TUI renders
whatever the payload lists). Writes go through the generic door, admin-only
and audit-logged (`setting_change` on the request's audit line):
`POST /api/gateway/admin/runtime-config {"apps.host": "0.0.0.0"}` (or
`{"apps": {"host": "0.0.0.0"}}`); an empty value clears back to env/default.
Each value is validated before anything is written (400 with the reason). Read
at each use: a change applies at the next app start (`node`, `host`, `ports`)
or the next download (the two URLs).

Three ways, same semantics:

| | Web console | Console TUI | CLI |
|---|---|---|---|
| Where | Apps → *Advanced: apps settings* (one field per setting, with its source pill) | Runtimes → *Runtime knobs* → *Edit apps settings* | `abstractgateway apps config get [NAME] [--json]` |
| Change | type, *Save apps settings* (only changed fields are sent; empty = clear) | one line per setting (stored value prefilled; empty = clear) | `abstractgateway apps config set NAME VALUE` (`""` clears) |
| Refusal | the gateway's sentence (*Not saved*) | the form shows the gateway's sentence | `refused: <sentence>`, exit 2 |

The CLI works on the data dir directly (`--data-dir`, default: the `serve`
resolution), so a headless server needs no browser.

### Backlog folder, exec runner and process manager (Continuum)

Continuum's Board, Backlog, Executions and Services pages read three runtime
settings. A fresh install needs none of them: the gateway keeps its own
backlog in `<data dir>/backlog/`, and creates the standard layout there the
first time the backlog is used (`docs/backlog/overview.md`,
`docs/backlog/template.md`, and the `planned/`, `proposed/`, `completed/`
folders; nothing existing is ever overwritten). Continuum then shows an empty
board with **Create your first item**.

| Key | Label | Default | Value |
|---|---|---|---|
| `triage_repo_root` | Backlog folder | `<data dir>/backlog` (created on first use) | a folder that contains `docs/backlog` (a project checkout), or the gateway's own folder |
| `backlog_exec_runner` | Backlog exec runner | off | `on` / `off`: run the items queued for execution on this machine |
| `process_manager` | Process manager | off | `on` / `off`: Continuum's Services page (process control also needs the backlog folder set to the framework checkout it manages) |

**Where a value comes from** (one resolution, `runtime_config.resolve_backlog_root`
and `resolve_exec_runner`; every consumer calls it: the backlog, report,
triage and process routes, the exec runner at each poll, the skills shelf):

1. the launch flag of the running gateway: `abstractgateway serve --backlog-root PATH`
   and `--exec-runner on|off` (for that run only; source `flag`);
2. the saved setting (source `stored`);
3. an environment value (`ABSTRACTGATEWAY_TRIAGE_REPO_ROOT`,
   `ABSTRACTGATEWAY_BACKLOG_EXEC_RUNNER`; source `env`;
   supported for compatibility, never needed; a saved value wins);
4. the default (source `default`).

`GET /api/gateway/admin/runtime-config` serves each as `{value, source, key,
label, help, cli, flag?}`; the backlog folder also carries `available`,
`reason` (why it is not usable, without the path), `default_path` and, under a
launch flag, the `stored_value` that applies once the gateway restarts
without it. Non-admins get the posture without server paths.
`GET /api/gateway/backlog/status` answers the same question for Continuum
(any signed-in user; paths for admins only).

Changing them, three doors with one validation (a folder must exist and
contain `docs/backlog`, or be the gateway's own folder, which is created; a
switch is `on` or `off`; a refusal is one plain sentence):

| | Web console | CLI | Continuum |
|---|---|---|---|
| Where | Apps → *Advanced: backlog settings (Continuum)* | `abstractgateway config get [KEY] [--json]` | Settings → *Gateway administration* |
| Change | edit, *Save backlog settings*; *Use the gateway's own folder* | `abstractgateway config set KEY VALUE`, `abstractgateway config unset KEY` | *Change…*, *Use the gateway's own folder*, *Enable* / *Disable* |

`config set` goes through the running gateway's door when one serves this
data dir on this machine (it applies at once and lands in the audit log);
otherwise it writes the settings store and the next start reads it. The same
`config get|set|unset` covers every runtime setting (`executor`,
`apps.<name>`, …).

When a saved folder disappears (a deleted or unmounted checkout), the backlog
routes answer `404` *Backlog folder not available on this gateway: the folder
does not exist (set by the saved setting)…* and Continuum shows the folder,
the reason and, for an admin, **Use the gateway's own folder** and **Choose a
folder…**.

Evidence: `src/abstractgateway/runtime_config.py` (`resolve_backlog_root`,
`resolve_exec_runner`, `validate_backlog_root`, `BACKLOG_SETTINGS`),
`src/abstractgateway/assets/backlog_skeleton/`, `src/abstractgateway/config_cli.py`,
`tests/test_gateway_backlog_root_settings.py`.

### Host state and model residency

Beyond weights on disk, these endpoints report and control what is loaded in
memory right now:

| Endpoint | What it does |
|---|---|
| `GET /api/gateway/host/state` | One-call host snapshot: memory, GPU, resident models (frozen `model_residency_row_v1` rows), session prompt caches, and byte totals. Sections degrade independently in-band (`degraded` + `reasons`); never a 500. |
| `GET /api/gateway/host/metrics/memory` | Host RAM/process/device memory snapshot; answers `supported: false` with a reason when the runtime facade has no snapshot. |
| `GET /api/gateway/host/metrics/gpu` | GPU utilization probe with the same `supported`/degraded style. |
| `GET /api/gateway/models/loaded` | Model residency listing: raw `models` records plus normalized `rows` (`row_schema = "model_residency_row_v1"`, including lock, modality, context-calibration, and host-identity fields). |
| `GET /api/gateway/models/context_estimate` | Context/KV memory estimate for a `provider`+`model` (optional `context_length` >= 1), with in-band `confidence`: `calibrated`, `estimated`, or `unknown`. |
| `POST /api/gateway/models/load` | Load (and by default pin) a model runtime. Admin only. |
| `POST /api/gateway/models/unload` | Unload a model runtime by `runtime_id` or task/provider/model selector. A locked model answers HTTP 409 unless the request carries `"force": true`. Admin only. |
| `POST /api/gateway/models/lock` | Lock a resident model against unload (same target selector as unload). Admin only. |
| `POST /api/gateway/models/unlock` | Release a model-residency lock. Admin only. |

Both consoles render this surface as a **Resources** view — a tab in the web
console, screen 8 in the console-TUI: memory and GPU meters, the resident-model
table (modality, tri-state residency, lock state, context facts), and session
prompt caches. Any authenticated user can browse it and request context
estimates; the warm-up, lock/unlock, unload (with a force confirmation when a
locked model answers 409), and cache-clear controls appear for admins.

The reads are available to any authenticated principal; the mutations (and
`POST /models/download` above) require an admin principal, and anonymous
requests are always rejected. For local development only,
`ABSTRACTGATEWAY_DEV_READ_NO_AUTH=1` (default off) allows unauthenticated
loopback reads as a non-admin read-only principal — see
[security.md](docs/security.md).

See [api.md](docs/api.md#host-state-and-model-residency) for payload shapes and
the `model_residency_row_v1` field list.

### Host control: pause, desktop tray, restart, update

The process's own controls, used by the desktop tray and the console. Reads
are user-level; every write is admin-only.

| Endpoint | What it does |
|---|---|
| `GET /api/gateway/host/runner` | `paused`, `paused_at`, `paused_by`, `reason`, `inflight_ticks` (runs still finishing their current step), `scope` (`"workflow runner"`), `runner_in_process`, `step_gate_supported`, restart/shutdown `capabilities`. |
| `POST /api/gateway/host/pause` / `resume` | Pause or resume execution process-wide (persisted in `<data_dir>/gateway_paused.json`). Body `{"reason": "..."}` optional. |
| `GET /api/gateway/host/metrics/live` | GPU + memory + paused/in-flight in one call, cached 1 s server-side — the tray's fast lane. |
| `GET /api/gateway/host/runs` | Recent runs across every data plane on this machine (`limit`, `window_hours`), newest first, with a readable `label` and the step count. Admin — it crosses tenants. Cached 5 s. Entity planes are skipped and named in `skipped_entity_planes`. |
| `GET /api/gateway/host/tray` | Whether the tray helper runs (`pid`, `ready`), and the decision (`reason`, `hint`) when it does not. |
| `POST /api/gateway/host/tray/show` | Retry the helper now (admin) — the escape hatch for one that crashed. There is no `hide`. |
| `POST /api/gateway/host/restart` / `shutdown` | Graceful restart (same command, same environment) or stop; `409` with the reason when this process cannot (`--reload`, not started by `abstractgateway serve`, an update is installing). |
| `GET /api/gateway/host/update` | How the gateway was installed (`install.kind`, `upgradable`, the command), the last update check, the upgrade job, `restart_pending`. |
| `POST /api/gateway/host/update/check` / `start` | Ask pypi.org for the latest release (offline is an in-band answer) / run the upgrade in the background. |

The tray icon has **no setting**: while the gateway serves a desktop that can
hold it, it is there. It is absent only for reasons that are facts about the
machine — no display, no `tray` extra, `serve --reload`, a runner-only process
— and `GET /host/tray` names which. There is no `desktop_tray` setting; a
write to that key is refused with this explanation. `GET /api/health` carries `"paused": true` while paused (status
stays `healthy`). Full description: [tray.md](docs/tray.md).

### Runtime-scoped Core capability defaults

In hosted user-auth mode, `GET /api/gateway/config/capability-defaults` returns
the execution-host Core capability routes plus the Gateway/root baseline and
any defaults configured for the current Gateway principal. The bootstrap
`default/admin` principal edits the Gateway baseline when it uses the default
runtime. Normal user writes to
`PUT /api/gateway/config/capability-defaults/{kind}/{modality}` or
`PUT /api/gateway/config/capability-defaults/{kind}/{modality}/{task}` are stored under
that principal's Gateway data plane as a Core config file and override the
Gateway baseline only for that user:

```text
$ABSTRACTGATEWAY_DATA_DIR/config/abstractcore.json
$ABSTRACTGATEWAY_DATA_DIR/users/<tenant>/<runtime>/runtime/config/abstractcore.json
```

This lets operators set a Gateway default and lets hosted users choose
remote-provider defaults for their own runtime without mutating the operator's
global AbstractCore config or other users. The route schema, normalization,
task-specific generated-media suffixes, and file format come from AbstractCore
capability-default contracts. Capability defaults live only in the AbstractCore
config file; a `config/capability_defaults.json` file in the data dir is not
read (recreate such defaults with `abstractgateway-config set-default ...`). Provider API keys and raw secrets are
not returned by these routes. Use Gateway provider connections when a route
default needs an API key or custom base URL.

Gateway model discovery delegates to AbstractRuntime's AbstractCore discovery
facade. LLM and embedding default pickers can filter models with Core route keys
such as `capability_route=input.image,output.text` or
`capability_route=embedding.text`. Generated image/video/voice/sound/music
defaults continue to use their capability plugin catalogs so provider readiness,
download/setup state, and backend-specific metadata do not get written into the
raw Core model registry.

CLI examples:

```bash
# Gateway baseline Core default
abstractgateway-config set-default input.text \
  --provider endpoint:openai-prod \
  --model gpt-4.1

# One user's runtime Core override
abstractgateway-config set-default input.text \
  --scope user \
  --tenant default \
  --user alice \
  --provider endpoint:alice-openai \
  --model gpt-4.1

abstractgateway-config defaults --scope user --user alice
```

#### Modality rows and task rows

`output.image`, `output.video` and `output.scene3d` are the **parent** rows of
their `output.<modality>.<task>` siblings, not duplicates of them. The
parent answers every task of that modality that has no row of its own, so
setting it alone is the simple path (one image model for generate, edit and
upscale) and is what a fresh install seeds. A task row overrides it for that
task, wholesale — route rows are single coherent backend identities and are
never field-merged with their parent.

Resolution everywhere — execution, the Sandbox, and what `/capabilities`
advertises — is **task row first, modality row second**. A modality-level
question resolves through the canonical generation task
(`output.image.text_to_image`) before falling back to `output.image`, so the
backend Gateway advertises is always the backend it will execute.

`output.voice`, `output.sound` and `output.music` have no task rows; their
modality row is the primary key, not a fallback.

In the Multimodal Capabilities grid the task rows are indented beneath their
modality row, and a modality row that is unset while every task row beneath it
is configured shows `not needed` rather than `not configured` — nothing can
reach it in that state. It stays editable, because setting it is still the
one-value-for-everything path.

`input.text` is the canonical text LLM route. `output.text` is reported as a
read-only derived view of `input.text`, and CLI/API writes to `output.text` are
canonicalized to `input.text` for compatibility. `input.image` is a fallback
image-understanding route only: when the selected `input.text` model is known
from AbstractCore model capabilities to accept image input, the console marks
`input.image` as covered by `input.text` and disables separate editing.
`input.video` follows the same coverage model when the text model can handle
visual frames, but it remains overrideable so operators can choose a dedicated
video/VLM route. `input.voice` is the speech-to-text fallback route; if it is
not configured and the selected text model cannot accept audio natively,
Gateway/Core fail clearly instead of using a hidden installed STT backend.
`input.sound` is for non-speech audio understanding and is not used as STT.
`input.music` is the corresponding music-audio understanding route. `input.sound`
and `input.music` may be shown as covered by `input.text` only when the selected
text model is known to accept those native inputs, and both rows remain
overrideable.
Audio-language candidates such as `qwen3-omni-30b-a3b-instruct`,
`qwen3-omni-30b-a3b-captioner`, `qwen2.5-omni-7b`, and
`qwen2-audio-7b-instruct` are registry-known options when the configured
provider can serve them. Qwen3.6 text/image/video defaults should not be treated
as sound or music understanding models.

### Provider connections

Gateway Console and `POST /api/gateway/config/provider-endpoint-profiles` let
signed-in users define reusable provider connections through a guided setup
flow for `openai`, `anthropic`, `openrouter`, `portkey`, `lmstudio`, `ollama`,
or `openai-compatible`. A connection includes a stable id, display name,
description, optional base URL, optional API key, and optional advanced model
allowlist. The raw API key is write-only: responses include only `api_key_set`
and a short fingerprint. AbstractCore owns model capability metadata, so normal
setup does not ask users to classify models manually.

The console's **Test** action calls the selected provider through
`POST /api/gateway/config/provider-endpoint-profiles/discover-models` and
previews model discovery before saving. Leave the advanced model restriction
empty to keep live discovery active, or select one or more models to store a
fixed allowlist. The **Multimodal Capabilities** tab shows configured provider
connections and direct providers that are already usable from scoped
AbstractCore config or environment variables. It does not collect endpoint base
URLs or API keys. Reachable default local servers such as LM Studio and Ollama
also appear automatically when Gateway can discover models from their
configured/default endpoint.

Enabled profiles appear in `GET /api/gateway/discovery/providers` as virtual
provider ids such as `endpoint:office-vllm`. Direct configured providers such
as `openai` or `anthropic` also appear automatically when their required API
key is available from scoped Core config or process environment. Use those
provider ids in Flow nodes or Gateway capability defaults. At runtime the
Gateway host resolves virtual providers to the real provider family, base URL,
and API key for the transient AbstractRuntime call; direct providers use the
scoped Core config/environment already available to the execution host.
Workflow JSON and browser storage do not contain the raw secret. Normal users
can manage user-scoped profiles. Gateway-scoped profiles require an admin
principal.

The console **Sandbox** tab reuses this configuration. It tests the selected
multimodal capability default rather than an ad hoc provider/model pair. Text
chat uses the configured text route, and generated media tests use configured
routes such as `output.image.text_to_image`, `output.video.text_to_video`,
`output.voice`, `output.sound`, and `output.music`. Image edit, image upscale,
and image-to-video are configured separately in the Multimodal Capabilities tab
through `output.image.image_to_image`, `output.image.image_upscale`, and
`output.video.image_to_video`. The Sandbox renders generated images, videos,
voice, sound, and music artifacts inline when the route completes, while keeping artifact
links available for opening the raw content. Text chat can include uploaded
attachments such as images, audio, video, PDFs, Markdown, or text documents.
Uploaded attachments are stored as Gateway artifacts and then materialized by
Runtime into provider-ready media for AbstractCore, so vision-capable
OpenAI-compatible text routes receive image uploads as native multimodal
`image_url` content. Sandbox text turns also send bounded browser-local
grounding context, including local datetime, timezone, timezone offset, and
locale. Runtime may use that browser context for prompt grounding only; it keeps
server-derived context as provenance and never uses browser metadata for auth,
runtime routing, or credential selection. Country grounding is inferred from the
browser timezone when possible, with locale only as a fallback.

### Workspace policy (filesystem scope)

The gateway enforces a server-side workspace policy so thin clients cannot expand filesystem access by sending arbitrary paths.

Operator-controlled roots:
- `ABSTRACTGATEWAY_WORKSPACE_DIR`: base directory used for `/api/gateway/files/*` helpers and to clamp run-provided `workspace_root` / `workspace_allowed_paths`.
- `ABSTRACTGATEWAY_WORKSPACE_MOUNTS`: additional allowed roots, newline-separated `name=/abs/path`.

Client scope overrides (permissive; trusted machines only):
- `ABSTRACTGATEWAY_ALLOW_CLIENT_WORKSPACE_SCOPE=1` (or `ABSTRACTGATEWAY_TRUST_CLIENT_WORKSPACE_SCOPE=1`) enables honoring client-provided `workspace_*` knobs, including `workspace_access_mode=all_except_ignored`.

Discoverability:
- `GET /api/gateway/workspace/policy` returns `{policy: {...}}` including whether client overrides are enabled (mount names only; no absolute paths).

Evidence: `src/abstractgateway/routes/gateway.py` (`_workspace_root`, `_workspace_mounts`, `_sanitize_run_workspace_policy`, `_client_workspace_scope_overrides_enabled`, `start_run`).

### Durability backend

- `ABSTRACTGATEWAY_STORE_BACKEND`: `file` (default) or `sqlite`
  Evidence: `src/abstractgateway/service.py`
- `ABSTRACTGATEWAY_DB_PATH`: SQLite DB file path (optional; default: `<DATA_DIR>/gateway.sqlite3`)
  Evidence: `src/abstractgateway/stores.py` (`build_sqlite_stores`)
  Note: for safety, when `ABSTRACTGATEWAY_STORE_BACKEND=sqlite`, the DB path must be **under** `ABSTRACTGATEWAY_DATA_DIR`.
  The gateway fails fast if `ABSTRACTGATEWAY_DB_PATH` points elsewhere (prevents cross-wiring UAT/prod durable state).

### KG memory store

Gateway selects an AbstractMemory TripleStore through a small resolver; it does
not implement memory stores itself.

- `ABSTRACTGATEWAY_MEMORY_STORE_BACKEND`: `lancedb` (default), `memory`, or `sqlite` when the installed AbstractMemory build exposes `SQLiteTripleStore`
- `ABSTRACTGATEWAY_MEMORY_STORE_PATH`: optional explicit store path
- `ABSTRACTGATEWAY_MEMORY_REQUIRE_VECTOR=1`: fail fast when the selected backend cannot satisfy semantic/vector recall

Backend behavior:

- `lancedb`: persistent and vector-capable; semantic `query_text` requires the execution-host
  `embedding.text` capability route.
- `sqlite`: persistent and structured-query only when `SQLiteTripleStore` is available; semantic `query_text` fails clearly.
- `memory`: process-local test/dev backend; non-durable.

The same resolver is used for bundle `memory_kg_*` nodes and
`POST /api/gateway/kg/query`. Capability discovery reports memory backend,
persistence, vector support, and embedder status. A missing on-disk store is not
an unavailable state by itself: when AbstractMemory is installed and the backend
resolves, fresh stores are authoring-ready and structured queries simply return
no matches until assertions are written.

### Runner tuning (advanced)

These map to `GatewayHostConfig` and `GatewayRunnerConfig`:
- `ABSTRACTGATEWAY_RUNNER`: `1` (default) / `0` to disable runner in-process
  Evidence: `src/abstractgateway/config.py`, `src/abstractgateway/cli.py`
- `ABSTRACTGATEWAY_POLL_S` (default `0.25`)
- `ABSTRACTGATEWAY_COMMAND_BATCH_LIMIT` (default `200`)
- `ABSTRACTGATEWAY_TICK_MAX_STEPS` (default `100`)
- `ABSTRACTGATEWAY_TICK_WORKERS` (default `4`)
- `ABSTRACTGATEWAY_RUN_SCAN_LIMIT` (default `200`)

Evidence: `src/abstractgateway/config.py`, `src/abstractgateway/runner.py`.

### Stop and the kill switch

A `cancel` command (the Stop button) cancels the run tree and also stops the
model call that is executing: the runtime hands the call a cancel event and the
provider stops within one token (MLX) or one stream chunk (any streaming
provider). The stopped call is recorded as an `llm_call` step with status
`cancelled` and `cancelled_by: command`.

If a model call of the cancelled tree is still executing after the kill-switch
deadline (a provider lane that cannot observe the event, e.g. a non-streaming
HTTP request), the gateway kills that inference in process, never the gateway
process: other runs, sessions and the HTTP API keep working. The runtime
injects an `EffectKilled` exception into the one thread executing the call (it
unwinds within one token of a Python-level decode loop); the step is recorded
`cancelled` with `killed_by: kill_switch`. The gateway logs an ERROR line
(`STOP KILL SWITCH FIRED … killed_by=kill_switch action=kill_inference`), writes
an `abstract.status` record "Stop forced at N s: inference killed" on every run
of the tree (the web UI shows it), and ends the runs CANCELLED with that reason.
A thread blocked inside ONE native call for more than 5 s after the kill
(`KILL_GRACE_S`) is reported as "could not be interrupted" in the log and the
ledger; the pending kill fires when that call returns. Tools are never
escalated: a tool still running is named in the log and its result is never
fed to another model call.

| Knob (runtime config key / env) | Default | Meaning |
|---|---|---|
| `stop_kill_switch_s` / `ABSTRACTGATEWAY_STOP_KILL_SWITCH_S` | `10` | seconds after the cancel is applied; `0` disables (logged at ERROR on every Stop) |

It is read at every Stop (runtime config via `POST /api/gateway/admin/runtime-config`
supersedes env, env supersedes the default). Evidence: `src/abstractgateway/stop_kill_switch.py`.

## LLM/tool defaults (bundle mode)

Only needed when the loaded bundle(s) contain LLM/tool/agent nodes.

- `input.text` capability route
  Default text route for LLM execution and Gateway LLM helper endpoints. Configure it through
  `abstractgateway-config set-default input.text ...` or
  `abstractcore config set-default input.text ...`.
  If no pair is configured, helpers return a clear configuration error instead of falling back to a
  hardcoded model.
  Evidence: `src/abstractgateway/provider_defaults.py`, `src/abstractgateway/hosts/bundle_host.py`
- `ABSTRACTGATEWAY_TOOL_MODE`:
  - `approval` (default): execute safe tools locally; require explicit approval for dangerous/unknown tools
  - `passthrough`: require explicit approval for *all* tools (then execute in-process on resume)
  - `delegated`: do not execute tools; tool calls yield a durable `JOB` wait for external executors
  - `local` (or `local_all`): execute all tools inside the gateway process (dev only; higher risk)
  Evidence: `src/abstractgateway/hosts/bundle_host.py` (tool executor selection)

### Embeddings

The gateway exposes an embeddings API when the execution host has an explicit `embedding.text`
capability default. Remote/provider-backed embeddings work with the base
remote-light install; local HuggingFace/sentence-transformer embeddings require
`abstractgateway[embeddings]`.

Configure it through the same capability-default control plane used by Flow:

```bash
abstractgateway-config set-default embedding.text \
  --provider lmstudio \
  --model text-embedding-nomic-embed-text-v1.5 \
  --base-url http://127.0.0.1:1234/v1
```

In embedded deployments Gateway uses the local Core embedding manager. In split deployments it
delegates to the remote AbstractCore `/v1/embeddings` route so provider `base_url` is evaluated
from the Core host.

Evidence: `src/abstractgateway/embeddings_config.py`

### Prompt cache controls (provider-dependent)

Gateway prompt-cache endpoints are available when the AbstractCore integration
for the active provider/model exposes them. Remote providers usually provide
server-managed cache hints; local in-process providers can expose stronger
control-plane operations when installed in a custom runtime image.
Provider-level endpoints remain available for operators, and session-level
endpoints provide a deterministic gateway-owned namespace/key lifecycle for thin
apps without pretending unsupported providers have local KV state.

- `GET /api/gateway/prompt_cache/capabilities`
- `GET /api/gateway/prompt_cache/stats`
- `POST /api/gateway/prompt_cache/set`
- `POST /api/gateway/prompt_cache/update`
- `POST /api/gateway/prompt_cache/fork`
- `POST /api/gateway/prompt_cache/clear`
- `POST /api/gateway/prompt_cache/prepare_modules`
- `POST /api/gateway/blocs/upsert_text`
- `GET /api/gateway/blocs/record`
- `GET /api/gateway/blocs`
- `POST /api/gateway/blocs/delete`
- `GET /api/gateway/blocs/kv/manifest`
- `GET /api/gateway/blocs/kv/list`
- `POST /api/gateway/blocs/kv/ensure`
- `POST /api/gateway/blocs/kv/load`
- `POST /api/gateway/blocs/kv/delete`
- `POST /api/gateway/blocs/kv/prune`
- `GET /api/gateway/prompt_cache/saved`
- `POST /api/gateway/prompt_cache/save`
- `POST /api/gateway/prompt_cache/load`
- `GET /api/gateway/sessions/{session_id}/prompt_cache/status`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/prepare`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/rebuild`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/clear`
- `GET /api/gateway/sessions/prompt_cache`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/clear_all` (admin)

Session lifecycle responses distinguish `unsupported`, `keyed`, and
`local_control_plane` modes. Keyed providers receive a stable `runtime_hint`;
local-control-plane providers can prepare, clear, and rebuild when their
AbstractCore provider exposes those operations.

Treat the prompt-cache surfaces separately:

- `/prompt_cache/*`: provider/model prompt-cache controls
- `/sessions/{session_id}/prompt_cache/*`: gateway-owned volatile session
  lifecycle, derived from the session/bundle/provider identity
- `GET /sessions/prompt_cache` + `/sessions/{session_id}/prompt_cache/clear_all`:
  enumeration of the caches the runtime actually minted — the recommended lane
  for observing and reclaiming session cache state, because it cannot miss
  caches whose keys the gateway never derived
- `/blocs/*`: durable exact-reuse bloc/KV contract that returns `prompt_cache_binding`

The `saved` / `save` / `load` aliases are Runtime-backed host-local admin
operations. Local runtimes write under `<DATA_DIR>/prompt_cache_exports`; remote
and hybrid runtimes report `prompt_cache_local_only`.

### Multimodal provider/plugin controls

The base install already includes the Gateway HTTP/SSE server and the Runtime
multimodal integration layer. Direct Gateway routes for voice/audio, image/video,
and music become available when the corresponding lower-layer capability
packages are installed on the gateway host (or when Gateway is configured to
proxy to a remote AbstractCore server).

Local heavy engines remain explicit opt-ins in the provider packages; Gateway
does not implicitly install them.

- `input.text` capability route: default text model for bundle LLM nodes
- `OPENAI_BASE_URL` / `OPENAI_API_KEY`: generic OpenAI-compatible text endpoint for AbstractCore providers
  - Apple/MLX Docker deployments should point the lightweight Gateway container
    at host-native inference, for example
    `http://model-runner.docker.internal/engines/v1`,
    `http://host.docker.internal:1234/v1`, or another `/v1` endpoint.
- `LMSTUDIO_BASE_URL` / `OLLAMA_BASE_URL`: named local endpoint providers for
  LM Studio and Ollama model discovery/routing from inside the Gateway container.
- `ABSTRACTGATEWAY_VISION_BACKEND` / `ABSTRACTGATEWAY_VISION_BASE_URL` / `ABSTRACTGATEWAY_VISION_API_KEY` / `ABSTRACTGATEWAY_VISION_MODEL_ID`: Gateway-scoped image backend settings. The `ABSTRACTVISION_*` names are also accepted by the lower package.
- `ABSTRACTGATEWAY_VOICE_TTS_ENGINE` / `ABSTRACTGATEWAY_VOICE_STT_ENGINE`: Gateway-scoped voice engine settings. The `ABSTRACTVOICE_*` names are also accepted by the lower package.
- `ABSTRACTGATEWAY_VOICE_TTS_MODEL` / `ABSTRACTGATEWAY_VOICE_STT_MODEL`: Gateway-scoped TTS/STT model defaults.
- `ABSTRACTGATEWAY_VOICE_REMOTE_BASE_URL` / `ABSTRACTGATEWAY_VOICE_REMOTE_API_KEY`: remote voice endpoint used by AbstractVoice.
- `GET /api/gateway/discovery/capabilities`: reports installed packages plus AbstractCore capability plugins for `voice`, `audio`, `vision`, and `music`; also returns `capabilities.contracts.version=1` with thin-client feature gates for AbstractFlow, AbstractAssistant, AbstractCode, shared run input/history endpoints, artifact search/import/export, direct voice/audio/image/video/music endpoints, workflow-backed image/video generation, and provider/session prompt-cache controls
- `GET /api/gateway/voice/voices`: proxies AbstractCore `/v1/audio/voices` when `ABSTRACTCORE_SERVER_BASE_URL` is configured; otherwise returns static Gateway/env voice descriptors.
- `GET /api/gateway/audio/speech/models`: proxies AbstractCore `/v1/audio/speech/models` when configured.
- `GET /api/gateway/audio/transcriptions/models`: proxies AbstractCore `/v1/audio/transcriptions/models` when configured.
- `GET /api/gateway/audio/music/providers`: proxies AbstractCore `/v1/audio/music/providers` when configured.
- `GET /api/gateway/audio/music/models`: proxies AbstractCore `/v1/audio/music/models` when configured.
- `GET /api/gateway/vision/provider_models`: proxies AbstractCore `/v1/vision/provider_models` when configured.
- `GET /api/gateway/vision/models`: reports locally known/cached AbstractVision model ids when the in-process capability path is available.
- `GET /api/gateway/vision/adapters`: lists installed compatible vision adapters for a provider/model/task combination through Runtime's discovery facade.
- `POST /api/gateway/runs/{run_id}/images/generate`: creates a durable Runtime child run for text-to-image and returns an artifact-backed image result. Optional `size`/`width`/`height`, batch `count` / `n`, `seeds`, and ordered `lora_adapters` values are passed through only when the client supplies them. Batch responses also return `image_artifacts` alongside the compatibility `image_artifact`.
- `POST /api/gateway/runs/{run_id}/images/edit`: creates a durable Runtime child run for image-to-image edits and optional mask-guided edits. Optional `size`/`width`/`height`, batch `count` / `n`, `seeds`, and ordered `lora_adapters` values are passed through only when the client supplies them. Batch responses also return `image_artifacts`.
- `POST /api/gateway/runs/{run_id}/images/upscale`: creates a durable Runtime child run for image upscaling from a run-visible `image_artifact`. Optional `resolution` accepts a shortest-edge integer or a scale factor such as `2x`; `scale`, `softness`, `seed`, `quantize`, and `vae_tiling` values are passed through only when the client supplies them.
- `POST /api/gateway/runs/{run_id}/videos/generate`: creates a durable Runtime child run for text-to-video and returns an artifact-backed video result. Optional batch `count` / `n`, `seeds`, ordered `lora_adapters`, and `flow_shift` values are passed through only when the client supplies them. Batch responses also return `video_artifacts`.
- `POST /api/gateway/runs/{run_id}/videos/from_image`: creates a durable Runtime child run for image-to-video and returns an artifact-backed video result. Optional batch `count` / `n`, `seeds`, ordered `lora_adapters`, and `flow_shift` values are passed through only when the client supplies them. Batch responses also return `video_artifacts`.
- `POST /api/gateway/runs/{run_id}/music/generate`: creates a durable Runtime child run and returns an artifact-backed music result for thin clients.

Direct image, image-edit, image-upscale, text-to-video, and image-to-video child runs advertise
`event_name=abstract.progress`. Thin clients should stream the returned
`child_run_id` ledger and render progress when the backend reports it; image
backends that do not expose step progress still emit at least a start record and
then the final artifact.

Core catalog proxy settings:

- `ABSTRACTCORE_SERVER_BASE_URL`: explicit Core server base URL for catalog proxying.
- `ABSTRACTGATEWAY_ABSTRACTCORE_SERVER_AUTH_TOKEN` / `ABSTRACTGATEWAY_ABSTRACTCORE_SERVER_API_KEY`
  (or Core's `ABSTRACTCORE_AUTH_TOKEN` / `ABSTRACTCORE_SERVER_API_KEY`): Core server auth token.
  This is separate from Gateway auth.
- `ABSTRACTGATEWAY_CORE_CATALOG_TIMEOUT_S`: catalog proxy timeout (default `3.0` seconds).

## CLI flags

`abstractgateway --help` shows all subcommands (serve/runner/migrate/triage/…).

Most-used:
- `abstractgateway serve [--host H] [--port P] [--data-dir DIR] [--no-runner] [--reload]`
  (host/port default to the [network exposure](#network-exposure-localhost--local-network--internet)
  setting; with none stored, `--host` defaults to `127.0.0.1` when no auth is
  configured, else `0.0.0.0`, and `--port` to `8080`. Explicit flags override the setting.)
  Evidence: `src/abstractgateway/cli.py`
- `abstractgateway network status|set|addresses|restart`: who can reach the
  gateway and the URLs to copy ([network exposure](#network-exposure-localhost--local-network--internet))
- `abstractgateway claim [--open] [--port P | --url URL] [--json]`: one-time
  console sign-in link ([first-run.md](docs/first-run.md))
- `abstractgateway service install|uninstall|enable|disable|status [--port P] [--host H] [--pin-command-line] [--data-dir DIR] [--dry-run] [--json]`:
  start the gateway at login (LaunchAgent, systemd user unit or XDG autostart
  entry, Windows Run entry; `enable`/`disable` are the tray's switch;
  [first-run.md](docs/first-run.md#4-start-the-gateway-at-login-optional)). The
  registration runs plain `serve`; `--host/--port` are written into the
  [network exposure](#network-exposure-localhost--local-network--internet)
  setting, or onto the command line with `--pin-command-line`
- `abstractgateway runner` (worker only)
- `abstractgateway config status --json`
- `abstractgateway config get [KEY] [--json]`, `config set KEY VALUE`, `config unset KEY`:
  runtime settings from a terminal ([backlog folder, exec runner, process
  manager](#backlog-folder-exec-runner-and-process-manager-continuum), and every other key)
- `abstractgateway serve --backlog-root PATH --exec-runner on|off`: the backlog
  folder and the exec runner for this run (they win over the saved settings until the gateway stops)
- `abstractgateway migrate --from=file --to=sqlite --data-dir <DIR> --db-path <FILE>`
- `abstractgateway models loaded|load|unload --url <URL> [--provider P --model M] [--force]`
  (model residency on a running gateway; see [console.md](docs/console.md#model-residency-from-a-shell))

## Related docs

- First run: [first-run.md](docs/first-run.md)
- Getting started: [getting-started.md](docs/getting-started.md)
- FAQ: [faq.md](docs/faq.md)
- Security configuration: [security.md](docs/security.md)
- Deployment: [deployment.md](docs/deployment.md)
- API overview: [api.md](docs/api.md)
- Operator tooling env vars: [maintenance.md](docs/maintenance.md)

---

## docs/apps.md

# Apps

The five browser apps (Observer, Continuum, Code, Entity and Flow Editor) can be
installed, started, stopped and updated from the gateway: from the console's
Apps page, from the HTTP API below, or with `abstractgateway apps`. Nobody has
to open a terminal or install Node.js by hand. The Apps page also lists the
desktop app, the **Assistant** (see [The Assistant](#the-assistant-a-desktop-app)).

On a card, a plain user sees one button for the state the app is in:

| State | Buttons |
|---|---|
| Not installed | **Install** |
| Installing | a progress bar (one row per part, e.g. "Code in the browser" and "Code in the terminal") and **Cancel** |
| Installed (running or not) | **Open**, and **Open in Terminal** right beside it when the app's terminal version is installed |
| Failed | the reason, **Show details**, and **Install** again |

Stop, Show log, Update, versions, addresses and commands are under
**Technical details**.

## What happens when you click Install

1. **Node.js.** The apps are small Node.js servers. If the gateway finds
   Node.js 18 or newer on the machine, it uses it. Otherwise it installs
   Node.js 24 for you in its own data folder (`<data dir>/runtime/node/`,
   about 56 MB to download, no administrator rights). This is the Node.js
   build published on PyPI as `nodejs-wheel-binaries`, the same one the
   installer's `--with-apps` option gets with `uv tool install nodejs-wheel`.
   The download is checked against PyPI's sha256 checksum.
2. **The app.** The gateway downloads `@abstractframework/<name>` from the npm
   registry into `<data dir>/apps/<app>/<version>/` and checks it against the
   registry's sha512 integrity hash. Apps that need other npm packages get them
   through npm (with its cache in `<data dir>/apps/npm-cache/`); Code needs
   none and is unpacked directly.
3. **The terminal app, too.** When the app also runs in a terminal and a
   ready-made download exists for this computer (Code, see
   [Terminal versions](#terminal-versions)), the same Install installs it
   right after the browser app: ONE job whose `parts` are the two rows the
   card shows (`install` then `install-tui`, each `waiting`, `running`,
   `done`, `failed`, `cancelled` or `skipped`). Cancel stops both. If the
   terminal part fails, the browser app stays installed and the job says so
   ("Code is installed for the browser, but its terminal app did not install:
   …"); with Technical details on, the card offers "Install terminal app" to
   try that part again. Without a ready-made download for this computer,
   Install installs the browser app only and the terminal app's command stays
   under Technical details. The row's `install_parts` (`["web"]` or
   `["web", "tui"]`) says in advance what Install covers.

Install only installs: nothing starts and no tab opens. The card then shows
**Open**, which starts the app on a free port when it is stopped, waits until
it answers, and opens it in a new tab, already signed in.

Every step is a job with a percentage, downloaded bytes and a plain message.
When a step fails, the job says why in one sentence and carries the full log
in its `details` field (`<data dir>/apps/jobs/<job>.log`).

## Running apps

- An app started from the gateway is a child process of the gateway. It stops
  when the gateway stops (even if the gateway is killed: the app watches the
  pipe the gateway holds open and exits when it closes).
- An app you started stays **enabled**: the next time the gateway starts, it
  starts the app again, on the same port when that port is free. Stop the app
  to turn this off (the console's **Stop** is under **Technical details**, or
  `abstractgateway apps stop <app>`). Nothing is registered with launchd, systemd or the login
  items; the gateway itself starts the apps.
- If an app exits unexpectedly, the gateway restarts it (after 1, 2 then 4
  seconds). After more than 3 crashes in a minute it stops trying and shows
  "crash_loop" with the app's log.
- Each app writes its output to `<data dir>/logs/apps/<app>.log`.
- Apps listen on `127.0.0.1` (the `apps.host` setting changes this). Ports: the app's usual port when it is
  free, else the first free port in 3100-3199. The usual ports are the
  framework's stack port map (`scripts/start-local.sh`): Observer 3001,
  Continuum 3002, Code 3003, Entity 3004, Flow 3005.

## Apps started outside the gateway

An app can also run without the gateway having started it: the framework's
development stack (`scripts/start-local.sh`), `npx @abstractframework/observer`,
a global npm install, a service. The gateway finds such an app by asking the
usual ports on this machine (3001-3005, then 3000 and 3007) for their start
page and reading its title
("AbstractObserver", "AbstractContinuum", "AbstractCode", "AbstractEntity",
"AbstractFlow"). None of the apps has an address that says who it is without
a gateway sign-in, and the start page is plain HTML, so this costs one short
local request per port (half a second at most, all ports at once, remembered
for 5 seconds). The version is read from the `package.json` of the program
listening on the port, when this computer shows which program that is.

Such an app is listed as installed and running, with `source: "external"`,
`managed: false`, its `url`, `port` and `version`, and one action: **Open**.
Opening it works exactly like opening an app the gateway started (the
one-time sign-in link below): the app's server reads the same sign-in
cookies whoever started it. The gateway does not stop, update or show the
log of an app it did not start; the console's **Technical details** says
"Started outside the gateway on port 3001" instead, and `POST /apps/{id}/stop`
answers 409 `started_outside_gateway`. Starting the gateway's own copy while
an outside one runs does nothing (the app is already running).

## Who may install

Installing an app (or Node.js, or a terminal version) runs software on the
gateway machine, so it follows the gateway's `allow_engine_install` setting.
With no saved choice, installs are allowed for someone at the gateway machine
itself, whatever address the gateway listens on (a browser or the tray on that
machine, including one that uses its network address), and for everyone when
the gateway listens on this machine only. A browser on another computer needs
an admin to turn the setting on. How "at the gateway machine" is decided is
in [configuration.md](docs/configuration.md#allow_engine_install).

## Opening an app signed in

The console asks the gateway for a one-time link
(`POST /api/gateway/apps/{id}/open`) and opens it in a new tab. The link
(`/apps/handover/<code>`) works once, for two minutes, and only on the address
it was made for. The gateway creates a browser session for the person who
clicked, puts it in the app's own sign-in cookies, and redirects to the app.
Browsers keep cookies per machine name, not per port, so the app's server finds
the session and the app opens connected. The gateway token never appears in
the page, the link or the browser's storage.

A browser on another computer cannot reach an app that listens on
`127.0.0.1`; the gateway says so instead of producing a broken link.

### Landing somewhere inside the app

`POST /api/gateway/apps/{id}/open` accepts an optional `path`: where inside
the app the browser lands after the handover, for example `/#new`, Entity's
creation form. The path is bound to the one-time code when the link is made
(the link itself carries nothing), and it must stay inside the app: it starts
with a single `/`, and a second leading slash (`//host`), a full address, a
backslash, a space or a control character is refused with 400
`invalid_app_path` before any link is made. Without `path` the browser lands
on the app's start page.

### An app with nothing in it yet

Each app row carries `content_summary`: a small fact about what the app holds
on this gateway, or `null` for apps that report nothing. Entity reports `{"entities_count": n}`: the number of entries
`GET /api/gateway/entities` lists (counted from the same entity registry,
without reading any entity), `null` when it cannot be known, including for an
Entity started outside the gateway whose page names another gateway. When the
count is exactly 0, the Entity card's button reads **Create your first
entity** and opens the app on `/#new` through the handover above; with one
entity or more, or an unknown count, it reads **Open**. The guide's Apps step
shows the same card.

## Terminal versions

Some apps also run in a terminal: **Code** (`abstractcode`, a
Rust terminal app from the abstractcode repository); Flow Editor, Observer,
Continuum and Entity are browser apps only. The gateway's own console also
has a terminal twin, `abstractgateway-console`, which the console's Done step
mentions once.

Every app row therefore lists its interfaces: `interfaces[0]` is the browser
app (it mirrors the row's own fields), and apps with a terminal version have a
second entry of kind `"tui"`:

```json
{"kind": "tui", "name": "Code in the terminal", "binary": "abstractcode",
 "installed": true, "version": "0.5.1", "source": "gateway", "path": "<data dir>/apps/bin/abstractcode",
 "latest_version": "0.5.1", "update_available": false,
 "install_available": false, "install_method": "release_binary", "install_blocked_reason": null,
 "install_command": "cargo install abstractcode", "download_page": "https://github.com/lpalbou/abstractcode/releases",
 "launch_available": true, "launch_blocked_reason": null, "launch_mode": "terminal",
 "command": "<data dir>/apps/bin/abstractcode --gateway http://127.0.0.1:8080",
 "signin_command": null, "active_job": null}
```

- **Found by presence.** The gateway never imports or runs an app to detect
  it: it looks for the binary in `<data dir>/apps/bin/`, on `PATH` and in
  `~/.cargo/bin`, and accepts it only when its `--help` names the program
  (PyPI's unrelated Python package `abstractcode` installs a script with the
  same name). `source` says where it was found (`gateway` or `path`).
- **Install (`install_method`).** `release_binary`: Code publishes prebuilt
  binaries for macOS (Apple silicon and Intel), Linux (x86_64 and arm64,
  glibc) and Windows (x86_64) on its GitHub release, with a `SHA256SUMS` file.
  The card's Install (and `install-tui` on its own) downloads the archive for
  this computer, checks it
  against `SHA256SUMS` and against the sha256 digest GitHub reports for the
  file (both must agree), unpacks the single binary into `<data dir>/apps/bin/`,
  makes it executable and runs `--version` before it replaces anything.
  About 3 MB, no administrator rights, no terminal. `cargo`: there is no
  prebuilt binary for this computer (musl Linux, Windows on ARM, other CPUs),
  or the program is published as source only (the gateway console, crates.io).
  Then `install_available` is `false`, `install_blocked_reason` starts with
  "Needs the Rust toolchain" and `install_command` is the exact command to
  copy; the console shows no Install button.
- **Open (`launch_mode`).** `terminal`: the caller is an admin on the gateway
  machine itself, and "Open in Terminal" opens a new terminal window there
  (macOS Terminal; on Linux the first of `x-terminal-emulator`,
  `gnome-terminal`, `konsole`, `xfce4-terminal`, `kitty`, `alacritty`, `xterm`
  when a desktop session exists; Windows `cmd`). `copy`: the browser is on
  another computer (or the caller is not an admin); with **Technical
  details** on, the card shows `command` (this gateway's address as the
  browser reaches it) and `signin_command` (`abstractcode login --gateway
  <url> --token <your token>`) to copy. A terminal is never opened for a
  remote browser. How the card presents each case: [console.md](docs/console.md#apps-tab).

### Signed in, without a token anywhere it could leak

The terminal window runs a small launcher script
(`<data dir>/apps/terminal/open-code-<random>.command`, mode 0700) that holds
a **one-time code** (two minutes, single use) and deletes itself as its first
line. It runs `tui_signin.py` from the gateway package (standard library only,
`python -I`), which trades the code at `POST /apps/tui-handover` for a bearer
token and then becomes the terminal app with the token in the app's
**environment** (`ABSTRACTCODE_GATEWAY_TOKEN`, which Code prefers over its
saved login). The token

- is new for each launch and is not the admin token;
- acts as the person who clicked (their identity and role, never more);
- is accepted only from this machine (a loopback socket peer);
- lives in the gateway's memory only, so it stops working when the gateway
  restarts (open the app again from the console);
- is never in a command line, a file, the page, or a URL.

### From a terminal

Everything above works without the console:

```bash
abstractgateway apps list                 # each app; Code also gets a "terminal:" line (installed, where, what to run)
abstractgateway apps install-tui code     # the same job: release binary, SHA256SUMS + GitHub digest, --version check
abstractgateway apps tui-command code     # the one-time sign-in line for THIS machine (2 minutes, works once) + the plain command
```

`install-tui` prints the job's progress; when there is no prebuilt binary for
the computer it prints the reason and `cargo install abstractcode` and exits 2.
`tui-command` makes the same one-use, self-deleting launcher as "Open in
Terminal" (it holds a single-use code, never a token) but opens no window: run
the printed line in a terminal on the gateway machine. From another computer
it prints the command and the `abstractcode login` line instead.

`/apps/tui-handover` sits outside `/api/gateway` like the browser handover:
the code in its body is its only credential, it answers only loopback socket
peers without proxy headers, and a browser handover code does not work there
(nor the reverse).

## How the apps are served

Each app runs its own small server, started by the gateway, rather than being
served as static files by the gateway. The app's server does real work:

- it holds the sign-in (`/api/connection/gateway`, HttpOnly session cookies,
  CSRF) and forwards `/api/*` to the gateway on the same origin, which is how
  live updates (server-sent events) reach the page;
- four of the five apps load their files from absolute `/assets/...` paths,
  and Code and Observer register a service worker at the site root, so they
  cannot live under a sub-path of the gateway;
- Observer reveals local folders, Flow keeps its connection file, and
  Continuum proxies the agora hub.

This is why each app has its own port. The gateway gives each server its
port, bind address and gateway URL: Continuum as launch flags (`--port`,
`--host`, `--gateway-url`), which win over its saved settings file; the
other four apps in the environment (`PORT`, `HOST`, `<APP>_GATEWAY_URL`).

## Settings

Five settings control the apps: `apps.node` (*Node.js for apps*),
`apps.ports` (*Ports for apps*), `apps.host` (*Where apps listen*),
`apps.npm_registry` (*npm registry*) and `apps.pypi_url` (*Node.js download
index*). Change them from the Apps page (*Advanced: apps settings*), the
terminal console (Runtimes → *Runtime knobs* → *Edit apps settings*) or the
CLI:

```bash
abstractgateway apps config get [NAME] [--json]
abstractgateway apps config set ports 3100-3199     # "" clears back to the default
```

A saved value applies at the next app start or download. Values, defaults and
the environment-variable fallbacks are listed in
[configuration.md](docs/configuration.md#browser-apps-settings-apps).

Install, update, start and stop need an admin, and installs follow
[Who may install](#who-may-install). Any signed-in user can list the apps and
open a running one (as themselves).

## Without internet

- Apps already installed keep working offline.
- Installing needs the npm registry (and PyPI for Node.js). When it cannot be
  reached, the Apps page says so on each app and the Install button is off;
  a job that loses the network fails with "The npm registry
  (registry.npmjs.org) is not reachable…". The gateway does not ship app
  copies: the five packages are about 6 MB, but four of them need npm
  dependencies (Flow alone installs to about 130 MB), which would have to
  ship too.

## Command line

The same actions, through the running gateway:

```bash
abstractgateway apps list                 # Node.js, every app, installed/latest, URL
abstractgateway apps install code            # the browser app and, where available, its terminal app
abstractgateway apps install code --launch   # ... and start it
abstractgateway apps install assistant       # the desktop Assistant, into the gateway's Python
abstractgateway apps launch assistant        # open it on this computer
abstractgateway apps launch observer
abstractgateway apps open observer        # prints a one-time signed-in link
abstractgateway apps logs observer --tail 50
abstractgateway apps update flow
abstractgateway apps stop observer
abstractgateway apps runtime              # install Node.js only
abstractgateway apps install-tui code     # Code's terminal version (see "Terminal versions")
abstractgateway apps tui-command code     # one-time signed-in launch line for this machine
abstractgateway apps jobs [JOB_ID]
```

`--url`, `--token` and `--data-dir` work as for `abstractgateway models`: by
default the command finds the gateway running for this data dir and uses its
admin token on a loopback URL.

## The Assistant (a desktop app)

AbstractAssistant (PyPI `abstractassistant`) is the framework's desktop
companion: a menu-bar app with a chat palette and hands-free voice
conversations. It is a Python app, not a browser app, so its card
(`kind: "desktop"`, id `assistant`, after the five browser apps) has no address
or port. The full `abstractframework` install already includes it, in the same
Python environment as the gateway; a gateway-only install may not.

- **Found by presence** (nothing is imported or started to find it): the
  `abstractassistant` command next to the gateway's own Python (or on `PATH`),
  the installed package (`importlib.util.find_spec`, without importing it; a
  folder that merely has the package's name does not count), and on macOS
  `AbstractAssistant.app` in `/Applications` or `~/Applications`. The version
  comes from the package, else from the app's `Info.plist`. It is **Running**
  when a process on this computer is the Assistant (its command, `python -m
  abstractassistant…`, or the app's own program). The tray's "Launch
  Assistant" uses the same detection (`apps_desktop.detect_assistant`), so
  the tray and the console always agree.
- **Install** installs `abstractassistant` into the gateway's own Python as a
  job (`uv pip install --python <gateway python> abstractassistant`, or pip
  when there is no uv), with every `abstract*` package the gateway runs
  pinned to its current version (`name==version` requirements in the same
  command), so installing the Assistant never changes the gateway. The same
  rule as the other installs decides who may install (see [Who may install](#who-may-install)).
- **Open** starts it on the gateway's computer: `open -a AbstractAssistant.app`
  when the app exists, otherwise its command, as a separate process with none
  of the gateway's tokens, secrets or keys in its environment. A running
  Assistant is not started twice: the app is brought to the front (or, when
  it was started from its command, the card says its icon is in the menu
  bar). The Assistant keeps its own connection settings (its Settings window,
  Connection): the gateway passes it no address and no token, and it has no
  one-time sign-in handover.
- **From another computer** the card says "The Assistant runs on the gateway's
  computer: open it there." with no button: it is a desktop app for that
  computer's screen.
- **Technical details** show its version, where it was found and the launch
  command (or the install command when it is not installed).

## HTTP API

All routes are under `/api/gateway/apps` and need a signed-in principal.

| Method and path | Who | What |
|---|---|---|
| `GET /apps?latest=true` | any user | Node.js status, one row per app (`kind` `web` with `interfaces[]`, see "Terminal versions", and `install_parts`; then the Assistant, `kind` `desktop` with `desktop {location, found_by, launch_command, install_command, launch_available, launch_blocked, launch_blocked_reason}`) and `console_tui` (the gateway console's terminal app). `latest=false` skips the npm registry and GitHub release lookups (cached 10 minutes). |
| `POST /apps/runtime/install` | admin | Install Node.js (a job), or `job: null` when one is already usable. |
| `POST /apps/{id}/install` `{"version"?, "launch"?, "with_terminal"?}` | admin | ONE job: Node.js if needed, download, check, dependencies, then the terminal app when the row's `install_parts` has `"tui"` (`with_terminal: false` skips it); the job's `parts` are its child rows. Starts nothing unless `launch: true`. For `assistant`: installs `abstractassistant` into the gateway's Python (every `abstract*` package is pinned to its current version in the same command). |
| `POST /apps/{id}/update` `{"version"?}` | admin | A job: install the latest (or given) version; a running app is restarted on it. |
| `POST /apps/{id}/launch` | admin | Start the app (waits until it answers) and mark it enabled. For `assistant`: open it on the gateway's computer, `{ok, app, already_running, message}`; from another computer 409 `not_on_gateway_machine`, and nothing starts. |
| `POST /apps/{id}/stop` | admin | Stop the app and mark it disabled. 409 `started_outside_gateway` for an app the gateway did not start. |
| `POST /apps/{id}/open` `{"remember"?, "path"?}` | any user | A one-time `open_url` (relative to the gateway) that opens the running app signed in, at `path` inside the app when given (e.g. `/#new`). 400 `invalid_app_path` for anything that is not a path inside the app. 409 `desktop_app` for the Assistant (use `/launch`). |
| `GET /apps/{id}/logs?tail=200` | admin | The end of the app's log. |
| `GET /apps/jobs`, `GET /apps/jobs/{job}` | any user | Jobs: `state` (queued, running, succeeded, failed, cancelled), `percent`, `bytes_done`, `bytes_total`, `message`, `steps`, `parts` (the child rows of an Install that covers two parts), `details` (full log on failure). |
| `POST /apps/jobs/{job}/cancel` | admin | Cancel a job. |
| `POST /apps/{id}/install-tui` | admin | A job: download, check and place the app's prebuilt terminal version (or update the gateway's copy). 409 `toolchain_required` with `command` when only a source build exists. |
| `POST /apps/{id}/launch-tui` | admin, on the gateway machine | Open the terminal version in a new terminal window, signed in as the caller: `{ok, app_id, interface: "tui", terminal, version, message, expires_in_s}`. From another computer (non-loopback peer or `Host`, or any proxy header): 409 `not_on_gateway_machine` with `command` and `signin_command`, and nothing opens. |
| `POST /apps/{id}/tui-command` | admin, on the gateway machine | The same one-use launcher as launch-tui without opening a window: `{ok, app_id, interface, version, signin_command, command, expires_in_s}`. From another computer: 409 `not_on_gateway_machine` with `command` and `signin_command`. |
| `POST /apps/tui-handover` `{"code"}` (outside `/api/gateway`) | the launcher script, loopback only | Trade a launcher's one-time code for `{token, token_env, url_env, gateway_url, gateway_flag, user}`. 403 `loopback_only`, 410 `handover_expired`. |

Errors are `{"ok": false, "reason", "message", "hint"?, "details"?}` with
404 (`unknown_app`, also for a terminal route on a browser-only app), 409
(`not_installed`, `node_missing`, `not_running`, `app_loopback_only`,
`toolchain_required`, `not_on_gateway_machine`, `no_terminal`,
`started_outside_gateway`), 403
(`installs_not_allowed`), 502 (`integrity_mismatch`), 503
(`network_unavailable`, `no_free_port`) or 500 (`launch_failed`, with the
app's output in `details`). The terminal errors also carry `command` (and,
where it helps, `install_command` or `signin_command`) so the console can
offer the command to copy instead.

---

## docs/tray.md

# 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 as
  `abstractgateway 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** — the last 24 hours of runs, newest first: a state badge
  (🟢 running, 🟡 waiting, ✅ completed, ❌ failed, ⚪️ cancelled), the step
  count and how long each took, under a one-line tally. Rows are information;
  **Open Runs in Console** at the bottom is the way in. Root runs only — a
  deep-research run spawns dozens of children and this is a glance. 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 in
  `skipped_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.**

The menu as shipped (macOS, a machine with 151 installed models; `[…]` is a
greyed information line, `▸` a submenu, `☐/☑` the check item):

```text
[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 ask pypi.org. 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:

```bash
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 |
| 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):

1. **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.
2. **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**.

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); **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 own copy in `<data dir>/apps/bin/`, `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:

```bash
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/health` carries `"paused": true` while `status` stays
  `"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** asks pypi.org for the latest release (5 s timeout, one
check per hour). Offline is a normal answer, not an error. The one-click
**Update** is offered only when the gateway can reproduce its own install:

| Install | Upgrade command used | One-click? |
|---|---|---|
| `pip` in a virtual environment | `python -m pip install --upgrade "abstractgateway[<your extras>]"` | yes |
| `uv venv` / `uv pip` | `uv pip install --python … --upgrade …` | yes |
| `pipx` | `pipx upgrade abstractgateway` | yes |
| `uv tool` | `uv tool upgrade abstractgateway` | yes |
| editable checkout (`pip install -e .`) | — update with `git pull` | no |
| Docker image | — pull the newer image | no |
| system Python (PEP 668 "externally managed") | — use pipx or a venv | no |

The installed extras (`apple`, `gpu`, `embeddings`) are detected and kept.
The upgrade runs in the background (log tail on the console's Gateway card);
when it finishes the tray offers **Restart to finish the update**. Restarting
is what makes the new version run — until then the process keeps serving the
old code.

## 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](docs/troubleshooting.md#there-is-no-tray-icon) for a
missing icon. Other cases:

- **The helper starts then disappears**: read `<data_dir>/logs/tray.log`;
  `GET /api/gateway/host/tray` reports `exit_code` and 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.

---

## docs/engines.md

# Local engines: install, start, stop

The gateway can install the local inference engines (Ollama, LM Studio, MLX,
llama.cpp, vLLM, the Hugging Face stack) on the machine it runs on, from the
console or the command line, without a terminal and without a password in the
common case. This page explains what each **Install** does, when it asks for
the Apple command-line tools or an administrator password, and the API a UI
renders.

## What Install does, engine by engine

| Engine | macOS (Apple silicon) | Linux | Admin rights? |
|---|---|---|---|
| **Ollama** | Downloads the signed `Ollama-darwin.zip` from Ollama's official GitHub release, checks its published SHA-256 and the Developer ID signature (team `3MU9H2V9Y9`), places `Ollama.app` in `/Applications` when your account can write it (an admin account can, with no password) or else in `~/Applications`, then starts it and waits until `http://127.0.0.1:11434/api/version` answers | Ollama's official installer (`curl -fsSL https://ollama.com/install.sh \| sh`) | macOS: no. Linux: yes (the installer writes `/usr/local` and a systemd service) |
| **LM Studio** | Downloads the signed disk image from lmstudio.ai, checks the signature (team `D65G88RHWN`), copies `LM Studio.app` to `/Applications` or `~/Applications`, opens it once and starts its local server (`lms server start`) | LM Studio's official installer: the headless daemon and the `lms` CLI under `~/.lmstudio` | no |
| **MLX** | Prebuilt `mlx` and `mlx-lm` wheels into the gateway's own Python | not supported | no |
| **llama.cpp** | Upstream's prebuilt Metal wheel (`llama-cpp-python` 0.3.28) into the gateway's own Python; no compiler | prebuilt CPU wheel (0.3.35) | no |
| **vLLM** | not supported (the row says why; no Install button) | Linux with an NVIDIA GPU: the vLLM wheels into the gateway's Python | no |
| **Hugging Face** | `abstractcore[huggingface]` at the installed AbstractCore version (several GB) | same | no |

On Windows (experimental), installs use the vendor commands AbstractCore
plans (winget, the vendor PowerShell installers, pip).

Everything the gateway's own Python receives is installed with the gateway's
`abstract*` packages pinned to the versions it runs, so an engine install can
never change the gateway itself.

### When the Apple command-line tools are needed

Only for llama.cpp when no prebuilt wheel fits (an Intel Mac, a Python under
Rosetta, or a broken wheel): it must then be built from source, which needs a
C compiler. The job stops **before** the build, in the `needs_tools` state,
with the reason in plain words, for example:

> The prebuilt llama.cpp wheel did not install (no matching package for this
> Python and machine); building from source needs the Apple command-line tools.

**Install tools** runs `xcode-select --install`, which opens Apple's own
installer dialog on the gateway machine's screen (no administrator password
involved). The job waits and continues by itself once the tools are there.

### When an administrator password is needed

Only when a step genuinely needs it, and never silently:

- you asked for `/Applications` (`"location": "system"`) and your account
  cannot write it (a standard, non-admin account);
- Ollama on Linux (its installer writes `/usr/local` and creates a service).

The job stops in the `needs_admin` state and shows the exact reason and the
exact command. Nothing runs with administrator rights until someone presses
**Continue with administrator password**; the gateway then asks the operating
system: on macOS the standard password dialog (`osascript … with
administrator privileges`), on a Linux desktop `pkexec`. On a machine that
cannot show a dialog (a headless server), the job shows the command to run in
a terminal and a **re-check** button. There is no hidden `sudo` anywhere.

### What you see when something fails

The job's `message` is one plain sentence (what failed and why); the whole
log (every line, never a tail) is behind it in `details` and in a log file in
the gateway's data folder (`engines/jobs/<job id>.log`). A failed llama.cpp
build, for example, reads:

> The prebuilt llama.cpp wheel did not install (the wheel file is corrupted),
> and building it from source failed: no C compiler was found. The full build
> log is in the details.

## Command line

```bash
abstractgateway engines status --probe
abstractgateway engines install ollama --yes            # [--location auto|user|system] [--force] [--dry-run]
abstractgateway engines continue <job-id>               # after needs_admin (password dialog) or needs_tools
abstractgateway engines continue <job-id> --action install_tools
abstractgateway engines cancel <job-id>
abstractgateway engines start ollama                     # stop | start: ollama, lmstudio
```

A job that stops for tools or an administrator exits with code 2 and prints
what it needs and the `continue` command.

## API (contract `gateway_engines_v2`)

All routes are under `/api/gateway`. Reads are user-level; every POST is
admin-only; `install` and `continue` also need `allow_engine_install`
([configuration.md](docs/configuration.md#allow_engine_install)); a dry run never does.

| Method and path | Returns |
|---|---|
| `GET /engines?probe=1` | `{schema: "gateway_engines_v2", engines: [row…], install_allowed, install_policy, host, active_job}` |
| `GET /engines/{id}?probe=1` | one row; 404 for an unknown id |
| `POST /engines/{id}/install` `{dry_run?, force?, location?: auto\|user\|system}` | a job (below); `dry_run` returns the plan; 409 `busy` while another engine installs (the same engine joins its job); 409 `unsupported_on_this_machine` |
| `GET /engines/jobs` | `{jobs: [job…]}`, newest first |
| `GET /engines/jobs/{job_id}` | one job; 404 |
| `POST /engines/jobs/{job_id}/continue` `{action?: approve_admin\|install_tools\|recheck}` | the job, resumed; 409 when it is not waiting |
| `POST /engines/jobs/{job_id}/cancel` | the job |
| `POST /engines/{id}/start`, `/stop` | `{ok, engine, action, running, base_url, …}`; 409 for an engine that is not a server |

**Row:** `id, name, description, supported, support_reason, installed,
version, install_location, running, reachable, base_url, models_count,
install {available, method: wheel|app|script|unsupported, target, needs_admin,
admin_reason, needs_tools, tools_action, notes, steps, command_preview, url,
allowed, fallback {kind: open_page, url, recheck}}, actions [{id, label,
enabled, reason?, method?, path?, url?}], active_job`. Action ids: `install`,
`open_page`, `recheck`, `start`, `stop`, `docs`. `running` is known only on a
probed read, so `start`/`stop` appear only with `probe=1`.

**Job (`engine_install_job_v1`):** `job_id, engine, state, percent,
bytes_done, bytes_total, message, details, log_path, admin_prompt {key,
reason, command, method: osascript|pkexec|manual, button, prompt_text,
where}, tools_prompt {key, reason, tools, action {kind, command, available,
button}, started}, continue_actions, can_cancel, events [{at, state, message,
percent}], result, error {code, message}, started_at, updated_at,
finished_at`. `state` is one of `queued, downloading, installing, needs_admin,
needs_tools, done, failed, cancelled`. While a step runs quietly, `message`
says so at least every 3 seconds (`… (still working, 45 s)`). The job also
carries `status` (`host_job_v1` words) so clients polling `GET /jobs/{id}`
read the same lifecycle as other host jobs.

Jobs live in the gateway process: a restart forgets them (the log files stay).

---

## docs/model-downloads.md

# Model downloads: progress you can see

Every model download the Gateway starts is a job that reports real progress
(bytes, total, percent, speed, time left, per file) from the first second to
the last, for every source: Hugging Face (and MLX / mlx-gen), Ollama,
LM Studio and Supertonic. A download that stops receiving bytes says it is
stalled; a cancelled download stops within about a second and never leaves
files that later read as installed.

The job machinery is AbstractCore's (`abstractcore.config.host_jobs`,
`host_job_v1`); the Gateway serves it unchanged and adds the parent job for
"Use recommended defaults", the cancel route and the event stream.

## Routes

All under `/api/gateway`.

| Method and path | Access | Returns |
|---|---|---|
| `POST /models/download` `{"provider", "artifact", "dry_run"?, "expected_bytes"?}` | admin | `{"ok": true, "job": {...}}` at once; the bytes move in the background |
| `POST /models/download` `{"recommended": true}` | admin | `{"ok": true, "recommended": true, "jobs": [one per model], "group": {parent}}` |
| `GET /models/download/{job_id}` | user | `{"ok": true, "job": {...}}`; a `grp_...` id returns the parent; 404 when unknown (jobs are per Gateway process) |
| `GET /models/downloads` | user | `{"ok": true, "jobs": [...]}`, newest first; parents are listed, and each child names its parent in `parent_job` |
| `POST /models/download/{job_id}/cancel` `{"via": "console"}`? | admin | `{"ok": true, "job": {...}}` with `cancel_requested: true` and `cancelled_by`; the job turns `cancelled` when the tool has stopped (normally < 1 s); a parent cancels every running child; 404 when unknown. A console sends `{"via": "console"}` when a person clicked Cancel; without it the cancel is recorded as `api` |
| `GET /models/downloads/stream` | user | Server-Sent Events, below |

### The event stream

`GET /models/downloads/stream` sends `event: downloads` with
`data: {"jobs": [...]}` (the same list as `GET /models/downloads`) each time
something changed, at most every 0.5 s, and a `: keepalive` comment every
15 s. `?job_id=<id>` streams one job as `event: job` / `data: {"job": {...}}`.
`?until_idle=1` ends the stream once nothing is running (after sending the
final state). Polling keeps working; the stream is optional.

## The job

| Field | Meaning |
|---|---|
| `job_id` (also `job`) | `dl_...` for one download, `grp_...` for a parent |
| `kind` | `download`, or `download_group` for a parent |
| `status` | the coarse lifecycle for simple pollers: `running` (queued included), `completed`, `failed`, `cancelled`; `host_status` keeps AbstractCore's own word |
| `state` | `queued`, `resolving`, `downloading`, `verifying`, `installing`, `done`, `failed`, `cancelled`, `stalled` |
| `bytes_done`, `bytes_total` | bytes so far and the total; same values as `downloaded_bytes`, `total_bytes` |
| `size_unknown`, `size_note` | `true` only when the source cannot say how big the download is; `size_note` says why |
| `percent` | `bytes_done / bytes_total × 100`; 100 when done; `null` while the size is unknown |
| `bytes_per_second` | speed over the last 5 s; falls to 0 when bytes stop, never frozen at its last value |
| `eta_s` | seconds left at that speed; `null` when unknown or stalled |
| `started_at`, `updated_at`, `finished_at` | ISO-8601 UTC; `updated_at` moves at least every 0.5 s while the job runs |
| `message` | one plain sentence to show as is |
| `detail` | the engine tool's own last line, unchanged |
| `files` | `[{name, bytes_done, bytes_total, state}]`, file state `pending`, `downloading`, `done`, `failed`, `cancelled`; Ollama layers are named `layer <digest>` |
| `current_file` | the file arriving now |
| `error` | the full reason when `failed` (the tool's own words) |
| `ended_reason` | when `failed` or `cancelled`: one plain sentence saying what happened and what a new download reuses, e.g. "The connection to Hugging Face dropped after 200 MB of 266 MB. Check the network connection, then download it again; the files that finished are kept, and the file that was in progress starts over." or "Cancelled in the console by admin at 21:15 after 105 MB of 275 MB. ..." |
| `stall_after_s`, `stalled_for_s` | the stall threshold (default 15 s) and how long the current stall has lasted |
| `transitions` | `[{at, state, why}]`, every state change |
| `cancel_requested` | `true` from the cancel request until the job ends |
| `cancelled_by`, `cancelled_by_user` | who asked for the cancel: `console` (a person clicked Cancel in a console), `api` (any other HTTP cancel), `cli` (`abstractcore models cancel`, Ctrl-C), `other_process` (a cancel marker from another program); the signed-in account when known. `null` unless a cancel was requested |
| `parent_job` | on a child of a parent job |

A parent (`download_group`) adds `children` (the full child jobs),
`child_job_ids` and `label`, and uses `files` for one row per model. Its
bytes, percent and speed add up its children; models already installed count
as done with nothing to fetch. Its `ended_reason` joins its children's. It is `stalled` only when every running child
is stalled, `failed` when every child ended and one failed (`error` names
which, with its reason), `cancelled` when one was cancelled, and `done` when
all are.

### States

- `queued`: accepted, not started (milliseconds for an in-process job).
- `resolving`: finding what to fetch and how big it is (hub file list, `lms get` search, Ollama manifest).
- `downloading`: bytes are moving.
- `stalled`: no bytes for `stall_after_s` seconds (15 by default,
  `ABSTRACTCORE_DOWNLOAD_STALL_S` on the Gateway host). The job keeps trying
  and turns back to `downloading` by itself when bytes arrive again. The
  stall and the recovery are logged (`abstractcore.host_jobs`) and listed in
  `transitions`.
- `verifying`: checking what arrived (Ollama's sha256, every Hugging Face file whole).
- `installing`: moving into the library (Ollama "writing manifest", LM Studio "Finalizing download...").
- `done`, `failed`, `cancelled`: finished. `cancelled` only follows a cancel request
  (`cancelled_by` says whose); a download that stops on its own -- a dropped
  connection, a Hub error, a full disk, the Gateway restarting (the job then
  reads `failed` from its saved snapshot, and the transfer stops with it) -- is
  `failed`, with `ended_reason`.

`verifying` and `installing` never count as stalls.

### Examples

One job per state, key fields only (sizes are illustrative).

`queued`:

```json
{"job_id": "dl_2b0e98639928", "provider": "ollama", "artifact": "all-minilm", "status": "running", "state": "queued", "bytes_done": null, "bytes_total": null, "size_unknown": false, "percent": null, "bytes_per_second": null, "eta_s": null, "updated_at": "2026-09-24T03:28:38.435Z", "current_file": null, "message": "queued (detached)", "error": null, "stalled_for_s": null}
```

`resolving`:

```json
{"job_id": "dl_be2ff93db7d6", "provider": "mlx-gen", "artifact": "AbstractFramework/flux.2-klein-4b-8bit", "status": "running", "state": "resolving", "bytes_done": null, "bytes_total": null, "size_unknown": false, "percent": null, "bytes_per_second": null, "eta_s": null, "updated_at": "2026-09-24T03:27:55.745Z", "current_file": null, "message": "Preparing · reading the file list of AbstractFramework/flux.2-klein-4b-8bit", "error": null, "stalled_for_s": null}
```

`downloading`:

```json
{"job_id": "dl_f46f63c01098", "provider": "mlx-gen", "artifact": "AbstractFramework/flux.2-klein-4b-8bit", "status": "running", "state": "downloading", "bytes_done": 25976720, "bytes_total": 65005200, "size_unknown": false, "percent": 39.96, "bytes_per_second": 7699301.9, "eta_s": 6, "updated_at": "2026-09-24T03:25:15.363Z", "current_file": "transformer/diffusion_pytorch_model.safetensors", "message": "Downloading transformer/diffusion_pytorch_model.safetensors (2 of 5) · 26 MB of 65 MB · 7.7 MB/s · 6 s left", "error": null, "stalled_for_s": null, "files": [{"bytes_done": 1200, "bytes_total": 1200, "name": "model_index.json", "state": "done"}, {"bytes_done": 10485760, "bytes_total": 40000000, "name": "transformer/diffusion_pytorch_model.safetensors", "state": "downloading"}, {"bytes_done": 10485760, "bytes_total": 20000000, "name": "text_encoder/model.safetensors", "state": "downloading"}, {"bytes_done": 5000000, "bytes_total": 5000000, "name": "vae/diffusion_pytorch_model.safetensors", "state": "done"}, {"bytes_done": 4000, "bytes_total": 4000, "name": "README.md", "state": "done"}]}
```

`stalled`:

```json
{"job_id": "dl_f8684d929f51", "provider": "supertonic", "artifact": "supertonic-3", "status": "running", "state": "stalled", "bytes_done": 25942208, "bytes_total": 56185929, "size_unknown": false, "percent": 46.17, "bytes_per_second": 0.0, "eta_s": null, "updated_at": "2026-09-24T03:27:38.444Z", "current_file": "onnx/vector_estimator.onnx", "message": "Stalled: no data for 16 s · 26 MB of 56 MB · still trying, it resumes by itself when data flows again", "error": null, "stalled_for_s": 15.3}
```

`verifying`:

```json
{"job_id": "dl_3d015e06ebaa", "provider": "mlx-gen", "artifact": "AbstractFramework/flux.2-klein-4b-8bit", "status": "running", "state": "verifying", "bytes_done": 65005200, "bytes_total": 65005200, "size_unknown": false, "percent": 100.0, "bytes_per_second": 3742244.1, "eta_s": 0, "updated_at": "2026-09-24T03:28:21.002Z", "current_file": "transformer/diffusion_pytorch_model.safetensors", "message": "Verifying · checking 5 file(s) are whole · 65 MB of 65 MB", "error": null, "stalled_for_s": null}
```

`installing`:

```json
{"job_id": "dl_faa442ea7875", "provider": "lmstudio", "artifact": "qwen/qwen3.5-9b@4bit", "status": "running", "state": "installing", "bytes_done": 6000000000, "bytes_total": 6000000000, "size_unknown": false, "percent": 100.0, "bytes_per_second": 466469568.4, "eta_s": 0, "updated_at": "2026-09-24T03:26:40.607Z", "current_file": null, "message": "Installing · Finalizing download... · 6.0 GB of 6.0 GB", "error": null, "stalled_for_s": null}
```

`done`:

```json
{"job_id": "dl_e36dca05d05c", "provider": "supertonic", "artifact": "supertonic-3", "status": "completed", "state": "done", "bytes_done": 56185929, "bytes_total": 56185929, "size_unknown": false, "percent": 100.0, "bytes_per_second": null, "eta_s": 0, "updated_at": "2026-09-24T03:26:41.409Z", "current_file": null, "message": "Downloaded 56 MB in 15 s", "error": null, "stalled_for_s": null}
```

`failed`:

```json
{"job_id": "dl_842ede4aff62", "provider": "ollama", "artifact": "all-minilm", "status": "failed", "state": "failed", "bytes_done": null, "bytes_total": null, "size_unknown": false, "percent": null, "bytes_per_second": null, "eta_s": null, "updated_at": "2026-09-24T03:27:14.156Z", "current_file": null, "message": "cannot reach the Ollama server at http://127.0.0.1:11499: [Errno 61] Connection refused", "error": "cannot reach the Ollama server at http://127.0.0.1:11499: [Errno 61] Connection refused", "stalled_for_s": null}
```

`cancelled`:

```json
{"job_id": "dl_be2ff93db7d6", "provider": "mlx-gen", "artifact": "AbstractFramework/flux.2-klein-4b-8bit", "status": "cancelled", "state": "cancelled", "bytes_done": 25976720, "bytes_total": 65005200, "size_unknown": false, "percent": 39.96, "bytes_per_second": null, "eta_s": null, "updated_at": "2026-09-24T03:27:59.076Z", "current_file": "transformer/diffusion_pytorch_model.safetensors", "message": "cancelled", "error": null, "stalled_for_s": null}
```

`download_group (downloading)`:

```json
{"job_id": "grp_080bae3c6d3e", "status": "running", "state": "downloading", "bytes_done": 0, "bytes_total": null, "size_unknown": true, "percent": null, "bytes_per_second": null, "eta_s": null, "updated_at": "2026-09-24T03:26:26.753Z", "message": "Downloading 3 models · 0 of 3 ready · 3 of 3 sources cannot report their size yet", "error": null, "kind": "download_group", "files": [{"bytes_done": null, "bytes_total": null, "job_id": "dl_faa442ea7875", "name": "lmstudio qwen/qwen3.5-9b@4bit", "state": "downloading"}, {"bytes_done": null, "bytes_total": null, "job_id": "dl_e36dca05d05c", "name": "supertonic supertonic-3", "state": "resolving"}, {"bytes_done": null, "bytes_total": null, "job_id": "dl_8dde3c3f4834", "name": "mlx-gen AbstractFramework/flux.2-klein-4b-8bit", "state": "resolving"}], "children": "[3 child jobs]"}
```

`download_group (stalled)`:

```json
{"job_id": "grp_a1e3f58d4cd0", "status": "running", "state": "stalled", "bytes_done": 6090947408, "bytes_total": 6121191129, "size_unknown": false, "percent": 99.51, "bytes_per_second": 0.0, "eta_s": null, "updated_at": "2026-09-24T03:25:34.055Z", "message": "Stalled: no data for 16 s from supertonic supertonic-3 · 2 of 3 ready · 6.1 GB of 6.1 GB · 0 B/s", "error": null, "kind": "download_group", "files": [{"bytes_done": 6000000000, "bytes_total": 6000000000, "job_id": "dl_ec66382e80a1", "name": "lmstudio qwen/qwen3.5-9b@4bit", "state": "done"}, {"bytes_done": 25942208, "bytes_total": 56185929, "job_id": "dl_2b7360ab34fe", "name": "supertonic supertonic-3", "state": "stalled"}, {"bytes_done": 65005200, "bytes_total": 65005200, "job_id": "dl_f46f63c01098", "name": "mlx-gen AbstractFramework/flux.2-klein-4b-8bit", "state": "done"}], "children": "[3 child jobs]"}
```

## What each source reports

| Source | What the source exposes | What the job reports | Cancel |
|---|---|---|---|
| Hugging Face, MLX, mlx-gen | the hub's file list with sizes and blob names; the files being written in the cache (`blobs/<etag>…incomplete`) | the total and every file before the first byte, then per-file bytes read from disk every 0.25 s; files already complete count as done (the download resumes) | the transfer runs in a child process, stopped at once; its temporary files are removed; a marker in the repo folder keeps an unfinished download from reading as installed |
| Ollama | `/api/pull` lines with `digest`, `total`, `completed` per layer | layers added up into one total that never goes back, one `files` row per layer; "pulling manifest" is `resolving`, "verifying sha256 digest" `verifying`, "writing manifest" `installing` | the connection is closed at once; Ollama keeps the layers it has |
| LM Studio (`lms get`) | a progress bar with bytes, total, speed and time left | those numbers; "Finalizing download..." is `installing`. When no bar is printed: the bytes landing in the LM Studio models folder, "LM Studio reports no progress; N MB on disk so far", with `size_unknown: true` unless the catalog size was sent as `expected_bytes` | answers `lms get`'s "continue in the background?" with No, so LM Studio stops too, then stops the CLI |
| Supertonic (voice) | the size of each file (HEAD), then the file bodies | per-file bytes with the total known before the first byte | stops at once; the partial file is removed, finished files are kept |

Hugging Face downloads started from a job use plain HTTP rather than Xet:
Xet writes a file only once it is complete, so the bar would sit still and
then jump. `ABSTRACTCORE_HF_XET=1` turns Xet back on (progress then moves one
whole file at a time). With huggingface_hub 1.x a cancelled file restarts
from zero on the next download; files that were complete are kept.

## Limits

- Parent jobs live in the Gateway process: after a restart a `grp_...` id
  answers 404, while its children (AbstractCore jobs, persisted) can still be
  read with `GET /models/download/{dl_id}` or `GET /jobs`.
- `lms get` is LM Studio's own CLI. Its progress bar and its cancel question
  are what the job reads; when `lms` prints neither, the job falls back to
  bytes on disk and a plain stop of the CLI.

---

## docs/api.md

# AbstractGateway — API overview

The HTTP API is implemented with FastAPI under the `/api` prefix:
- Health: `GET /api/health`
- Gateway surface: `/api/gateway/*` (durable runs + operator tooling)

The API is documented at runtime:
- OpenAPI JSON: `GET /openapi.json`
- Swagger UI: `GET /docs` (use **Authorize** to paste the bearer token)

Context:
- In the AbstractFramework ecosystem, UIs and automations call this API to operate **AbstractRuntime** runs.
- Architecture diagram and core concepts: [architecture.md](docs/architecture.md)

## Route families

This page covers the run contract, artifacts, discovery, media, models and
host state. Other route families are documented next to the feature they
serve:

| Routes | Purpose | Reference |
|---|---|---|
| `/api/gateway/session/login`, `/session/logout`, `/session/claim`, `/me` | browser sessions, one-time sign-in links, the current principal | [security.md](docs/security.md), [first-run.md](docs/first-run.md) |
| `/api/gateway/admin/users`, `/admin/runtime-reservations` | user accounts and retained runtimes (admin) | [security.md](docs/security.md#tenant-and-user-isolation) |
| `/api/gateway/admin/runtime-config` | runtime settings (admin) | [configuration.md](docs/configuration.md) |
| `/api/gateway/network`, `/network/restart` | network exposure, addresses, reverse proxy | [configuration.md](docs/configuration.md#api-gateway_network_v1) |
| `/api/gateway/apps/*`, `/apps/handover/{code}`, `/apps/tui-handover` | browser apps, terminal apps, the Assistant | [apps.md](docs/apps.md#http-api) |
| `/api/gateway/engines/*` | local engine installs | [engines.md](docs/engines.md#api-contract-gateway_engines_v2) |
| `/api/gateway/models/download*`, `/models/downloads*` | model download jobs and their event stream | [model-downloads.md](docs/model-downloads.md) |
| `/api/gateway/host/*` | host state, pause, restart, update, tray | [Host state](#host-state-and-model-residency), [Host control](#host-control-pause-desktop-tray-restart-update) |
| `/api/gateway/backlog/*`, `/reports/*`, `/triage/*`, `/processes` | operator tooling | [maintenance.md](docs/maintenance.md) |
| `/api/gateway/entities/*` | summoned entities | [entities.md](docs/entities.md) |

## Auth

By default, `/api/gateway/*` is protected by `GatewaySecurityMiddleware` (bearer token + origin allowlist).
See: [security.md](docs/security.md).

All examples below assume:

```bash
export BASE_URL="http://127.0.0.1:8080"
export AUTH="Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")"
```

## Provider connections

Gateway-owned provider connections let users create reusable cloud, local, or
OpenAI-compatible endpoints without putting raw API keys in workflow JSON or
browser storage. The API route is named `provider-endpoint-profiles`; the
console presents them as provider connections.

- `GET /api/gateway/config/provider-endpoint-profiles`: list visible profiles.
- `POST /api/gateway/config/provider-endpoint-profiles`: create a user- or
  admin-owned profile.
- `POST /api/gateway/config/provider-endpoint-profiles/discover-models`:
  discover models for a draft or saved profile by calling the configured
  provider family and base URL with the entered or server-side key. The raw key
  is never returned.
- `PUT` or `DELETE /api/gateway/config/provider-endpoint-profiles/{profile_id}`:
  update or delete a profile.

Enabled profiles appear in `GET /api/gateway/discovery/providers` as virtual
providers such as `endpoint:office-vllm`. Model discovery through
`GET /api/gateway/discovery/providers/{provider_name}/models` returns either the
fixed profile allowlist or the live endpoint model catalog.

## Core workflow lifecycle

### 1) List bundles (bundle mode)

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/bundles"
```

Upload a bundle:

```bash
curl -sS -H "$AUTH" \
  -F "file=@./my-bundle@0.1.0.flow" \
  -F "overwrite=false" \
  -F "reload=true" \
  "$BASE_URL/api/gateway/bundles/upload"
```

### 2) Start a run

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","input_data":{"prompt":"Hello"}}' \
  "$BASE_URL/api/gateway/runs/start"
```

If you need a specific entrypoint:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","flow_id":"ac-echo","input_data":{"prompt":"Hello"}}' \
  "$BASE_URL/api/gateway/runs/start"
```

Evidence: request/response models live in `src/abstractgateway/routes/gateway.py` (`StartRunRequest`, `start_run`).

For VisualFlow bundles, Gateway runs the packed JSON through AbstractRuntime.
Structured LLM/Agent schemas are Runtime/Core-owned: `response` remains textual,
and schema-conformant object values are available through the node `data` output
for data edges such as Break Object and Switch.

#### Durable session replay (`use_session_history`)

Thin clients do not need to carry conversation transcripts. Passing
`"input_data": {"use_session_history": true}` together with a `session_id`
makes the gateway seed the run's `context.messages` from the session's prior
COMPLETED root runs before the run starts: the run store is the durable
transcript.

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","session_id":"sess-1","input_data":{"prompt":"and what did I say before?","use_session_history":true}}' \
  "$BASE_URL/api/gateway/runs/start"
```

Rules (the model-vs-display divergence contract — what the model replays is
deliberately narrower than what history views display):

- Client-provided non-empty `context.messages` always win; the seed never
  overwrites them. An EMPTY client `context.messages` list does not count as
  a transcript — the seed still runs (use the cap below to disable).
- Only COMPLETED root runs of the session contribute, as strictly alternating
  user/assistant pairs. FAILED and CANCELLED turns are invisible to replay by
  design (a promptless answer or answerless prompt would seed a dangling
  message and invite re-answering a stale ask); history views still show them.
- Steering/operator guidance injected mid-run is not replayed; over-long
  messages are truncated with a labeled `#TRUNCATION` marker; whole oldest
  turns are dropped first (`session_history_max_chars` cumulative budget).
- Caps: `input_data.session_history_max_messages` (1..200; explicit `0`
  disables replay for the run) > `ABSTRACTGATEWAY_SESSION_HISTORY_MAX_MESSAGES`
  > default 40. Chars: `session_history_max_chars` >
  `ABSTRACTGATEWAY_SESSION_HISTORY_MAX_CHARS` > default 24000.
- Failures degrade to a labeled `_runtime.session_history` `#FALLBACK` note
  and an unseeded start — never a blocked run. Success records
  `_runtime.session_history = {seeded: N, ...}` on the run for observability.
- Entity lanes never ride this: their transcript authority is the entity home
  (`_visit.history` / the chat driver), not the run store.

Evidence: `_seed_session_history` in `src/abstractgateway/hosts/bundle_host.py`
and `abstractruntime.session_history.session_chat_messages`.

### 2b) Schedule a run (bundle mode)

`POST /api/gateway/runs/schedule` starts a **scheduled parent run** that launches the target workflow as child runs over time.

Example (run 3 times, every hour, starting now):

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","flow_id":"ac-echo","input_data":{"prompt":"Ping"},"start_at":"now","interval":"1h","repeat_count":3,"share_context":true,"session_id":"sess-1"}' \
  "$BASE_URL/api/gateway/runs/schedule"
```

Notes:
- `start_at`: ISO 8601 timestamp (recommended) or `"now"`.
- `interval`: e.g. `"15m"`, `"1h"`, `"2d"`. If omitted, runs once.
- `repeat_count`: if omitted and `interval` is set, repeats forever. Alternatively use `repeat_until` (ISO 8601).
- To stop a schedule, cancel the scheduled parent run via `POST /api/gateway/commands` with type `cancel`.

Evidence: `ScheduleRunRequest`, `start_scheduled_run` in `src/abstractgateway/routes/gateway.py`.

### 2c) Shared workflow catalog

Private `/api/gateway/bundles` routes are scoped to the signed-in user's routed
runtime, and you may change the registry you own. The gateway's own bundle
directory is shared by every user, so writing it — upload, delete, reload,
deprecate, and `POST /visualflows/{flow_id}/publish` — requires an admin
principal and otherwise returns `403`. Listing and running are unaffected. See
[security.md](docs/security.md) for the full rule.

Shared/default workflows use the Gateway workflow catalog instead:

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/workflow-catalog"
```

Admin-only catalog operations live under
`/api/gateway/admin/workflow-catalog/*`:

- upload or promote immutable `.flow` versions;
- move a bundle's default pointer;
- set ACLs;
- deprecate, block, or tombstone a version without deleting bundle bytes.

Start a catalog workflow in the requesting user's runtime by setting
`registry_scope`:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"registry_scope":"tenant_catalog","bundle_id":"basic-agent","flow_id":"root","input_data":{"prompt":"Hello"}}' \
  "$BASE_URL/api/gateway/runs/start"
```

If `bundle_version` is omitted, Gateway uses the admin-managed catalog default
pointer. Exact older versions keep working until that specific version is
deprecated, blocked, or tombstoned.

Catalog scope is explicit: omitting `registry_scope` starts only private
runtime bundles. Flow/schema inspection for catalog workflows should use the
ACL-aware catalog endpoints:

- `GET /api/gateway/workflow-catalog/{bundle_id}/versions/{bundle_version}/flows/{flow_id}`
- `GET /api/gateway/workflow-catalog/{bundle_id}/versions/{bundle_version}/flows/{flow_id}/input_schema`

`framework_catalog` is reserved but not loadable yet; use `tenant_catalog`.

### 2d) Docs Q&A (`docs-qa` catalog bundle)

`docs-qa` is the shared transport for docs-grounded assistant panels (the
unified top-bar drawers). The contract: the CALLER supplies its own corpus
(typically its `llms.txt` text) — the bundle never guesses one, so answers are
never silently grounded on another app's docs.

Fresh installs need no manual publish: the gateway ships `docs-qa` in the
wheel and boot idempotently publishes it into the tenant catalog
(publish-if-absent by exact version; an admin's default pointer, tombstones,
and publisher attribution are never touched; publisher `system:gateway-boot`).
The publish is skipped on custom-bundle deployments whose private registry
carries no LLM-bearing flow (it would add a boot requirement they never had)
and can be disabled with `ABSTRACTGATEWAY_AUTO_PUBLISH_SHIPPED=0` — the
manual upload below then remains the path.

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" -d '{
  "registry_scope": "tenant_catalog",
  "bundle_id": "docs-qa",
  "bundle_version": "0.1.0",
  "flow_id": "docsqa001",
  "input_data": {
    "question": "How do I publish a workflow bundle?",
    "history": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}],
    "docs": "<your llms.txt text>",
    "app": "MyApp"
  }
}' "$BASE_URL/api/gateway/runs/start"
```

Then poll `GET /runs/{run_id}` (or stream the ledger); the answer is
`output.response`. `provider`/`model`/`temperature` may ride `input_data` to
override gateway defaults. Answers cite section headings and say plainly when
the docs do not answer — the bundle refuses to invent endpoints or behavior.
Docs Q&A must never route through entity chat (a visit is billable and forms
memories).

### Run-level skills selection

`input_data.skills` (a list of skill NAMES) attaches curated skills to any
run started through `/runs/start`:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" -d '{
  "bundle_id": "basic-agent",
  "input_data": {"prompt": "…", "skills": ["agora-collaboration"]}
}' "$BASE_URL/api/gateway/runs/start"
```

Trust semantics (the same abstractskill gate as `GET /skills` and the
workforce spawn lane — one gate, never a second resolver): VALIDATED skills
activate and their index lands in the run's `_runtime.skills_block`
(byte-stable for the whole run) with the `read_skill` tool made reachable;
UNVERIFIED skills are held; advisory-BLOCKED skills never ride. Every
outcome is recorded as a labeled verdict in `_runtime.skills_resolution`
(`requested`/`active`/`verdicts`/`resolved_tree_hashes`) — nothing is
silently dropped. Agent-node subruns inherit the block verbatim with
`read_skill` appended to explicit child allowlists (empty allowlists keep
registry defaults). A caller-supplied `_runtime.skills_block` is never
overwritten; the selection is then ignored with a labeled verdict.

The gateway serves its OWN corpus for the console drawer:

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/docs/corpus"
```

Returns `{app, source, chars, text}`. Resolution order: the
`ABSTRACTGATEWAY_DOCS_CORPUS` env override first (set-but-missing is an honest
404 naming the checked candidates, never a silent fallback), then the repo
`llms.txt` in dev checkouts, then the corpus packaged with the wheel.

### 3) Replay the ledger (cursor-based)

Ledger pages are replayed using `after` as “number of items already consumed”.

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/runs/<run_id>/ledger?after=0&limit=200"
```

Response shape:
- `items`: list of durable ledger records
- `next_after`: the next cursor to use

Evidence: `src/abstractgateway/routes/gateway.py` (`get_ledger`).

### 3b) Replay ledgers for multiple runs (batch)

Use `POST /api/gateway/runs/ledger/batch` to reduce request fanout when observing many runs/subflows.

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"limit":200,"runs":[{"run_id":"<run_id_1>","after":0},{"run_id":"<run_id_2>","after":0}]}' \
  "$BASE_URL/api/gateway/runs/ledger/batch"
```

Evidence: `src/abstractgateway/routes/gateway.py` (`get_ledger_batch`).

### 4) Stream ledger updates (SSE)

SSE is an optimization; clients should always be able to reconnect by replaying from the last `next_after`.

```bash
curl -N -H "$AUTH" "$BASE_URL/api/gateway/runs/<run_id>/ledger/stream?after=0"
```

Evidence: `src/abstractgateway/routes/gateway.py` (`stream_ledger`).

## Artifacts and filesystem handoff

Gateway artifacts are the cross-package representation for files, media, and
large payloads. Thin clients should pass artifact refs across runs instead of
raw bytes or local paths:

```json
{
  "$artifact": "abc123",
  "artifact_id": "abc123",
  "run_id": "session_memory_sess-1",
  "content_type": "image/png",
  "filename": "input.png"
}
```

Gateway uses three distinct file-like source terms:

- `Artifact`: a durable runtime-owned payload reference.
- `Local File`: a browser/client upload source. Hosted clients should upload
  bytes; browser-local paths are never interpreted as server paths.
- `Server File` / `Server Folder`: user-facing wording for a workspace-scoped
  server path under Gateway policy. The engineering contract is the canonical
  `WorkspacePath` string returned by `/files/*`, artifact import/export, and
  Runtime file nodes.

Hosted local uploads stay artifact-backed:

- one local file upload creates one artifact ref;
- multiple local files create an ordered list of artifact refs in Flow;
- a local folder uploads one artifact per file and may send `source_path`
  (for example `reports/2026/summary.md`) so relative member paths survive in
  artifact provenance without exposing browser-local absolute paths.

Upload a local file or folder member:

```bash
curl -sS -H "$AUTH" \
  -F "session_id=sess-1" \
  -F "source_path=reports/summary.md" \
  -F "file=@./summary.md" \
  "$BASE_URL/api/gateway/attachments/upload"
```

List run artifacts:

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/runs/<run_id>/artifacts"
```

List artifacts visible to a session:

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/sessions/sess-1/artifacts"
```

Browse server workspace files/folders:

```bash
curl -sS -H "$AUTH" \
  "$BASE_URL/api/gateway/files/list?path=&include_directories=true&limit=200"
```

Optional filters:
- `path`: browse a specific workspace folder or mount alias.
- `recursive=true`
- `family=image|video|audio|document|text|code|json|archive|other`
- `extensions=png,jpg` or newline-separated values
- `query=substring`
- `max_depth=<n>`

Search artifacts across Gateway storage:

```bash
curl -sS -H "$AUTH" \
  "$BASE_URL/api/gateway/artifacts/search?scope=all&artifact_kind=image&query=logo&tags=pin_id=image&include_stats=true&limit=500"
```

`scope` can be `all`, `session`, or `run`. Use `session_id` with
`scope=session` and `run_id` with `scope=run`; omit both for `scope=all`.
Search responses carry the row fields and also include
`artifact_envelope_v1`, a normalized projection of Runtime-owned descriptors,
access stats, and Gateway action links.

Useful query parameters:
- `artifact_kind`: UI-oriented kind filter. Comma-separated values match
  `semantic_kind`, `render_kind`, or `modality`; generic `audio` means
  unclassified audio and does not match canonical `voice`, `music`, or `sound`.
  Single canonical kinds such as `music`, `voice`, `image`, `markdown`, or
  `json` map to Runtime catalog filters. Multi-kind unions are supported, but
  may be Gateway post-filters until Runtime exposes OR filters.
- `semantic_kind` / `render_kind`: canonical descriptor filters when the caller
  wants the two dimensions separately.
- `modality`, `content_type`, `workflow_id`, `node_id`, `created_after`,
  `created_before`, and `tags`: server filters for indexed descriptor fields.
- `query`: case-insensitive metadata search. Gateway may post-filter this field
  when Runtime cannot index it directly.
- `include_stats=true`: include exact `stats.total`, byte totals, and facet
  counts for the selected server-side filter set, independent of `limit`.
- `limit`, `offset`, and `cursor`: bounded paging. The default Runtime Explorer
  page size is 500; `limit<=0` is bounded unless `debug_unlimited=true` is used
  by an admin/debug caller.

`artifact_envelope_v1` contains normalized fields such as `semantic_kind`,
`render_kind`, `workflow_id`, `node_id`, `turn_id`, `ledger_cursor`,
`generation`, `producer`, `media`, `source_refs`, `access`, and `links`.
Sparse producer metadata is represented as missing fields; Gateway does not
invent provider/model provenance from filenames.

Generated-media artifacts created by child runs and projected into the parent
run preserve Runtime descriptors and structured metadata. Direct transcription
routes store transcript artifacts with source-audio refs, language/prompt hints,
provider/model when available, and bounded route parameters.
Descriptor-provided action links are sanitized to relative Gateway/UI links
before they appear in envelopes; raw external provider URLs should be represented
as trace availability or Gateway-owned trace records.

Content reads can label the access type for Runtime access stats:

```bash
curl -sS -H "$AUTH" \
  "$BASE_URL/api/gateway/runs/<run_id>/artifacts/<artifact_id>/content?access_action=preview"
```

Supported access actions are `content`, `preview`, and `download`. The shorter
`access=preview` alias is also accepted.

Import a server workspace path into a session artifact:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"session_id":"sess-1","source":{"kind":"workspace_path","path":"inputs/photo.png"},"pin_id":"image"}' \
  "$BASE_URL/api/gateway/artifacts/import"
```

Export an artifact back into the server workspace:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"path":"outputs/photo.png","create_parent_dirs":true,"overwrite":false}' \
  "$BASE_URL/api/gateway/runs/<run_id>/artifacts/<artifact_id>/export"
```

Import and export use the same Gateway workspace policy as file helpers:
workspace roots, mounted roots, ignored paths, and size limits are enforced on
the server. Browser-local files should be uploaded through
`POST /api/gateway/attachments/upload`; browser-local file paths are not
interpreted as Gateway workspace paths. In hosted user-auth mode, server
workspace import/export and `/files/*` helpers require an admin principal.
Ordinary users can still upload browser-local files and list/search artifacts in
their own routed runtime.

Canonical Gateway server paths use `rel/path` for the main workspace root and
`mount_alias/rel/path` for approved mounts. When two allowed mounts share the
same basename, Gateway emits deterministic digest-suffixed aliases so the same
public path string can round-trip through `/files/*`, artifact import/export,
and Runtime file nodes.

## Durable commands (`POST /api/gateway/commands`)

Commands are appended to a durable inbox and applied asynchronously by the runner.

Request fields (see `SubmitCommandRequest` in `src/abstractgateway/routes/gateway.py`):
- `command_id`: client-supplied idempotency key (UUID recommended)
- `run_id`: target run id (or session id for some event use-cases)
- `type`: `pause|resume|cancel|emit_event|update_schedule|compact_memory`
- `payload`: command-specific object

### Pause / cancel

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"command_id":"'"$(python -c 'import uuid; print(uuid.uuid4())')"'", "run_id":"<run_id>", "type":"pause", "payload":{"reason":"operator_pause"}}' \
  "$BASE_URL/api/gateway/commands"
```

### Resume a paused run

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"command_id":"'"$(python -c 'import uuid; print(uuid.uuid4())')"'", "run_id":"<run_id>", "type":"resume", "payload":{}}' \
  "$BASE_URL/api/gateway/commands"
```

### Resume a WAITING run with a payload (WAIT resume)

When `payload.payload` is present, the runner interprets this as “resume a WAITING run with a durable payload”:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"command_id":"'"$(python -c 'import uuid; print(uuid.uuid4())')"'", "run_id":"<run_id>", "type":"resume", "payload":{"wait_key":"<optional_wait_key>", "payload":{"approved":true}}}' \
  "$BASE_URL/api/gateway/commands"
```

Evidence: `src/abstractgateway/runner.py` (`_apply_command`, `_apply_run_control`).

### Emit an external event

Minimal form:

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"command_id":"'"$(python -c 'import uuid; print(uuid.uuid4())')"'", "run_id":"<session_id>", "type":"emit_event", "payload":{"name":"chat.message","payload":{"text":"hi"}}}' \
  "$BASE_URL/api/gateway/commands"
```

Evidence: `src/abstractgateway/runner.py` (`_apply_emit_event`).

## Beyond the core

`/api/gateway/*` also includes optional operator/tooling endpoints (reports inbox, triage queue, backlog browsing + exec runner, process manager, file/attachment helpers, embeddings, voice, discovery, …).
See: [maintenance.md](docs/maintenance.md).

## Discovery endpoints (optional)

These exist to help thin clients adapt to the deployed gateway.

- Capabilities (best-effort): `GET /api/gateway/discovery/capabilities`
- Providers/models discovery (best-effort): `GET /api/gateway/discovery/providers`, `GET /api/gateway/discovery/providers/{provider}/models`
- Tools (thin-client allowlist help): `GET /api/gateway/discovery/tools`
- Skills inventory: `GET /api/gateway/skills` — the abstractskill shelf with
  trust verdicts (roster rows `{name, description, trust_level, blocked,
  requires_review, tree_hash, source, has_scripts, reasons}`); degradations
  are labeled `warnings`, never a fabricated list. Shelf resolution:
  `ABSTRACTGATEWAY_SKILLS_SHELF`, else the triage repo's
  `abstractskill/registry`.
- MCP server inventory: `GET /api/gateway/mcp/servers` — the declared
  registry at `<data_dir>/config/mcp_servers.json`
  (`{"version": 1, "servers": [{"name", "url"?, "description"?,
  "auth_required"?, "tags"?}]}`), served with declared fields only and
  `probed: false` (connect state/tool counts require a probe lane and are
  never faked).
- Dynamic capability catalogs: `GET /api/gateway/voice/voices`, `GET /api/gateway/audio/speech/models`, `GET /api/gateway/audio/transcriptions/models`, `GET /api/gateway/audio/music/providers`, `GET /api/gateway/audio/music/models`, `GET /api/gateway/vision/provider_models`

The capabilities payload includes package presence (`abstractruntime`,
`abstractcore`, `abstractmemory`, `abstractvoice`, `abstractvision`), existing
gateway helpers (`tools`, `visualflow`, `media`), memory-store readiness, and
AbstractCore capability plugin status for `voice`, `audio`, `vision`, and
`music`.

The route paths and contract descriptors are the stable part of this surface.
Catalog routes also include a stable Gateway-owned envelope:

- `catalog.contract = gateway_catalog_v1`
- `catalog.version = 1`
- `items = [...]`

The lower-layer fields stay in the payload for compatibility. Thin clients
should read `catalog` plus `items`; the route-specific fields (`models`,
`provider_models`, `profiles`, `voices`) remain available.

Provider discovery also reports the resolved default provider/model when one is
configured. The resolver follows request values, flow pins, and the execution-host
`input.text` capability route; if no pair exists, the response includes
`default_error` rather than a hardcoded local model.

It also includes a versioned thin-client contract:

- `capabilities.contracts.version`: currently `1`
- `capabilities.contracts.common`: shared run start/list/summary/input/history,
  ledger, artifact, attachment, workspace, discovery, provider prompt-cache
  controls, and the host-visibility descriptors `model_residency` (including
  `row_schema = "model_residency_row_v1"` and the canonical `modality_ui`
  color map), `host_state`, and `session_caches`
  (see [Host state and model residency](#host-state-and-model-residency)).
  `common.artifacts` includes run listing/content, session artifact
  listing, artifact search with `artifact_envelope_v1`, exact stats/facets,
  `artifact_kind` UI filtering, workspace import, and workspace export
  descriptors when available. Permission-sensitive descriptors are principal-aware:
  ordinary users see admin-only workspace import/export and provider
  prompt-cache controls marked unavailable with `admin_required` metadata.
- `capabilities.contracts.common.readiness`: compact Gateway-owned
  `gateway_surface_readiness_v1` summary derived from the shared endpoint/media/
  residency descriptors
- `capabilities.contracts.flow_editor`: the AbstractFlow editor/runtime surface
- `capabilities.contracts.assistant`: assistant-facing voice/audio/media/cache
  feature gates
- `capabilities.contracts.abstractcode`: code-client run/history/workspace/cache
  feature gates

Contract booleans are intentionally conservative. Package `installed=true` is
not the same thing as endpoint `available=true`; clients should branch on the
versioned contract fields when enabling controls.

`common.readiness` is intentionally narrower than provider/backend health. It
summarizes Gateway surface availability from existing descriptors, but it does
not invent selected backend/provider/model truth or stable degraded-state
reason codes.

Evidence: `src/abstractgateway/routes/gateway.py` (`discovery_capabilities`, `discovery_providers`).

## AbstractFlow gateway-first editor contract

The browser editor can use AbstractGateway as its runtime and storage host.

Draft VisualFlow records:

- `GET /api/gateway/visualflows`
- `POST /api/gateway/visualflows`
- `GET /api/gateway/visualflows/{flow_id}`
- `PUT /api/gateway/visualflows/{flow_id}`
- `DELETE /api/gateway/visualflows/{flow_id}`
- `POST /api/gateway/visualflows/{flow_id}/publish`

Bundle inspection and editor run-schema helpers:

- `GET /api/gateway/bundles`
- `GET /api/gateway/bundles/{bundle_id}`
- `GET /api/gateway/bundles/{bundle_id}/flows/{flow_id}`
- `GET /api/gateway/bundles/{bundle_id}/flows/{flow_id}/input_schema`

The input-schema endpoint returns a versioned payload with:

- `version`
- `bundle_id`, `bundle_version`, `bundle_ref`, `flow_id`, `workflow_id`
- `inputs`: entrypoint input pins derived from the `on_flow_start` node
- `defaults`: pin defaults from VisualFlow JSON
- `input_data_schema`: a small JSON Schema object for the Run Flow modal

Example:

```bash
curl -sS -H "$AUTH" \
  "$BASE_URL/api/gateway/bundles/my-bundle/flows/ac-echo/input_schema"
```

### Native-loop bundles (react / codeact / memact)

Some shipped bundles declare `metadata.native_loop_factory` instead of VisualFlow
JSON (`manifest.flows` is empty). The gateway materializes an abstractagent
loop at load time. Discovery uses the same bundle list endpoint — **not**
`/discovery/workflows`.

Thin clients should:

1. `GET /api/gateway/bundles` (authenticated).
2. Filter entrypoints whose `interfaces` includes `abstractcode.agent.v1`.
3. Read `metadata.native_loop_factory` (`react`, `codeact`, or `memact`) to
   distinguish native loops from VisualFlow agent bundles.
4. Use each entrypoint's `workflow_id` (for example `react-agent@0.1.0:react`).
5. Start runs with `POST /api/gateway/runs/start` and
   `bundle_id` / `flow_id` from the bundle listing (for example
   `react-agent` + `react`).

Native-loop entrypoints do not ship VisualFlow JSON. The gateway serves a
versioned input-schema stub (`prompt` required; `provider` and `model`
optional) from
`GET /api/gateway/bundles/{bundle_id}/flows/{flow_id}/input_schema`.
Headless clients may also pass those fields without fetching the schema.

The shipped `react-agent@0.1.0` bundle is built by
`scripts/build_react_agent_bundle.py` and force-included in the wheel. A running
gateway process must restart (or call bundle reload) after the file lands on
disk before `/bundles` lists it.

### Run history bundle (`GET /runs/{run_id}/history_bundle`)

Thin clients should prefer this endpoint over stitching ledger, session, and
artifact endpoints. The export is owned by AbstractRuntime; the gateway forwards
query parameters and returns the bundle JSON unchanged (including in-band
degradations).

Query parameters:

| Parameter | Default | Notes |
|-----------|---------|-------|
| `include_subruns` | `true` | Descendant runs in the bundle tree |
| `include_session` | `false` | Root session turn list |
| `session_turn_limit` | `200` | Cap when `include_session=true` |
| `ledger_mode` | `tail` | `tail` or `full` |
| `ledger_max_items` | `2000` | Per-run ledger cap when `ledger_mode=tail` |
| `detail` | `full` | `full` (complete payloads) or `replay` (transcript-fold projection) |

**`detail=replay`** drops request-side payloads and observability paths the
transcript fold never reads; runtime marks each omission with `$omitted` inside
ledger records. Use it for session replay and thin-client folds — it is much
smaller than `full` (gzip helps further; send `Accept-Encoding: gzip`).

**`warnings`** (always present, may be empty): typed degradations the export
survived instead of failing silently. Each entry is an object with at least
`code` and `detail`; many include `run_id`. Known codes today:

| Code | Meaning |
|------|---------|
| `subtree_discovery_failed` | Could not list child runs; bundle covers root only |
| `subtree_truncated` | Descendant discovery hit the run cap |
| `ledger_read_failed` | Ledger for a run id could not be read |
| `torn_rows_skipped` | Corrupt/unparseable ledger lines skipped |
| `ledger_tail_window` | Ledger truncated to `ledger_max_items` (tail mode) |
| `input_data_offload_failed` | Input-data artifact reference could not be resolved |

Clients must surface non-empty `warnings` to the operator — a bundle that
"looks complete" but carries warnings may be missing subruns, ledger tail, or
offloaded input data.

### Session history bloc (`GET /sessions/{session_id}/history/bloc`)

Returns one cursor-bounded bloc of **root session turns**, each with an inline
`history_bundle` export — one round-trip instead of N per-turn bundle fetches
(laurent c5551). Resume pagination uses an ISO `created_at` cursor in the
`before` query parameter (never turn-count offsets).

Query parameters:

| Parameter | Default | Notes |
|-----------|---------|-------|
| `before` | *(omit)* | ISO-8601 cursor; only turns strictly **before** this timestamp |
| `limit` | `5` | Max turns in this bloc (1–50) |
| `detail` | `replay` | Forwarded to each turn's bundle export (`full` \| `replay`) |
| `include_subruns` | `true` | Per-turn bundle tree |
| `ledger_mode` | `tail` | `tail` or `full` |
| `ledger_max_items` | `2000` | Per-turn ledger cap when `ledger_mode=tail` |
| `include_drafts` | `false` | Include draft-test root runs |

Response fields: `session_id`, `cursor_before` (echo of `before`), `cursor_after`
(oldest turn returned — pass as the next `before`), `older_remaining`, `warnings`,
and `turns[]` (`run_id`, `created_at`, `status`, `bundle` or `error`).

The editor observes runs with the core lifecycle endpoints above:
`/runs/start`, `/runs/{run_id}`, `/runs/{run_id}/ledger`,
`/runs/{run_id}/ledger/stream`, `/runs/ledger/batch`,
`/runs/{run_id}/input_data`, `/runs/{run_id}/history_bundle`, and
`/runs/{run_id}/artifacts`.

## Optional multimodal scope

Current direct Gateway endpoints:
- `POST /api/gateway/runs/{run_id}/voice/tts`
- `POST /api/gateway/runs/{run_id}/voice/tts/stream`
- `POST /api/gateway/runs/{run_id}/audio/transcribe`
- `POST /api/gateway/runs/{run_id}/images/generate`
- `POST /api/gateway/runs/{run_id}/images/edit`
- `POST /api/gateway/runs/{run_id}/images/upscale`
- `POST /api/gateway/runs/{run_id}/videos/generate`
- `POST /api/gateway/runs/{run_id}/videos/from_image`
- `POST /api/gateway/runs/{run_id}/music/generate`
- `GET /api/gateway/voice/voices`
- `GET /api/gateway/audio/speech/models`
- `GET /api/gateway/audio/transcriptions/models`
- `GET /api/gateway/audio/music/providers`
- `GET /api/gateway/audio/music/models`
- `GET /api/gateway/vision/provider_models`
- `GET /api/gateway/vision/adapters`

`/voice/tts` returns a durable audio artifact after synthesis. `/voice/tts/stream`
returns JSON Lines stream events for progressive playback when discovery advertises
`capabilities.contracts.assistant.voice.tts.streaming=true`; successful streams still
finish with a Runtime-owned child-run audio artifact.

The catalog endpoints proxy AbstractCore Server routes when
`ABSTRACTCORE_SERVER_BASE_URL`
is configured. Gateway uses explicit Core auth settings for that hop and never
reuses the Gateway bearer token as a Core/provider secret. Without a configured
Core server, the voice/model routes return bounded static descriptors from
Gateway and capability-package environment variables.

Each route adds:

- `catalog`: Gateway-owned route metadata (`contract`, `version`, `kind`,
  `scope`, `route_source`, optional `upstream_source`, and route filters)
- `items`: one canonical primary array for thin clients

Examples:

- `/voice/voices`: `items` contain voice/profile records with `id`, `label`,
  optional `provider`, optional `model`, and `voice_kind`
- `/audio/*/models`: `items` contain model records with `id`, `label`,
  optional `provider`, optional `tasks`, and optional `parameters`
- `/audio/music/providers` and `/discovery/providers`: `items` contain provider
  records with `id`, `label`, and `provider`

Generated images are available through Runtime workflows when a compatible
image backend is installed and configured. Gateway also exposes a direct image
generation endpoint that uses the Runtime/Core output-selector contract rather
than a provider-specific image client. The route creates a durable child run,
stores the generated image as a run artifact, and returns
`event_name="abstract.progress"` so thin clients can stream the child-run ledger
for progress:

- `run_id`, `request_id`, `prompt`
- optional `provider`, `model`, `size`, `width`, `height`, `format`, batch
  `count` / `n`, `seeds`, and ordered `lora_adapters`
- `image_artifact`: first generated image for compatibility
- `image_artifacts`: full ordered image artifact list for batch generation

`size`, `width`, and `height` are optional passthrough request overrides. Do
not inject a client-side default size. Different image providers/models accept
different size sets; when the client leaves dimensions unset, Runtime/Core lets
the configured backend use its default or `auto` behavior.

If the active workflow runtime already has an AbstractCore LLM client, the route
uses it. For tools-only workflows, the route can create a direct Runtime/Core
client from request `provider`/`model` or the execution-host capability route
default. Unsupported or unconfigured deployments return a structured `ok=false`
response instead of a failed run.

Gateway also exposes a direct image-edit sibling route:

- `POST /api/gateway/runs/{run_id}/images/edit`

The request uses a source `image_artifact`, optional `mask_artifact`, the same
provider/model and image backend selectors as image generation, plus optional
batch `count` / `n`, `seeds`, and ordered `lora_adapters`, and returns an
artifact-backed edited image. Batch responses also return `image_artifacts`.
Thin clients should feature-detect it from
`capabilities.contracts.flow_editor.media.edited_image` or
`capabilities.contracts.assistant.media.edited_image`. It uses the same
child-run `abstract.progress` progress contract as direct image generation.

Gateway also exposes a direct image-upscale sibling route:

- `POST /api/gateway/runs/{run_id}/images/upscale`

The request uses a run-visible source `image_artifact`, optional provider/model
selectors, and optional upscaler controls such as `scale`, `resolution`,
`softness`, `seed`, `quantize`, and `vae_tiling`; `resolution` may be a
shortest-edge integer or a scale factor such as `2x`. Thin clients should
feature-detect it from `capabilities.contracts.flow_editor.media.upscaled_image`
or `capabilities.contracts.assistant.media.upscaled_image`, list models with
`GET /api/gateway/vision/provider_models?task=image_upscale`, and stream the
returned child-run ledger for `abstract.progress` events.

Generated music follows the same direct child-run pattern. Thin clients should
discover it from `capabilities.contracts.flow_editor.media.generated_music` or
`capabilities.contracts.assistant.media.generated_music`, list providers/models
from the music catalog routes, and treat the returned `child_run_id` plus
`music_artifact` as the durable output handle.

Generated video also follows the direct child-run pattern:

- `POST /api/gateway/runs/{run_id}/videos/generate` uses the Runtime/Core
  `output.modality=video` / `task=text_to_video` contract and accepts optional
  batch `count` / `n`, `seeds`, ordered `lora_adapters`, and `flow_shift`.
- `POST /api/gateway/runs/{run_id}/videos/from_image` accepts a run-visible
  source `image_artifact`, accepts the same optional batch/adapter/video
  control fields, and uses `task=image_to_video`.
- Thin clients should discover these routes from
  `capabilities.contracts.flow_editor.media.generated_video` and
  `capabilities.contracts.flow_editor.media.image_to_video` (or the matching
  `assistant.media.*` entries), use
  `GET /api/gateway/vision/provider_models?task=text_to_video|image_to_video`
  for model catalogs, use `GET /api/gateway/vision/adapters` for compatible
  installed adapter catalogs, stream the returned `child_run_id` ledger for
  `abstract.progress` events, and read `video_artifacts` when batch generation
  is requested.

STT and listen contract notes:

- `POST /api/gateway/runs/{run_id}/audio/transcribe` accepts a run-visible
  `audio_artifact` plus optional `language`, `prompt`, `response_format`,
  `temperature`, `format`, `provider`, and `model` hints.
- `capabilities.contracts.flow_editor.voice.stt` and
  `capabilities.contracts.assistant.voice.stt` point to that upload route.
- `capabilities.contracts.flow_editor.voice.listen` and
  `capabilities.contracts.assistant.voice.listen` are host-capture contracts,
  not a live microphone socket. They tell higher apps to capture locally and
  emit an event or upload the resulting audio artifact.

## KG memory

`POST /api/gateway/kg/query` queries the configured AbstractMemory TripleStore.
Gateway resolves the store through:

- `ABSTRACTGATEWAY_MEMORY_STORE_BACKEND=lancedb|memory` (`sqlite` when the installed AbstractMemory build exposes `SQLiteTripleStore`)
- `ABSTRACTGATEWAY_MEMORY_STORE_PATH`
- `ABSTRACTGATEWAY_MEMORY_REQUIRE_VECTOR`

Structured queries work with LanceDB and in-memory stores. SQLite also works
when the installed AbstractMemory build exposes `SQLiteTripleStore`. Semantic
`query_text` requires a vector-capable backend plus the execution-host
`embedding.text` route; SQLite returns a clear 400 instead of pretending to
support semantic recall.

Capability discovery reports KG memory as available when AbstractMemory is
installed and the configured backend can be resolved. A fresh persistent store
does not need to exist yet; empty-store structured queries return an empty
result rather than making Flow authoring nodes unavailable.

## Models and engines

The gateway serves AbstractCore's models and engines payloads unchanged,
under `/api/gateway`. The bodies and payloads
are the same as AbstractCore's own `/acore/*` routes; `abstractcore` and
`abstractgateway` render them with the same screens.

| Method and path | Access | Body / query | Returns |
|---|---|---|---|
| `GET /host/profile` | user | `refresh=1` | `host_profile_v1` |
| `GET /engines` | user | `probe=1` | `gateway_engines_v2` rows (AbstractCore's detection plus the install plan and actions) with `install_allowed` and `install_policy`; see [engines.md](docs/engines.md) |
| `GET /engines/{id}` | user | `probe=1` | one engine row plus `install_allowed`; 404 for an unknown id |
| `POST /engines/{id}/install` | admin | `{"dry_run": bool, "force": bool, "location": "auto"\|"user"\|"system"}` | an `engine_install_job_v1` job (user-level first; pauses in `needs_admin` / `needs_tools`), see [engines.md](docs/engines.md) |
| `GET /engines/jobs`, `GET /engines/jobs/{id}` | user | | engine install jobs |
| `POST /engines/jobs/{id}/continue`, `/cancel` | admin | `{"action"?}` | the job |
| `POST /engines/{id}/start`, `/stop` | admin | | Ollama / LM Studio server state |
| `GET /models/catalog` | user | `q`, `engine`, `fits=1`, `hub=1`, `tag` (repeatable) | `model_catalog_v1` |
| `GET /models/installed` | user | `provider` | `models_installed_v1` |
| `POST /models/download` | admin | `{"provider", "artifact", "dry_run", "expected_bytes"?}` or `{"recommended": true}` | `{"ok": true, "job": {...}}`; with `recommended`, `{"ok": true, "recommended": true, "jobs": [...], "group": {...}}` |
| `GET /models/download/{job}` | user | | `{"ok": true, "job": {...}}` (a `grp_...` id returns the parent job) |
| `GET /models/downloads` | user | | `{"ok": true, "jobs": [...]}`, newest first, parents included |
| `POST /models/download/{job}/cancel` | admin | none | `{"ok": true, "job": {...}}`; stops the transfer within about a second; a `grp_...` id cancels every running child; 404 when unknown |
| `GET /models/downloads/stream` | user | `job_id`, `until_idle=1` | Server-Sent Events of the same dicts, see [model-downloads.md](docs/model-downloads.md) |
| `POST /models/delete` | admin | `{"provider", "artifact", "dry_run": bool, "force": bool}` | `host_job_v1` (kind `delete`) |
| `GET /jobs` | user | `kind`, `status` | `{"schema": "host_jobs_v1", "jobs": [...], "generated_at"}`, newest first |
| `GET /jobs/{id}` | user | | `host_job_v1`; 404 when unknown |
| `POST /jobs/{id}/cancel` | admin | none (an empty `{}` is accepted) | `host_job_v1`; 404 when unknown |

Empty query values (`q=`, `engine=`) mean "no filter". `probe`, `fits` and
`hub` accept `1`/`0` and `true`/`false`.

**Catalog artifacts.** `model_catalog_v1` is AbstractCore's payload, served
unchanged (field reference: AbstractCore `docs/models.md`, "The catalog").
Besides `quant` (the artifact's own label, lowercased, or `null`) and `bits`
(effective bits per weight), every artifact carries `quant_class`, one of
`2bit`, `3bit`, `4bit`, `5bit`, `6bit`, `8bit`, `16bit`, `full`, `unknown`,
for filtering by quantization: `q4_k_m`, `4bit`, `mxfp4` and `oq4e` are
`4bit`; `q8_0` and `8bit` are `8bit`; `bf16` and `f16` are `16bit`; `f32` is
`full`. `quant_class_source` is `stated` (the reference names its quant),
`assumed` (a bare Ollama tag such as `qwen3.5:9b` or LM Studio id: the class of
the engine's default build, which the fit estimate assumes too) or `null` (no
quant information; the class is `unknown`). `options` holds the route options a
recommendation copies with the artifact (`{}` for most); `companions` lists
repos downloaded with it (an MLX build's MTP drafter, from AbstractCore's
drafter registry; `[]` for most), `companion_bytes` is their size, and
`download_bytes` already includes it; `note` is one sentence about the build. On Apple silicon the text rows pre-select the
memory tier's MLX build and exactly one text row is the `starter`.

**Jobs.** A `host_job_v1` has `schema`, `job_id`, `kind`
(`download | delete | engine_install`), `status`
(`queued | running | completed | failed | cancelled`), `provider`, `artifact`,
`engine`, `percent`, `downloaded_bytes`, `total_bytes`, `message`, `log_tail`,
`command` (the exact argv), `dry_run`, `started_at`, `finished_at`, `error`
(a string or `null`), `joined`, `result` and `cli_equivalent`, which names the
`abstractgateway` command that does the same thing. A dry run finishes before
the POST returns. On the `/models/download` routes the job also carries
`job` (the id), `events`, `host_status`, reports `queued` as `running`, and
counts `joined` including the first request.

**Download progress.** A download job also carries `state`
(`queued | resolving | downloading | verifying | installing | done | failed |
cancelled | stalled`), `bytes_done`, `bytes_total`, `size_unknown`,
`size_note`, `bytes_per_second`, `eta_s`, `updated_at`, `files`
(`[{name, bytes_done, bytes_total, state}]`), `current_file`, a one-sentence
`message`, the tool's own `detail`, and `transitions`. "Use recommended
defaults" (`{"recommended": true}`) returns one parent job (`kind:
"download_group"`, id `grp_...`) whose bytes, percent, speed and time left
add up its children. The full contract, one real example per state and what
each source reports: [model-downloads.md](docs/model-downloads.md).

**Refusals** share one body:
`{"ok": false, "status", "reason"?, "message", "detail", "error": {"message", "type"}, ...}`.

| Status | When |
|---|---|
| 400 `invalid` | provider or artifact missing |
| 403 `refused` / `not_allowed` | a real engine install while `allow_engine_install` is off ([configuration.md](docs/configuration.md#allow_engine_install)); `install_policy` says why |
| 403 | the caller is not an admin (every POST above) |
| 404 `not_found` | unknown job id, engine id, or a model that is not installed |
| 409 `busy` | an engine install is already running (`job` is the running one) |
| 409 `refused` | the engine is not supported here or has no install command (`install` is the plan), or a delete is blocked (`delete_blockers`: `loaded`, `shared_cache:…`, `unknown_location`, `engine_not_running`, `remote_engine`; `force: true` overrides the first two) |
| 501 `unsupported` / `abstractcore_too_old` | the installed AbstractCore is too old for these routes; `required`, `installed` and `missing` name what to upgrade |
| 503 `unavailable` | AbstractCore is not installed |

Example:

```bash
curl -s -H "Authorization: Bearer $TOKEN" "$GW/api/gateway/models/catalog?q=qwen3&fits=1" | jq '.rows[0].artifacts[0].fit'
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"dry_run": true}' "$GW/api/gateway/engines/ollama/install" | jq '.command, .cli_equivalent'
```

## Host state and model residency

Gateway exposes a host-level view of the execution machine — memory, GPU,
resident models, and session prompt caches — so consoles and agents can render
an "agentic OS" panel from one API surface.

Read endpoints (any authenticated principal):

- `GET /api/gateway/host/state` — one-call host snapshot
- `GET /api/gateway/host/metrics/memory` — host memory snapshot
- `GET /api/gateway/host/metrics/gpu` — GPU utilization probe
- `GET /api/gateway/models/loaded` — model residency listing
- `GET /api/gateway/models/context_estimate` — context/KV memory estimate for
  a provider+model
- `GET /api/gateway/sessions/prompt_cache` — session prompt-cache enumeration

Mutation endpoints (admin principal required):

- `POST /api/gateway/models/load` — load (and by default pin) a model runtime
- `POST /api/gateway/models/unload` — unload a model runtime
- `POST /api/gateway/models/lock` — lock a resident model against unload
- `POST /api/gateway/models/unlock` — release a model-residency lock
- `POST /api/gateway/models/download` — fetch model weights onto the host
- `POST /api/gateway/sessions/{session_id}/prompt_cache/clear_all` — clear
  every runtime-minted prompt cache for a session

Reads are visibility every authenticated client needs; mutations spend shared
host resources and stay operator acts. Anonymous requests are rejected on all
of these routes, like every other `/api/gateway/*` path.

### `GET /host/state`

One snapshot with `memory`, `gpu`, `models`, and `session_caches` sections:

```json
{
  "ok": true,
  "ts": 1787857000.0,
  "memory": {"ram": {"...": "..."}, "process": {"rss_bytes": 140443648}, "device": {"backend": "metal", "allocated_bytes": 0, "...": "..."}},
  "gpu": {"supported": true, "source": "ioreg", "gpus": [{"name": "...", "utilization_gpu_pct": 0.0}]},
  "models": [{"runtime_id": "...", "provider": "...", "model": "...", "resident": true, "...": "..."}],
  "session_caches": [],
  "totals": {"models": 2, "models_resident": 1, "model_bytes": 3109915433, "session_caches": 0, "session_cache_bytes": null},
  "degraded": [],
  "row_schema": "model_residency_row_v1"
}
```

- `models` rows use the frozen `model_residency_row_v1` schema described
  below; `session_caches` relays the runtime facade's cache rows verbatim.
- Every section is independently best-effort and the route never returns a
  500. A missing facade method or a failed probe nulls that section and names
  it in `degraded`; a `reasons` map (present only when non-empty) says why.
- The `gpu` section keeps its in-band `{"supported": false, "reason": "..."}`
  payload when the probe answers but reports no support; it still counts as
  degraded.
- `totals.model_bytes` sums the known `size_bytes` values and is `null` when
  no row reports a size; `totals.session_cache_bytes` behaves the same over
  the cache rows' `bytes`.
- `totals.models` counts every known row — configured / cached rows included —
  while `totals.models_resident` (additive) counts only rows with
  `resident: true`. Clients that display "N loaded" must read
  `models_resident`: default ≠ loaded, and presenting configured capability
  defaults as loaded is exactly the lie this field removes.
- When the runtime memory snapshot reports a host identity, the response also
  carries a top-level `host` object (the identity facts of the machine the
  snapshot describes). The block is omitted when the runtime does not report
  one. Together with the per-row `host_id`/`host_name` fields below, this is
  the seam a multi-machine resource pool would aggregate on; one gateway
  binds one runtime host, and the pool design is proposed in
  [backlog 0093](https://github.com/lpalbou/abstractgateway/blob/main/docs/backlog/proposed/0093_multi_machine_model_resource_pool.md).

### `GET /host/metrics/memory`

Returns `{"ok": true, "supported": true, ...}` plus the snapshot sections:
`ram` (total/available/used bytes and percent), `process` (`rss_bytes`), and
`device` (`backend`, `allocated_bytes`, `total_bytes`, `free_bytes`). When the
runtime host facade does not expose a memory snapshot, the route answers 200
with `{"ok": true, "supported": false, "reason": "..."}` — the same degraded
style as `GET /host/metrics/gpu`.

How to compare memory measurements: `process.rss_bytes` and
`device.allocated_bytes` are different axes. In-process device backends (for
example MLX on Metal) return freed buffers to the process heap and the
operating system may retain those pages, so process RSS does not shrink when a
model unloads. Use `device.allocated_bytes` to verify that an unload freed
device memory; use `ram` and `process` for overall host pressure.

### Model residency (`/models/loaded`, `/models/load`, `/models/unload`)

`GET /models/loaded` lists the model runtimes the host knows about, with
optional `task`, `provider`, `model`, and `base_url` query filters. The
response keeps the raw runtime records in `models` and adds a normalized
`rows` array in the frozen `model_residency_row_v1` schema (named by
`row_schema`), so thin clients do not need per-provider alias tables.

Each `model_residency_row_v1` row has exactly these fields (unknown values
are `null`, never guessed):

`runtime_id`, `task`, `provider`, `model`, `source`, `resident`, `state`,
`pinned`, `default`, `size_bytes`, `size_vram_bytes`, `expires_at`,
`context_length`, `loaded_at`, `last_used_at`, `locked`, `lockable`,
`modalities`, `calibrated_context_length`, `context_calibrated`, `host_id`,
`host_name`, `details`

The schema is additive-tolerant and keeps the `model_residency_row_v1` name
as optional fields are added; treat fields beyond the original 16 as
optional. The lock/calibration/host fields mean:

- `locked` / `lockable` — tri-state booleans: whether the model is locked
  against unload, and whether this runtime supports locking it at all.
- `modalities` — list of modality strings when the runtime reports one
  (`null` otherwise, including when the value is not a clean string list).
- `calibrated_context_length` / `context_calibrated` — the measured usable
  context length and whether it came from calibration rather than metadata.
- `host_id` / `host_name` — identity of the machine serving the model,
  stamped by the runtime that reported the row (see the `host` block note
  under `GET /host/state` above).

Residency truth is provider-first: `provider_resident` / `provider_loaded`
booleans in the source record outrank the runtime-lease booleans `resident` /
`loaded`, because a runtime can hold a lease on a model the provider has
already evicted. A loaded-looking `state` string (`provider_loaded`, `loaded`,
`resident`) can confirm residency, but a state string is never proof of
absence — with no boolean present and no loaded-like state, `resident` stays
`null`. `details` preserves the raw record for fields outside the schema.

`POST /models/load` accepts `task` (default `text_generation`), `provider`,
`model`, optional provider `options`, `pin` (default `true`), `base_url`,
`timeout_s`, and `lock` (default `false`) — with `lock: true` a successful
load is immediately locked against unload, and the lock outcome is reported
additively under `lock` in the response (a lock failure or a runtime without
lock support never turns the successful load into a failure). `POST
/models/unload` selects the runtime by `runtime_id` or by
`task`/`provider`/`model`. Both relay Runtime's host facade and return the
normalized residency response (`operation`, affected records, and in-band
`ok=false` errors instead of opaque failures).

One unload failure gets a real status code: when the target model is locked,
`POST /models/unload` answers **HTTP 409** with the normalized refusal payload
as the body (`ok: false`, `error: "model_locked"`, plus whatever detail the
runtime included), so clients can offer force-unload or point at
`/models/unlock`. Sending `"force": true` in the unload request unloads the
model despite the lock. Every other unload outcome stays in-band at 200.

### Model locks and context estimates

- `POST /models/lock` and `POST /models/unlock` (admin) pin a resident model
  against unload and release that pin. The body selects the target like
  unload does: `runtime_id`, or `provider` + `model`, with optional
  `base_url` and `timeout_s`. Lock requires provider-verified residency: a
  configured or merely-warm model refuses with an
  `error: "model_not_resident"` payload (load it with `lock: true` instead);
  unlock always works, even for a since-evicted model, so locks are never
  stranded. Rows report `lockable` so clients know whether
  a lock can work, and `locked` so they can render the current state.
- `GET /models/context_estimate?provider=&model=&context_length=` (any
  authenticated principal) relays the Runtime host facade's context/KV memory
  estimate for a provider+model. `provider` and `model` are required;
  `context_length` is optional and must be >= 1 (schema-rejected with 422
  otherwise). The estimate reports its `confidence` in-band — `calibrated`,
  `estimated`, or `unknown` — alongside facade fields such as
  `predicted_max_context` (the context that fits beside the weights), the
  tri-state `fits_weights` / `fits_requested_context` split, `budget_bytes`,
  `est_kv_bytes`, and `notes` (which state the budget basis and reserve).
  The estimate is advisory only — no load path gates on it.

Like the other host-facade relays, these routes never 500 on capability gaps:
a runtime without the method answers 200 with `ok: false`,
`available: false`, and `code = "model_residency_unavailable"` (lock/unlock)
or `code = "context_estimate_unavailable"` (estimate); facade exceptions use
the matching `*_error` codes.

### Session prompt-cache enumeration

- `GET /api/gateway/sessions/prompt_cache?session_id=<optional>`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/clear_all` (admin)

The list route enumerates the prompt caches the runtime actually minted. Each
cache row carries the provider/model/runtime identity, byte and token counts,
and stamped attribution metadata (`session_id`, `run_id`, `workflow_id`,
`node_id`). Omit `session_id` to list every session's caches. This
enumeration lane is the recommended way to observe and reclaim session cache
state: unlike the identity-derived session lifecycle endpoints described
under the prompt-cache control plane below, it cannot miss caches whose keys
the gateway never derived.

`clear_all` unloads every runtime-minted cache for one session in a single
call. It requires an admin principal because it accepts any session id and
clears real provider cache state; the identity-derived, caller-scoped session
lifecycle endpoints remain user-level.

When the runtime facade does not expose enumeration, both routes answer 200
with `ok=false`, `available=false`, and `code="session_caches_unavailable"`
(facade errors use `code="session_caches_error"`); the list route always
carries a `caches` array and `clear_all` always carries `cleared` and `count`.

### Discovery descriptors

`GET /discovery/capabilities` advertises this surface under
`capabilities.contracts.common`:

- `model_residency`: `endpoints` (`loaded`, `load`, `unload`, `lock`,
  `unlock`, `context_estimate`), the per-task support map,
  `row_schema = "model_residency_row_v1"`, and `modality_ui` — the canonical
  modality color map (`{version: 1, colors: {...}}`, one `{color, label}`
  entry per residency task plus an `unknown` fallback) so every client
  renders the same modality palette instead of hardcoding its own. It is a
  rendering contract, not a runtime capability, so it is served even when the
  runtime facade is absent.
- `host_state`: `endpoints` (`state`, `memory`, `gpu`) plus
  `memory_available`. The state route itself always answers; per-section truth
  lives in the payload's `degraded` list.
- `session_caches`: `endpoints` (`list`, `clear_all`) plus `available`,
  reflecting whether the runtime facade supports cache enumeration.

Evidence: `src/abstractgateway/routes/gateway.py` (`host_state`,
`host_memory_metrics`, `model_residency_loaded`, `model_residency_lock`,
`model_context_estimate`, `session_prompt_caches_list`) and
`src/abstractgateway/security/authorization.py` (route-family policy).

## Host control (pause, desktop tray, restart, update)

The process's own controls — the surface behind the desktop tray icon and
the console's Gateway card (see [tray.md](docs/tray.md)). Reads are available to
any authenticated principal; writes require an admin principal.

- `GET /api/gateway/host/runner` — execution state:

```json
{"ok": true, "paused": true, "paused_at": "2026-09-05T06:38:26+00:00", "paused_by": "default/admin", "reason": "meeting",
 "inflight_ticks": 0, "scope": "workflow runner", "runner_in_process": true, "step_gate_supported": true,
 "runners": [{"status": "paused", "...": "..."}], "degraded": false,
 "capabilities": {"restart": true, "shutdown": true, "reason": null, "update_job_running": false}}
```

- `POST /api/gateway/host/pause` (body `{"reason": "..."}` optional) and
  `POST /api/gateway/host/resume` — both answer the payload above.
- `GET /api/gateway/host/metrics/live` — `{gpu, memory, runner}` in one call
  (1 s caches); `gpu`/`memory` carry the same in-band `supported` shape as
  `/host/metrics/gpu` and `/host/metrics/memory`.
- `GET /api/gateway/host/runs?limit=25&window_hours=24` — recent runs across
  every data plane on this host (admin; `/runs` answers only for the calling
  principal's plane). `{ok, items: [{run_id, workflow_id, label, status,
  created_at, updated_at, ledger_len, plane, started_epoch}], count, has_more,
  planes, skipped_entity_planes?, warnings?}`. `label` decodes a catalog
  workflow's internal id (`__catalog__v2__…<base64>`) to the name an operator
  uses. Root runs only; the gateway's own bookkeeping runs (`__`-prefixed, but
  never a catalog id) are excluded.
- `GET /api/gateway/host/tray` — `{dependencies_installed, install_hint,
  decision: {start, reason, hint}, supervisor: {running, ready, pid,
  exit_code, failure, log_path}, can_control}`. There is no setting: the icon
  is shown whenever this process and this desktop can hold it.
- `POST /api/gateway/host/tray/show` — retry the helper now (409 when this
  process cannot). No `hide` counterpart, by design.
- `POST /api/gateway/host/restart`, `POST /api/gateway/host/shutdown` —
  `{"ok": true, "restart": true, "requested_by": "...", "reason": "..."}`;
  409 with a plain reason when unsupported (`--reload`, embedded server, an
  update is installing).
- `GET /api/gateway/host/update`, `POST /api/gateway/host/update/check`,
  `POST /api/gateway/host/update/start` — `{current, install: {kind,
  upgradable, reason, command, extras}, check: {latest, update_available,
  offline, checked_at, error}, job: {state, log_tail, exit_code,
  restart_recommended, version_before, version_after}, restart_pending}`.
  `start` answers 409 when the install cannot be upgraded in place or a job
  is already running.

`GET /api/health` adds `"paused": true` while paused; `status` stays
`"healthy"`.

## Prompt-cache control plane (operator API)

The gateway exposes prompt-cache operator endpoints under `/api/gateway/prompt_cache/*`.
Provider prompt-cache controls affect process-local or remote provider state
and require an admin principal in hosted user-auth mode.

Core endpoints:

- `GET /api/gateway/prompt_cache/capabilities?provider=...&model=...`
- `GET /api/gateway/prompt_cache/stats?provider=...&model=...`
- `POST /api/gateway/prompt_cache/set`
- `POST /api/gateway/prompt_cache/update`
- `POST /api/gateway/prompt_cache/fork`
- `POST /api/gateway/prompt_cache/clear`
- `POST /api/gateway/prompt_cache/prepare_modules`

Behavior:

- These routes use the runtime's AbstractCore prompt-cache client contract rather than directly depending on provider-instance access.
- In local mode they delegate to the in-process provider.
- In remote/hybrid mode they follow whatever `/acore/prompt_cache/*` surface the configured AbstractCore server exposes.
- All core prompt-cache responses include `operation` and `capabilities`, with structured unsupported/error cases (`code="prompt_cache_unsupported"` / `code="prompt_cache_error"` / `code="prompt_cache_unavailable"`).
- These endpoints remain provider/model controls, not a Gateway-owned CachedSession persistence system.

Session lifecycle endpoints:

- `GET /api/gateway/sessions/{session_id}/prompt_cache/status`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/prepare`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/rebuild`
- `POST /api/gateway/sessions/{session_id}/prompt_cache/clear`

These routes derive a deterministic bounded namespace/key from `session_id`,
`bundle_id`, `bundle_version`, `flow_id`, `provider`, `model`, optional
`template_id`, and `version`. The private hash also includes the authenticated
principal scope, so two hosted users using the same session id/provider/model do
not collide in a shared provider control plane; the returned `identity` remains
portable app-level data and does not expose that private scope. These routes
expose three honest modes:

- `unsupported`: provider/model does not expose prompt-cache support; responses include `supported=false`, `ok=false`, and capabilities.
- `keyed`: gateway returns a stable `runtime_hint`/`prompt_cache_key` for Runtime/Core injection, but does not claim module preparation occurred.
- `local_control_plane`: gateway uses supported provider operations such as `prepare_modules`, `fork`, `set`, `clear`, and `stats`.

`status` is read-only. `prepare` accepts optional modules (`system_prompt`,
`workflow_instructions`, `tools`, `pinned_attachments`) and returns either
provider operation results or a key hint. `rebuild` is clear-plus-prepare for
providers that expose clear controls.

These identity-derived endpoints only see caches whose keys the gateway
derived. To enumerate or bulk-clear the caches the runtime actually minted for
a session, use the recommended
[session prompt-cache enumeration lane](#session-prompt-cache-enumeration).

Durable bloc exact-reuse endpoints:

- `POST /api/gateway/blocs/upsert_text`
- `GET /api/gateway/blocs/record`
- `GET /api/gateway/blocs`
- `POST /api/gateway/blocs/delete`
- `GET /api/gateway/blocs/kv/manifest`
- `GET /api/gateway/blocs/kv/list`
- `POST /api/gateway/blocs/kv/ensure`
- `POST /api/gateway/blocs/kv/load`
- `POST /api/gateway/blocs/kv/delete`
- `POST /api/gateway/blocs/kv/prune`

These routes are the primary app-facing durable prompt-cache path:

- create or identify a durable text bloc;
- ensure or load a KV artifact for a target local provider/model;
- use the returned `prompt_cache_binding` in later Runtime-backed generation;
- list/delete/prune artifacts without reaching into provider-private cache state.

They delegate through Runtime's public AbstractCore host facade rather than
proxying Core directly. They are operator-style host controls, so the routes
themselves are not ledgered run execution; the ledgered exact-reuse path is the
later `LLM_CALL.params.prompt_cache_binding` used inside real Runtime runs.

Host-local prompt-cache export/import admin aliases:

- `GET /api/gateway/prompt_cache/saved`
- `POST /api/gateway/prompt_cache/save`
- `POST /api/gateway/prompt_cache/load`

These remain explicitly local/operator-oriented:

- the route paths are compatibility aliases, but the implementation delegates to Runtime's public host facade:
  - `saved` -> `list_prompt_cache_exports(...)`
  - `save` -> `prompt_cache_export(...)`
  - `load` -> `prompt_cache_import(...)`
- local bundle/file runtimes store these exports under the Gateway data dir at `prompt_cache_exports/`
- remote and hybrid runtimes return `code=prompt_cache_local_only`
- response payloads follow Runtime's host-local export/import contract, including `operation`, `local_only`, `artifact_*`, `capabilities`, and `provider_response`

## Email inbox (operator UI; optional)

These endpoints power AbstractObserver’s **Inbox → Email** UI. They are **account-scoped**: the browser cannot supply arbitrary IMAP/SMTP host/user credentials. The gateway host must be configured with one or more email accounts (multi-account YAML or env vars).

Endpoints:
- `GET /api/gateway/email/accounts`
- `GET /api/gateway/email/messages?account=…&mailbox=…&since=…&status=…&limit=…`
- `GET /api/gateway/email/messages/{uid}?account=…&mailbox=…&max_body_chars=…`
- `POST /api/gateway/email/send`

Examples:

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/email/accounts"
```

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/email/messages?status=unread&since=7d&limit=20"
```

```bash
curl -sS -H "$AUTH" "$BASE_URL/api/gateway/email/messages/12345?max_body_chars=20000"
```

```bash
curl -sS -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"to":"you@example.com","subject":"Hello","body_text":"Hi!"}' \
  "$BASE_URL/api/gateway/email/send"
```

Configuration notes (gateway host):
- Multi-account: set `ABSTRACT_EMAIL_ACCOUNTS_CONFIG=/path/to/emails.yaml` (recommended).
- Single-account env fallback: set `ABSTRACT_EMAIL_IMAP_*` and/or `ABSTRACT_EMAIL_SMTP_*`.
- The secret itself must be present in the env var referenced by `*_PASSWORD_ENV_VAR` (e.g. `EMAIL_PASSWORD=...`).

Evidence: `src/abstractgateway/routes/gateway.py` (`/email/accounts|messages|send`) which proxies to the Runtime AbstractCore comms facade.

Troubleshooting and common questions: [faq.md](docs/faq.md).

---

## docs/console.md

# Consoles: web (`/console`) and terminal (`abstractgateway-console`)

AbstractGateway ships two operator consoles over the same admin HTTP API. Both
edit the same stores through the same endpoints with the same request bodies, so
a change made in one is immediately visible in the other.

| | Web console | Terminal console |
|---|---|---|
| Delivery | served by the gateway at `GET /console` (part of the `abstractgateway` Python package) | Rust crate [`abstractgateway-console`](https://crates.io/crates/abstractgateway-console), installed with `cargo install` |
| Sign-in | Gateway browser session (user id + token) | base URL + bearer token (`ABSTRACTGATEWAY_AUTH_TOKEN` or the Connection screen) |
| Best for | day-to-day administration in a browser, sandbox chat with media previews | SSH sessions, headless hosts, keyboard-only setup |

For how the stores behind these screens are owned (Gateway vs AbstractCore),
see [configuration.md](docs/configuration.md). For the endpoints themselves, see
[api.md](docs/api.md).

## Web console (`/console`)

Start the gateway and open `http://<host>:<port>/console`:

```bash
abstractgateway serve
# then open http://127.0.0.1:8080/console
```

On a first run, `serve` prints a one-time link that signs you in
([first-run.md](docs/first-run.md)); `abstractgateway claim --open` prints a new
one. The console uses the current origin, so it never asks for a gateway URL.
You can also sign in with a user id and token (the admin's first token is in
`<data dir>/auth/bootstrap-admin-token`).

The sidebar lists these tabs:

| Tab | What it covers |
|---|---|
| **Users & Entities** | user records, token rotation, retained runtime reservations, and the summoned-entity roster ([entities.md](docs/entities.md)) |
| **Runtimes** | execution planes: runs (cancel, steer), sessions, data and caches |
| **Workflows** | every registered workflow with versions and entrypoints, import, export, delete, and versions that are not served (with the reason) |
| **Providers** | provider connections (OpenAI, Anthropic, OpenRouter, Portkey, LM Studio, Ollama, custom OpenAI-compatible endpoints) with write-only keys |
| **Multimodal** | capability route defaults, the text reasoning effort, the MTP (speculative decoding) default, and model weights per route |
| **Sandbox** | quick chat and media generation against the configured defaults |
| **Resources** | memory and GPU meters, resident models (warm up, lock, unload), session prompt caches, and the **Gateway** card (pause, update, restart, desktop icon) |
| **Models** | browse models that fit this machine, download them, delete installed ones (below) |
| **Engines** | the local engines on the gateway host: installed or not, running or not, install, start, stop (below) |
| **Apps** | the browser apps, Code's terminal app and the desktop Assistant (below), plus *Advanced: apps settings* and *Advanced: backlog settings (Continuum)* |
| **Network** | who can reach the gateway (localhost only, local network, internet), its addresses, and *Advanced: reverse proxy* ([configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet)) |

The **Technical details** switch at the bottom of the sidebar shows commands,
route ids and other technical information throughout the console. The top bar
holds the docs assistant (answers grounded on this gateway's documentation),
the appearance settings, the **Setup** button that reopens the first-run guide
(admins), and the sign-out control.

### Models and Engines tabs

Everything these two tabs show and do happens **on the gateway host**: the
machine that runs `abstractgateway serve`, not the computer your browser runs
on. The catalog data, the presence checks and the fit verdicts come from
AbstractCore (`GET /api/gateway/models/catalog`, contract `model_catalog_v1`);
downloads are the gateway's own jobs (see [Model downloads](docs/model-downloads.md)).

**Models** (tab id `catalog`) shows the catalog as **one card per model**:

- The card header: the model's name, organisation, parameter count and
  licence, its capabilities (Text, Thinking, Tools, Vision, Audio, Embedding,
  Voice, Image) and a **Starter** badge for the models of the recommended
  starter set.
- The card body: one row per downloadable build (artifact) of that model: the
  provider (MLX, Ollama, LM Studio, Hugging Face, ...), the artifact id (long
  ids are shortened with "..."; hover to read the whole id, click it to copy
  it), the quantization (4-bit, 8-bit, 16-bit, ... and its bits per weight),
  the download size ("about" when the size is estimated from the parameter
  count), whether the weights are already here (Downloaded, Not downloaded,
  Unknown, Remote) and whether it fits this machine (Fits, Tight, Partial
  offload, Too large; hover the pill for the numbers behind it). The build
  recommended for this computer comes first and is marked; the others are
  quieter.
- One action per row: **Download** (a download shows its progress bar with
  bytes, speed and time left, and **Cancel**, the same progress display as the
  setup guide), then **Use as default** once a text model is downloaded (it
  sets the default text model, like the Multimodal tab).
- Many models have 8-bit builds next to the 4-bit ones when upstream publishes
  them (MLX `-8bit` repositories, Ollama `-q8_0` tags, GGUF `Q8_0` files).
  Every build the catalog knows is listed; nothing is cut off.

The filter bar above the cards:

- **Search** matches model names, organisations and artifact ids (every word
  must match). **Escape** clears it.
- **Quantization**: All, 4-bit, 8-bit, Other (every other class: 16-bit, full
  precision, 2/3/5/6-bit and builds whose reference names no quantization).
  The classes come from AbstractCore's `quant_class` field.
- **Catalog / Hugging Face**: the switch left of the search box. In
  **Hugging Face** mode, type a name and press **Enter** (or **Search**): the
  gateway searches the Hugging Face Hub (answers are cached for 24 hours) and
  the results show as the same cards, with a **Hugging Face** badge: provider,
  artifact id, quantization when the result names one ("Not stated"
  otherwise, never guessed), size from the Hub, fit, and **Download** with
  the same progress bar. Capabilities of a Hub result are unknown until it is
  installed, so it offers no **Use as default** from here. When the Hub has
  nothing for the query the view says so; when the gateway host cannot reach
  the Hub it says "Hugging Face could not be searched right now" (the raw
  reason sits behind **Show details**), and results the Hub only partly
  answered carry a warning. The query is part of the address:
  `/console#catalog?hf=smollm`.
- **Provider**, **Capability** and **Status** (Downloaded, Not downloaded)
  chips, each with the number of builds it would show.
- **Fits this computer** hides the builds that do not fit (only Fits and Tight
  remain).
- A live count: "12 of 77 models · 31 artifacts shown". The row with the search
  box, the count and the filters in use stays under the header while you
  scroll; **Filters** brings the chips back into view. When nothing matches,
  **Clear filters** resets them.
- The filters are part of the address: `/console#catalog?quant=8bit&provider=mlx&fits=1`
  opens the tab with exactly that view, so a link reproduces it. The keys are
  `q`, `quant` (`4bit`, `8bit`, `other`), `provider`, `cap`, `status`
  (`downloaded`, `not_downloaded`), `fits=1` and `hf` (Hugging Face mode and
  its query).

Below the cards, **On this computer** is AbstractCore's own list of the models
the local engines hold (including models that are not in the catalog), with
their size and location, and **Delete** (with a confirmation that names the
model and any blocker, such as a model that is loaded right now).

The setup guide's **Default model** step shows the same catalog cards with
**Fits this computer** already on; **Open in the Models tab** carries the
filters over. An engine card's **Browse models** opens the tab filtered to that
engine's builds.

**Engines** (tab id `engines`):

- One card per engine with a status pill (Ready, Running, Installing, Needs
  your approval, Needs Apple tools, Not installed, Not for this computer),
  its version, base URL and model count, and one primary action for its
  state: **Install**, **Start**, **Stop**, **Continue with administrator
  password**, **Install tools** or **Try again**.
- **Install** opens a confirmation that shows what will run and the host it
  runs on, with a **Preview (dry run)** button. Ollama and LM Studio offer
  "Install" (just for you, no password) and, when that plan needs an
  administrator, "Install for all users (administrator)". What each install
  does, engine by engine: [engines.md](docs/engines.md).
- An install shows its progress on the card, one plain sentence first and the
  full log behind **Show details**. **Open download page** links to the
  vendor page.

Keys (when the tab is visible and you are not typing in a field): `/` search,
`f` fits-only on/off, `r` refresh; on a focused row `w` download, `d` delete,
`i` install, `o` open the download page, `c` cancel the row's job.

Who can do what:

- Every signed-in user can browse both tabs.
- Download, Delete, Install and Cancel are for **admins** only; the buttons are
  disabled for other users, and the gateway refuses those calls from them.
- Installing an engine also needs the gateway setting
  [`allow_engine_install`](docs/configuration.md#allow_engine_install). It lives in
  the runtime configuration and is on by default when the gateway listens on
  this machine only (`127.0.0.1`), and, whatever it listens on, for someone
  using the console on the gateway machine itself. A browser on another
  computer is refused until an admin turns the setting on. A dry run
  (**Preview**) is always allowed.
- Every action is recorded in the gateway's audit log, and each job card shows
  the equivalent command, for example
  `abstractgateway models download ollama qwen3:8b`.

If AbstractCore on the gateway host is missing or too old for these routes,
both tabs show a card with the installed version and the upgrade command; the
rest of the console keeps working.

### Apps tab

The Apps tab and the setup guide's Apps step show the same cards (the apps
themselves are described in [apps.md](docs/apps.md)). What a plain user sees on
each card: the app's mark, name and status pill (Not installed, Installed,
Running, Installing, Stopped unexpectedly, Keeps crashing), one line of
description (hover it for the whole sentence), and one row of buttons. The
button rows of the cards side by side are always at the same height.

| The app is | The action row |
|---|---|
| not installed | **Install** (installs Node.js first when the gateway needs it, then the app and, for Code when a ready-made download exists for this computer, its terminal app too; nothing opens by itself) |
| installing | a progress bar above the row (with one row per part: "Code in the browser", "Code in the terminal"), and **Cancel** (it stops both) |
| an install failed | the reason, **Show details**, and **Install** again |
| installed and running | **Open** (a new tab, already signed in) |
| installed but stopped, or crashed | **Open** (starts it, then opens it); a crash also shows the reason, with **Show details** |

Code has a terminal version too. Next to Code's Open: **Open in Terminal**
when the terminal version is installed and the browser is on the gateway
machine (a new terminal window opens there, signed in). The plain view never
installs the terminal version on its own: Code's Install installs both. When
the terminal version needs the Rust toolchain, or when the browser is on
another computer, the plain view shows no terminal button (the commands are
under Technical details, as is "Install terminal app" for a browser app that
is installed without it).

The last card is the **Assistant**, the desktop app: **Install** when it is
not on the gateway's computer, then **Open** (it starts in the menu bar of the
gateway's computer, or comes to the front when it already runs). From another
computer the card says "The Assistant runs on the gateway's computer: open it
there." with no button. See [apps.md](docs/apps.md#the-assistant-a-desktop-app).

A result box appears only after something you did ("Code opened in a new
Terminal window, signed in to this gateway.", "Flow Editor opened in a new
tab.") and closes itself after a few seconds. A failure stays, with the
gateway's reason and **Show details** (the full response or log).

The **Technical details** switch (bottom left of the console, and in the
guide) adds a secondary line under each card's buttons, and removes it again
when switched off:

- **Stop** (a running app), **Start** (start without opening), **Show log** /
  **Hide log** (the app's log, the log file's path, **Show more** up to 5000
  lines), **Update to X** when a newer version is published, and the version.
- For Code's terminal version: its version, **Update terminal app to X**, and
  the exact command to copy (one line, **Copy**): the command that opens it,
  `cargo install abstractcode` when it needs the Rust toolchain, or, for a
  browser on another computer, the command to run there and the one-time
  `abstractcode login` line.
- The app's local address, the `npx @abstractframework/<app>` line, and the
  log of a finished install.
- For an app started outside the gateway (the development stack, `npx`, a
  service): "Started outside the gateway on port 3001" instead of Stop,
  Start, Show log and Update; the card shows the Running pill and **Open**
  like any running app ([apps.md](docs/apps.md#apps-started-outside-the-gateway)).

Installing, starting, stopping and updating need an admin; other users see the
buttons disabled with the reason on hover.

## Terminal console (`abstractgateway-console`)

Install it from crates.io (Rust 1.87 or newer):

```bash
cargo install abstractgateway-console
```

Connect it to a running gateway. Pass the token through the environment rather
than on the command line:

```bash
ABSTRACTGATEWAY_AUTH_TOKEN=... abstractgateway-console --url http://127.0.0.1:8080
abstractgateway-console --help
```

It opens as a guided wizard on first run and as free tabs afterwards, with eight
screens: Connection, Providers, Routes (capability defaults, including the MTP
selector and a Test verb per route), Users & Entities, Runtimes (runs with
cancel and steer, data homes), Workflows, Review & Test (the session's change
journal), Resources, Models and Engines. Every write is verified with a
follow-up read and recorded in the journal. Keys: `Tab` focus, `Enter`
activate, `1`-`9` and `0` screens, `r` refresh, `q` quit; each screen lists its
actions in the footer.

### Models and Engines in the terminal console

Screens 9 (**Models**) and 0 (**Engines**) are AbstractCore's own screens,
taken from the `abstractcore-console` crate rather than rebuilt, so they look
and behave the same in `abstractcore-console` and here. In the gateway console
they act on the gateway's host, through the gateway's
`/api/gateway/host/profile`, `/engines`, `/models/catalog`,
`/models/installed`, `/models/download`, `/models/delete`,
`/engines/{id}/install` and `/jobs/{id}` routes:

- **Models:** browse the catalog with a fit verdict for the gateway host,
  download (`w`), delete after a confirm that lists any blocker (`d`), filter
  (`/`), fits only (`f`), engine (`e`), installed view (`v`), cancel (`c`).
- **Engines:** see which engines are installed and running; install one (`i`)
  after a confirm that shows the exact command and the host it runs on (a dry
  run is offered), or open its download page (`o`).

Downloads, deletes and installs are admin-only and run on the gateway host; a
refusal (for example installs disabled on a remote gateway, or a loaded model)
is shown with the gateway's reason.

The terminal console needs no gateway-side component beyond the admin API. The
crate version is independent of the Python package version; see
[`console-tui/CHANGELOG.md`](console-tui/CHANGELOG.md).

## Model residency from a shell

The same model routes the consoles drive are available from the Python CLI
against a running gateway:

```bash
abstractgateway models loaded --url http://127.0.0.1:8080
abstractgateway models load   --url http://127.0.0.1:8080 --provider ollama --model qwen3:4b
abstractgateway models unload --url http://127.0.0.1:8080 --provider ollama --model qwen3:4b
```

The token comes from `--token` or `ABSTRACTGATEWAY_AUTH_TOKEN`, and the URL from
`--url` or `ABSTRACTGATEWAY_URL`. The command prints the gateway's JSON answer
and exits non-zero when the gateway reports a failure. `unload --force` unloads
a locked model; in-flight calls on the model are cancelled first.

---

## docs/shipped-workflows.md

# Shipped Workflows

A fresh Gateway install serves a ready-to-use workflow registry. In bundle
mode (the default), Gateway loads the `.flow` bundles packaged with the wheel,
so you can run a proven coding agent, a deep-research pipeline, and a
co-scientist hypothesis engine out of the box — no manual bundle install.

List them on a running gateway:

```bash
curl -sS -H "Authorization: Bearer $TOKEN" \
  "http://127.0.0.1:8080/api/gateway/bundles"
```

## The shipped set

`flow_id` values below are what you pass to the API — an entrypoint's display
name is not resolvable. A `*` marks the bundle's default entrypoint, used when
you omit `flow_id`.

| Bundle | Version | `flow_id` (interfaces) | What it does |
| --- | --- | --- | --- |
| `basic-agent` | 0.0.4 | `81795ea9`* (`abstractcode.agent.v1`) | The framework default chat agent: one Agent node with tools, memory, and status updates. Serves entity phases and chat hosts. |
| `coding-agent` | 0.2.6 | `coder` (`abstractcode.agent.v1`), `coding-agent`* (`abstractcode.coding.v1`) | Verify-gated coding: a builder agent writes code, an independent verifier runs build/execute/match gates each round, and failures feed back as reprompts until gates pass. `coder` is the chat entrypoint; `coding-agent` is the structured pipeline. |
| `deep-research` | 0.1.7 | `deep-research`* (`abstractcode.agent.v1`, `abstractresearch.deep.v1`) | Production research with adversarial review, a verified source ledger, and Markdown/PDF/DOCX export. See [deep-research.md](docs/deep-research.md). |
| `co-scientist` | 0.2.0 | `co-scientist`* (`abstractresearch.coscientist.v1`) | Multi-agent hypothesis engine: literature grounding through the deep-research investigation flows, then cycles of generation, reflection, Elo-ranked pairwise debate, and evolution into a final reviewed research overview. |
| `abstractassistant-orchestrator` | 0.0.0 | `d5d4e5a1`* (`abstractassistant.agent.v1`) | Orchestrator for the compact AbstractAssistant tray surface. |
| `docs-qa` | 0.1.0 | `docsqa001`* | Documentation Q&A grounded on the asking app's `llms.txt` corpus; also auto-published into the tenant workflow catalog at boot. |
| `react-agent` / `codeact-agent` / `memact-agent` | 0.1.0 | `react`* / `codeact`* / `memact`* (`abstractcode.agent.v1`) | Native ReAct / CodeAct / MemAct agent loops (abstractagent). |

Interfaces are how clients pick workflows without knowing bundle internals:
anything declaring `abstractcode.agent.v1` can serve a plain chat prompt.
AbstractCode TUI resolves its default agent through that contract — a saved
preference first, then `coding-agent`'s `coder` when installed, then
`basic-agent` — so a stock Gateway gives it the verify-gated coder by
default.

## Running one

Start a run with the normal runs API (see [api.md](docs/api.md) for the full
contract and streaming). `flow_id` selects an entrypoint; omit it to run the
bundle's default entrypoint:

```bash
curl -sS -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"bundle_id":"coding-agent","flow_id":"coder","input_data":{"prompt":"Write a CLI that ..."}}' \
  "http://127.0.0.1:8080/api/gateway/runs/start"
```

`deep-research` takes `request`, `viewpoint`, and `effort` inputs
([deep-research.md](docs/deep-research.md)); `co-scientist` takes a `research_goal`
and scales its effort with `max_cycles`.

Markdown, PDF, and DOCX export work on a base install. `co-scientist` also
draws two figures (an architecture diagram and an Elo trajectory) through the
`write_chart` node, which needs `matplotlib`. That package is not part of the
base install: without it the run still completes and reports its findings, and
only the figures are skipped. Install it if you want them:

```bash
pip install matplotlib
```

## Managing workflows from the console

Both consoles carry a **Workflows** surface — a tab in the web console, step 6
in the console-TUI — listing every workflow registered on this gateway with how
many published and draft versions each has. Select a workflow to see its
versions and its entrypoints with their declared interfaces.

From there you can:

- **Import** one or more `.flow` bundles. Each file reports whether the gateway
  is serving the result, not merely that the upload succeeded.
- **Export** a version as its original `.flow` bytes, byte-identical to what is
  installed, so it can be archived or re-installed elsewhere. In the TUI, `e`
  writes the file next to your working directory.
- **Delete** a single version or every version of a workflow, behind a
  confirmation that states what is irreversible. In the TUI, `d` removes the
  selected version and `D` the whole workflow.

Versions the gateway is not serving are listed separately under **Not loaded**,
with the reason and the file path. They stay on disk until you remove them.

## Who can change the registry

The gateway's own bundle directory is the shared set: every user sees it and can
run what it contains. Changing it is an operator act, so installing, replacing,
removing, deprecating and reloading workflows there require an admin principal.
Listing and running remain available to every user.

Under hosted user auth each principal also gets its own workflow registry, and
you can install and remove workflows there without admin rights — the shared
directory stays visible and read-only alongside it. The rule is ownership: you
may change the registry you own.

The same check covers every route that writes the registry, including
`POST /bundles/upload`, `DELETE /bundles/{bundle_id}`, `POST /bundles/reload`
and `POST /visualflows/{flow_id}/publish`. A non-admin request against the
shared registry returns `403`.

`basic-agent.flow` is the default framework agent and the gateway verifies it at
startup, so `DELETE` refuses to remove it and answers `409`. To replace the
default agent, install the replacement bundle first, then remove the old file.

## Customizing the registry

- The shipped registry is used when `ABSTRACTGATEWAY_FLOWS_DIR` is unset;
  point it at your own bundle directory to serve a custom set
  ([configuration.md](docs/configuration.md)).
- Shipped bundle versions are immutable pins; newer versions install alongside
  them through the normal bundle upload or workflow catalog routes.
- The editable sources for the shipped workflows are VisualFlow JSON files in
  the AbstractFlow repository (`abstractflow/examples/flows/`), packed with
  `abstractflow bundle pack`.

---

## docs/deployment.md

# AbstractGateway deployment

AbstractGateway can run as a Python process or as a containerized server. The
container path is the recommended baseline for a single self-contained Gateway
deployment because it packages the HTTP API, durable runner, AbstractRuntime,
and the Runtime-owned provider/tool stack together.

## Published image

Release images are published to GHCR. The default image is the light,
portable server image:

```bash
docker pull ghcr.io/lpalbou/abstractgateway:0.4.3
```

NVIDIA hosts can try the experimental full GPU image when local
vLLM/HuggingFace/Diffusers engines are wanted. This image is published
best-effort until it has a real CUDA build and smoke gate:

```bash
docker pull ghcr.io/lpalbou/abstractgateway:0.4.3-gpu
```

The image names `ghcr.io/lpalbou/abstractgateway-server:*` and
`ghcr.io/lpalbou/abstractgateway-server-nvidia:*` are published as aliases for
existing deployments; use `abstractgateway` for new ones.

The default image installs the base `abstractgateway` package, which includes:

- `AbstractRuntime`
- `AbstractMemory[lancedb]>=0.3.0`
- `abstractagent`
- FastAPI/Uvicorn

This profile supports hosted/commercial providers, OpenAI-compatible text
and multimodal provider routing, Runtime-owned tool execution, KG memory, and
provider/session prompt-cache controls. Remote embeddings are included through
the `embedding.text` capability route for hosted providers, LM Studio, vLLM,
other OpenAI-compatible endpoints, or a remote AbstractCore server. Local
sentence-transformer embeddings and hardware-local model runtimes remain
explicit opt-ins, so the base Linux image does not pull PyTorch/CUDA runtime
packages. MLX, vLLM, HuggingFace
Transformers, local Diffusers/sdcpp, AbstractVoice local engines, and local
AbstractMusic engines belong in native `abstractgateway[apple]` or
`abstractgateway[gpu]` installs.

The NVIDIA image installs `abstractgateway[gpu]` and uses a CUDA/PyTorch base.
It is experimental and release automation publishes it as
best-effort for `linux/amd64`; the default image remains the release-grade
portable `linux/amd64` and `linux/arm64` image. Treat the NVIDIA image as
production-ready only after a CUDA host build/smoke gate is added and passes.

### Apple Silicon / MLX

There is no Apple/MLX Gateway Docker image target. MLX uses Apple's Metal
stack, while Docker Desktop runs Linux containers without Metal/MPS device
access. The supported Docker shape is a lightweight Gateway container calling a
host-native OpenAI-compatible inference endpoint:

```bash
docker run --rm --name abstractgateway \
  -p 8080:8080 \
  -e ABSTRACTGATEWAY_DATA_DIR=/data \
  -e ABSTRACTGATEWAY_USER_AUTH=1 \
  -e OPENAI_BASE_URL="http://model-runner.docker.internal/engines/v1" \
  -v "$PWD/runtime:/data" \
  ghcr.io/lpalbou/abstractgateway:latest
```

Set the execution-host text route separately:

```bash
docker exec abstractgateway abstractgateway-config set-default input.text \
  --provider openai-compatible \
  --model your-model \
  --base-url http://model-runner.docker.internal/engines/v1
```

Other host-native endpoints are also valid: LM Studio at
`http://host.docker.internal:1234/v1` with `LMSTUDIO_BASE_URL`, Ollama at
`http://host.docker.internal:11434` with `OLLAMA_BASE_URL`, or `mlx_lm.server`
exposed on a host port. For fully native non-Docker installs with local engines, use
`pip install "abstractgateway[apple]"` on Apple Silicon, and
`pip install "abstractgateway[gpu]"` on GPU workstations or NVIDIA Docker builds.

## Compose quickstart

Create an env file from the template, adjust provider keys/defaults, then start
the server. The default env keeps user auth enabled and bootstraps
`default/admin` if missing:

```bash
cp docker/abstractgateway-server/.env.example docker/abstractgateway-server/.env
docker compose --env-file docker/abstractgateway-server/.env \
  -f docker/abstractgateway-server/compose.yml up -d
```

For the experimental NVIDIA image on a GPU host with the NVIDIA Container
Toolkit:

```bash
docker compose --env-file docker/abstractgateway-server/.env \
  -f docker/abstractgateway-server/compose.yml \
  -f docker/abstractgateway-server/compose.nvidia.yml up -d
```

The default compose profile binds to `127.0.0.1:8080`, mounts a durable Gateway
data volume at `/data`, and exposes a container workspace at `/workspace`. It
serves the workflows the image ships with — `basic-agent`, `coding-agent`,
`deep-research`, `co-scientist`, and more
([shipped-workflows.md](docs/shipped-workflows.md)).

To serve your own bundles instead, point `ABSTRACTGATEWAY_HOST_FLOWS_DIR` at
your bundle directory (mounted read-only at `/data/flows`) and set
`ABSTRACTGATEWAY_FLOWS_DIR=/data/flows`:

```bash
ABSTRACTGATEWAY_HOST_FLOWS_DIR=/path/to/bundles \
ABSTRACTGATEWAY_FLOWS_DIR=/data/flows \
  docker compose -f docker/abstractgateway-server/compose.yml up -d
```

Smoke checks:

```bash
curl http://127.0.0.1:8080/api/health

ADMIN_TOKEN="$(docker compose -f docker/abstractgateway-server/compose.yml exec -T abstractgateway cat /data/auth/bootstrap-admin-token)"
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/gateway/me
```

## Core configuration

Required for hosted/container user-auth mode:

- `ABSTRACTGATEWAY_USER_AUTH=1`: enables Gateway user tokens and per-user routing
- `ABSTRACTGATEWAY_BOOTSTRAP_ADMIN=1`: creates `default/admin` if missing

Optional:

- `ABSTRACTGATEWAY_AUTH_TOKEN`: a shared admin bearer token for
  server/operator scripts; browser apps use Gateway user accounts

Common:

- Browser origins and trust proxy are settings, not variables: the console's
  Network → *Advanced: reverse proxy*, the TUI's Connection screen, or
  `abstractgateway network set --allowed-origins https://gateway.example.com --trust-proxy on`
  (inside a container: `docker exec <container> abstractgateway network set …`).
  They apply to the next request. `ABSTRACTGATEWAY_ALLOWED_ORIGINS` /
  `ABSTRACTGATEWAY_TRUST_PROXY` in the container environment still pin them
  (reported as `overridden_by_env`); see
  [configuration.md](docs/configuration.md#reverse-proxy-allowed-origins-and-trust-proxy).
- `input.text` capability route: default for LLM/agent nodes
- `ABSTRACTGATEWAY_TOOL_MODE`: `approval`, `passthrough`, `delegated`, or local dev modes
- `ABSTRACTGATEWAY_STORE_BACKEND`: `file` or `sqlite`
- `ABSTRACTGATEWAY_DB_PATH`: SQLite file, when using `sqlite`
- `ABSTRACTGATEWAY_RUNNER`: `1` for combined API+runner, `0` for API-only
- `ABSTRACTGATEWAY_MEMORY_STORE_BACKEND`: `lancedb` or `memory` for KG workflows and `/kg/query`; `sqlite` works when the installed AbstractMemory build exposes `SQLiteTripleStore`

Provider keys and endpoints:

- `OPENAI_API_KEY`
- `ANTHROPIC_API_KEY`
- `OPENROUTER_API_KEY`
- `PORTKEY_API_KEY` / `PORTKEY_CONFIG`
- `OPENAI_BASE_URL` / `OPENAI_API_KEY` for generic OpenAI-compatible endpoints
- `OPENAI_COMPATIBLE_BASE_URL` / `OPENAI_COMPATIBLE_API_KEY` (aliases); prefer `OPENAI_BASE_URL` for AbstractCore discovery
- `LMSTUDIO_BASE_URL`
- `OLLAMA_BASE_URL`
- `VLLM_BASE_URL`

Image/voice plugin endpoints:

- `ABSTRACTVISION_BACKEND`: `openai`, `openai-compatible`, `diffusers`, or `sdcpp`
- `ABSTRACTGATEWAY_VISION_BACKEND` / `ABSTRACTGATEWAY_VISION_BASE_URL` / `ABSTRACTGATEWAY_VISION_API_KEY` / `ABSTRACTGATEWAY_VISION_MODEL_ID` (the `ABSTRACTVISION_*` names also work)
- `ABSTRACTGATEWAY_VOICE_TTS_ENGINE` / `ABSTRACTGATEWAY_VOICE_STT_ENGINE` (`openai` by default in the server image; the `ABSTRACTVOICE_*` names also work)
- `ABSTRACTGATEWAY_VOICE_REMOTE_BASE_URL` / `ABSTRACTGATEWAY_VOICE_REMOTE_API_KEY`
- `ABSTRACTGATEWAY_VOICE_TTS_MODEL` / `ABSTRACTGATEWAY_VOICE_STT_MODEL`

Core catalog proxying:

- `ABSTRACTCORE_SERVER_BASE_URL`: explicit standalone Core server URL for voice, TTS/STT, and vision catalog routes
- `ABSTRACTGATEWAY_ABSTRACTCORE_SERVER_AUTH_TOKEN`: Core server auth token, separate from Gateway auth
- `ABSTRACTGATEWAY_CORE_CATALOG_TIMEOUT_S`: timeout for catalog routes

Filesystem/media controls from AbstractCore remain available:

- `ABSTRACTCORE_SERVER_BASE_URL_ALLOWLIST`
- `ABSTRACTCORE_SERVER_URL_FETCH_ALLOWLIST`
- `ABSTRACTCORE_SERVER_MEDIA_ROOT`
- `ABSTRACTCORE_SERVER_ALLOW_LOCAL_FILES`

## Single machine without Docker

On a desktop or laptop, `abstractgateway service install` registers the
gateway as a per-user login service (macOS LaunchAgent, Linux systemd user
unit or XDG autostart entry, Windows Run entry) that runs plain `serve`, so the
[network exposure](docs/configuration.md#network-exposure-localhost--local-network--internet)
setting decides the bind (seeded to `localhost`, i.e. `127.0.0.1`, on install),
with data in the per-user data folder. See [first-run.md](docs/first-run.md). Containers and
servers use the explicit configuration shown on this page: the image sets
`--host 0.0.0.0` with user accounts on.

## Cache and auth notes

Gateway auth is controlled by `ABSTRACTGATEWAY_*` variables and protects
`/api/gateway/*`. AbstractCore provider/server auth variables control upstream
provider access inside AbstractCore integrations. Keep those two layers
separate: clients receive only the Gateway token, while provider keys stay in
the server environment.

Prompt-cache control endpoints are exposed under `/api/gateway/prompt_cache/*`
where supported by the active provider/model. Session lifecycle routes under
`/api/gateway/sessions/{session_id}/prompt_cache/*` provide Gateway-owned
naming/status/prepare/clear/rebuild orchestration on top of those provider
controls. They are not a provider-independent local KV cache or full
CachedSession persistence system.

## Local-source image

Before a version is published to PyPI, build from the checkout:

```bash
ABSTRACTGATEWAY_INSTALL_MODE=local \
ABSTRACTGATEWAY_IMAGE_TAG=0.4.3-local \
docker compose -f docker/abstractgateway-server/compose.yml up -d --build
```

Release automation builds the published image from the PyPI package after the
PyPI release is available, matching the AbstractCore server image pattern.

---

## README.md

# AbstractGateway

AbstractGateway is a **deployable Run Gateway host** for AbstractRuntime runs,
and the control plane of an AbstractFramework installation:

- start durable runs, accept a durable command inbox, and replay or stream a
  durable ledger (replay-first);
- enforce a security baseline: user accounts, browser sessions, an origin
  allowlist, request limits and an audit log;
- manage users, providers, capability defaults, local engines, model
  downloads, browser apps and network exposure from a web console
  (`/console`), a terminal console, the CLI or a desktop tray icon.

Clients (AbstractFlow, AbstractCode, AbstractObserver, AbstractContinuum,
AbstractEntity, AbstractAssistant, scripts) talk to the gateway over HTTP, so
none of them depends on the others.

Start here: [docs/first-run.md](docs/first-run.md) on your own machine, or
[docs/getting-started.md](docs/getting-started.md) for an explicit setup.

## AbstractFramework ecosystem

AbstractGateway is part of the **AbstractFramework** ecosystem:

- **AbstractRuntime** (required): durable run model + workflow registry + stores (`pyproject.toml`, `src/abstractgateway/runner.py`)
- **AbstractCore, AbstractAgent, AbstractMemory** (installed with the gateway): Runtime owns the LLM/tool/media integration boundary; Gateway uses its discovery and run facades for prompt-cache controls, generated and edited media, voice, audio and music, and KG-backed bundle execution (`src/abstractgateway/hosts/bundle_host.py`)
- Apps (optional): AbstractFlow (authoring/bundling), AbstractCode, AbstractObserver, AbstractContinuum, AbstractEntity, AbstractAssistant

Related repos:
- AbstractFramework: https://github.com/lpalbou/AbstractFramework
- AbstractCore: https://github.com/lpalbou/abstractcore
- AbstractRuntime: https://github.com/lpalbou/abstractruntime

## Quickstart: your own machine

```bash
pip install abstractgateway
abstractgateway serve
```

On a first run with nothing configured, `serve` binds `127.0.0.1:8080`, turns
on user auth, creates the admin user, and prints a one-time link
(`http://127.0.0.1:8080/console#claim=...`). Open it in a browser on the same
machine: you are signed in as the admin and a short first-run guide helps you
pick a local engine, a default model and the browser apps. A new link:
`abstractgateway claim --open`. Start the gateway at login:
`abstractgateway service install`. See [docs/first-run.md](docs/first-run.md).

### Local models and engines

The console's **Models** and **Engines** tabs (and the matching commands)
install a local engine and download a model that fits this machine, without a
terminal. They are AbstractCore's model browser and engine installer, shown
inside the gateway:

```bash
abstractgateway engines status --probe          # Ollama, LM Studio, MLX, llama.cpp, ...
abstractgateway engines install ollama --dry-run # the exact command, nothing runs
abstractgateway models catalog --fits           # models that fit this machine
abstractgateway models download ollama qwen3:8b
abstractgateway models list                     # installed models with sizes
```

The commands talk to the running gateway (admin rules and audit log apply);
add `--local` to run them in-process instead. Engine installs run on the
gateway host and are on by default for a loopback gateway, and for someone at
the gateway machine whatever it listens on
([`allow_engine_install`](docs/configuration.md#allow_engine_install)). See
[docs/engines.md](docs/engines.md), [docs/model-downloads.md](docs/model-downloads.md)
and [docs/console.md](docs/console.md).

### Browser apps, network access and the tray

```bash
abstractgateway apps install observer --launch   # Flow, Code, Observer, Continuum, Entity, Assistant
abstractgateway apps open observer               # a one-time signed-in link
abstractgateway network set lan                  # let your local network reach the gateway
abstractgateway network restart                  # apply it now
```

The gateway installs Node.js when needed, installs the browser apps from npm,
runs them and opens them already signed in ([docs/apps.md](docs/apps.md)). The
network setting decides who can reach the gateway: `localhost`, `lan` or
`internet`
([docs/configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet)).
With `pip install "abstractgateway[tray]"`, `serve` also shows a menu bar /
system tray icon ([docs/tray.md](docs/tray.md)).

## Quickstart (HTTP server, bundle mode, explicit configuration)

```bash
pip install abstractgateway

export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"

# Optional: set only for a custom bundle registry. When unset, Gateway uses
# the packaged shipped bundle directory containing basic-agent.
# export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"

# User accounts: the sign-in path for the console and the browser apps.
export ABSTRACTGATEWAY_USER_AUTH=1

abstractgateway serve --host 127.0.0.1 --port 8080
```

OpenAPI docs (Swagger UI): `http://127.0.0.1:8080/docs`

Smoke checks:

```bash
curl -sS "http://127.0.0.1:8080/api/health"

curl -sS -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  "http://127.0.0.1:8080/api/gateway/bundles"
```

That last call lists the workflows a fresh install already serves — including a
verify-gated coding agent (`coding-agent`), `deep-research`, and
`co-scientist`, alongside the default `basic-agent`. See
[docs/shipped-workflows.md](docs/shipped-workflows.md).

## User accounts and the console

With user accounts on (the default of a plain `serve`, or
`ABSTRACTGATEWAY_USER_AUTH=1`), the gateway creates `default/admin`, writes its
first token to `<data dir>/auth/bootstrap-admin-token`, and routes each user to
their own runtime and data plane (`1 user = 1 runtime`). Admins manage users
from the console or `/api/gateway/admin/users`; user tokens are returned once
and stored only as hashes. Browser apps exchange a user token for an opaque
session (`POST /api/gateway/session/login`, HTTP-only cookie plus CSRF token)
instead of keeping the token. `ABSTRACTGATEWAY_AUTH_TOKEN` is a shared
server/operator token, not a browser sign-in token. See
[docs/security.md](docs/security.md).

The built-in console at `/console` covers users and entities, runtimes,
workflows, provider connections, multimodal capability defaults, a sandbox,
host resources, models, engines, apps and network access. The same
configuration surfaces exist in a terminal through the `abstractgateway-console`
Rust app (`cargo install abstractgateway-console`). See
[docs/console.md](docs/console.md).

## Docker server

Release images are published to GHCR. The default image is the light,
portable server image:

```bash
docker pull ghcr.io/lpalbou/abstractgateway:0.4.3
```

NVIDIA hosts can try the experimental full GPU image when local
vLLM/HuggingFace/Diffusers engines are wanted. This image is published
best-effort until it has a real CUDA build and smoke gate:

```bash
docker pull ghcr.io/lpalbou/abstractgateway:0.4.3-gpu
```

The `abstractgateway-server` and `abstractgateway-server-nvidia` GHCR names are
published as aliases for existing deployments; use `abstractgateway` for new
ones.

The image installs the base `abstractgateway` package: HTTP server,
`AbstractRuntime`, Runtime-owned provider/tool and
multimodal facades, OpenAI-compatible text/media providers,
provider/session prompt-cache helpers, AbstractMemory/LanceDB KG support,
AbstractAgent, and AbstractFlow compatibility. Local sentence-transformer
embeddings and hardware-local inference engines are explicit extras so the
light server image does not pull PyTorch/CUDA runtime packages. Remote text
embeddings remain part of the light profile through the `embedding.text`
capability route: point it at OpenAI, OpenRouter, Portkey, LM Studio, vLLM,
any OpenAI-compatible embeddings endpoint, or a remote AbstractCore server.

AbstractFlow note:
- You do **not** need the `abstractflow` Python package to run `.flow` bundles. You only need it to author bundles; the gateway runs bundles only (store VisualFlows through `/api/gateway/visualflows/*` and publish them as bundles).

```bash
docker run --rm --name abstractgateway \
  -p 8080:8080 \
  -e ABSTRACTGATEWAY_DATA_DIR=/data \
  -e ABSTRACTGATEWAY_USER_AUTH=1 \
  -e LMSTUDIO_BASE_URL="http://host.docker.internal:1234/v1" \
  -v "$PWD/runtime:/data" \
  ghcr.io/lpalbou/abstractgateway:latest
```

On first start, the container creates `default/admin` and writes the admin user
token to `runtime/auth/bootstrap-admin-token`. Use that token in `/console`,
then rotate it or create named users from the console.

Configure framework model defaults through execution-host capability routes:

```bash
docker exec abstractgateway abstractgateway-config set-default input.text \
  --provider lmstudio \
  --model your-model \
  --base-url http://host.docker.internal:1234/v1
```

In user-auth mode this writes the Gateway baseline Core config at
`/data/config/abstractcore.json`. Per-user runtime overrides use the same Core
schema under `/data/users/<tenant>/<runtime>/runtime/config/abstractcore.json`;
use `abstractgateway-config set-default --scope user --user alice ...` for
operator-side scripting.

`output.text` is a compatibility alias for this same text route. Gateway reports
it as a read-only view of `input.text`, so LLM text input and output do not drift
to different default models.

On Apple Silicon, keep Metal/MLX inference native on macOS and run the
lightweight Gateway container as the transport/control plane. Point
`OPENAI_BASE_URL` at a generic host-native OpenAI-compatible endpoint such as
Docker Model Runner (`http://model-runner.docker.internal/engines/v1`) or
`mlx_lm.server` on a host port. For named providers, use
`LMSTUDIO_BASE_URL=http://host.docker.internal:1234/v1` or
`OLLAMA_BASE_URL=http://host.docker.internal:11434`.
For native non-Docker installs with local engines, use
`pip install "abstractgateway[apple]"` on Apple Silicon, and
`pip install "abstractgateway[gpu]"` on GPU workstations or NVIDIA Docker builds.
For a minimal Apple-local Gateway + Flow setup, see
[docs/apple-local-gateway-flow.md](docs/apple-local-gateway-flow.md).

Compose and deployment details: [docs/deployment.md](docs/deployment.md).

## Capability scope

Direct, run-scoped Gateway routes (each creates a durable child run and
returns artifacts):

- voice and audio: `POST /api/gateway/runs/{run_id}/voice/tts`,
  `POST /api/gateway/runs/{run_id}/audio/transcribe`
- images: `POST /api/gateway/runs/{run_id}/images/generate`, `/images/edit`,
  `/images/upscale`
- video: `POST /api/gateway/runs/{run_id}/videos/generate`,
  `/videos/from_image`
- music: `POST /api/gateway/runs/{run_id}/music/generate`
- run data: `GET /api/gateway/runs/{run_id}/input_data`,
  `GET /api/gateway/runs/{run_id}/history_bundle`

Discovery and catalogs for thin clients:

- `GET /api/gateway/discovery/capabilities`: a versioned contract of packages,
  plugins, endpoints and feature gates, with `common.readiness`
- voice, speech, transcription, music and vision catalogs
  (`/api/gateway/voice/voices`, `/audio/*/models`, `/audio/music/providers`,
  `/vision/*`), each with a `gateway_catalog_v1` envelope and canonical `items`
- `/api/gateway/artifacts/search`: cross-run, session and run artifact search
- `/api/gateway/kg/query`: KG memory queries (LanceDB by default)
- `/api/gateway/prompt_cache/*` and `/api/gateway/sessions/{session_id}/prompt_cache/*`:
  provider-dependent prompt-cache controls

Media generation needs a configured backend for the route (the console's
**Multimodal** tab). Image and video routes stream `abstract.progress` records
on the child run's ledger; prompt-cache support depends on the provider and
model. Details: [docs/api.md](docs/api.md) and [docs/faq.md](docs/faq.md).

## Client contract (replay-first)

- Clients **start runs**: `POST /api/gateway/runs/start`
  - optional `thinking` sets the run-scoped `_runtime.thinking` default used by
    Flow LLM/Agent nodes and AbstractAgent adapters when Core/provider support
    reasoning controls
- Clients can **schedule runs** (bundle mode): `POST /api/gateway/runs/schedule`
- Clients **act** by submitting durable commands: `POST /api/gateway/commands`
  - supported types: `pause|resume|cancel|emit_event|update_schedule|compact_memory`
- Clients **render** by replaying/streaming the durable ledger:
  - replay: `GET /api/gateway/runs/{run_id}/ledger?after=...`
  - stream (SSE): `GET /api/gateway/runs/{run_id}/ledger/stream?after=...`

Model residency is available from a shell through
`abstractgateway models loaded|load|unload` ([docs/console.md](docs/console.md)).

See [docs/api.md](docs/api.md) for curl examples and the live OpenAPI spec (`/openapi.json`).

## Install

### Base remote-light server

Requires Python `>=3.10` (see `pyproject.toml`).

The base install is the remote-light HTTP/SSE server: Gateway, Runtime,
Agent, Flow compatibility, Runtime-owned provider/tool and multimodal facades,
and LanceDB-backed Memory. It intentionally excludes local sentence-transformer
embeddings and hardware-local inference engines so Linux installs do not pull
PyTorch/CUDA packages. Remote embeddings and remote multimodal input/output
still work in this profile through hosted providers, OpenAI-compatible
endpoints, or a remote AbstractCore server.

```bash
pip install abstractgateway
```

### Optional extras

- `abstractgateway[apple]`: full native macOS Python profile with Apple-local engines and all non-NVIDIA framework capabilities
- `abstractgateway[gpu]`: full local GPU profile with vLLM/HuggingFace, local Diffusers image generation, local voice engines, music, and KG memory; this is also the NVIDIA Docker install profile
- `abstractgateway[embeddings]`: local sentence-transformer embeddings for semantic KG queries
- `abstractgateway[tray]`: a menu bar / system tray icon for `abstractgateway serve` (macOS, Windows, Linux) — open the console, pause/resume workflows, unload models, watch memory and GPU, restart or update; see [docs/tray.md](docs/tray.md)
- `abstractgateway[docs]`: MkDocs site tooling
- `abstractgateway[dev]`: local test/dev deps

KG memory nodes use Gateway's memory resolver. The default durable/vector
backend is LanceDB; `memory` is process-local dev/test storage, and `sqlite` is
structured-only when the installed AbstractMemory build exposes
`SQLiteTripleStore`.

Gateway has a first-class config helper:

```bash
abstractgateway-config status
abstractgateway config init --env-file .env
```

For details on capability route defaults, store backends, and workflow sources, see [docs/configuration.md](docs/configuration.md).

## Creating a `.flow` bundle (authoring)

Use AbstractFlow to pack a bundle:

```bash
abstractflow bundle pack /path/to/root.json --out /path/to/bundles/my.flow --flows-dir /path/to/flows
```

See [docs/getting-started.md](docs/getting-started.md) for running, split API/runner, and file→SQLite migration.

## Docs

Published docs site: https://www.lpalbou.info/AbstractGateway/

- Docs index: [docs/README.md](docs/README.md)
- First run: [docs/first-run.md](docs/first-run.md)
- Getting started: [docs/getting-started.md](docs/getting-started.md)
- Architecture: [docs/architecture.md](docs/architecture.md)
- API overview: [docs/api.md](docs/api.md)
- Configuration: [docs/configuration.md](docs/configuration.md)
- Consoles: [docs/console.md](docs/console.md)
- Apps: [docs/apps.md](docs/apps.md)
- Local engines: [docs/engines.md](docs/engines.md)
- Model downloads: [docs/model-downloads.md](docs/model-downloads.md)
- Desktop tray: [docs/tray.md](docs/tray.md)
- Security: [docs/security.md](docs/security.md)
- Deployment: [docs/deployment.md](docs/deployment.md)
- Shipped workflows: [docs/shipped-workflows.md](docs/shipped-workflows.md)
- FAQ: [docs/faq.md](docs/faq.md)
- Troubleshooting: [docs/troubleshooting.md](docs/troubleshooting.md)
- Operator tooling: [docs/maintenance.md](docs/maintenance.md)

Project: [CHANGELOG.md](CHANGELOG.md) · [CONTRIBUTING.md](CONTRIBUTING.md) ·
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) · [SECURITY.md](SECURITY.md) ·
[ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md) · [LICENSE](LICENSE) (MIT)

---

## docs/README.md

# AbstractGateway docs

AbstractGateway is the durable run gateway and control plane of
AbstractFramework: it starts and replays AbstractRuntime runs over HTTP/SSE,
and lets you manage users, providers, models, local engines, browser apps and
network access from a web console, a terminal console, the CLI or a desktop
tray icon.

New here? Start with [first-run.md](docs/first-run.md) on your own machine, or
[getting-started.md](docs/getting-started.md) for an explicit setup.

## AbstractFramework ecosystem

- **AbstractRuntime** (required): durable runs, workflow registry, stores.
- **AbstractCore**, **AbstractAgent**, **AbstractMemory** (installed with the
  gateway): providers, tools and media capabilities through Runtime's
  facades, agent nodes, and KG memory.
- Apps that use the gateway (optional): AbstractFlow, AbstractCode,
  AbstractObserver, AbstractContinuum, AbstractEntity, AbstractAssistant.

Related repos:
[AbstractFramework](https://github.com/lpalbou/AbstractFramework) ·
[AbstractCore](https://github.com/lpalbou/abstractcore) ·
[AbstractRuntime](https://github.com/lpalbou/abstractruntime)

## Core docs

| Page | Read it for |
|---|---|
| [first-run.md](docs/first-run.md) | the zero-configuration start on your own machine: one-time sign-in link, first-run guide, start at login |
| [getting-started.md](docs/getting-started.md) | explicit setup: bundles, starting and scheduling runs, split API/runner, file vs SQLite stores |
| [architecture.md](docs/architecture.md) | components, diagrams, the replay-first durable contract, deployment shapes |
| [api.md](docs/api.md) | the client contract with curl examples, discovery, media, models, host state; map of every route family |
| [configuration.md](docs/configuration.md) | every setting: runtime settings, network exposure, apps, backlog, capability defaults, environment variables, CLI flags |
| [faq.md](docs/faq.md) | recurring questions and limits |
| [troubleshooting.md](docs/troubleshooting.md) | symptoms, causes and fixes: sign-in, network modes, runs, installs, downloads, tray, login service |

## Topic guides

| Page | Read it for |
|---|---|
| [console.md](docs/console.md) | the web console at `/console` (every tab) and the `abstractgateway-console` terminal app |
| [apps.md](docs/apps.md) | installing, starting and opening the browser apps (Flow, Code, Observer, Continuum, Entity), Code's terminal app and the desktop Assistant |
| [engines.md](docs/engines.md) | installing local engines (Ollama, LM Studio, MLX, llama.cpp, vLLM, Hugging Face): what each Install does, when a password or the Apple tools are needed |
| [model-downloads.md](docs/model-downloads.md) | download jobs: progress, stalls, cancel, end reasons, parent jobs, the event stream |
| [tray.md](docs/tray.md) | the desktop tray icon: apps, models, pause, start at login, network, restart and update |
| [security.md](docs/security.md) | user accounts, sessions, origins, network exposure, workspace scope, limits, audit log |
| [deployment.md](docs/deployment.md) | Docker images, Compose, provider variables, single machine without Docker |
| [shipped-workflows.md](docs/shipped-workflows.md) | the workflows a fresh install serves (coding agent, deep research, co-scientist, …) and managing the registry |
| [deep-research.md](docs/deep-research.md) | the shipped `deep-research` workflow contract |
| [entities.md](docs/entities.md) | summoned entities: homes, lifecycle, summoning, replay |
| [apple-local-gateway-flow.md](docs/apple-local-gateway-flow.md) | an Apple Silicon local Gateway + Flow setup with local engines |
| [maintenance.md](docs/maintenance.md) | operator tooling: reports, triage, backlog, exec runner, process manager, bridges (high trust) |

## API reference (generated)

Published docs site: https://www.lpalbou.info/AbstractGateway/

When the server is running (`abstractgateway serve`):

- Health: `GET /api/health`
- OpenAPI JSON: `GET /openapi.json`
- Interactive Swagger UI: `GET /docs`

## Project docs

- Package README: [../README.md](README.md)
- Changelog: [../CHANGELOG.md](CHANGELOG.md)
- Contributing: [../CONTRIBUTING.md](CONTRIBUTING.md)
- Code of conduct: [../CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)
- Security policy (vulnerability reporting): [../SECURITY.md](SECURITY.md)
- Acknowledgments: [../ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md)
- License: [../LICENSE](LICENSE)

---

## docs/getting-started.md

# AbstractGateway — Getting started

AbstractGateway is a deployable HTTP/SSE host for **durable AbstractRuntime runs**:
- clients **start runs** and submit **durable commands**
- clients **render** by replaying/streaming the durable ledger (replay-first)

This guide starts with the zero-configuration path on your own machine, then
runs the gateway with explicit configuration, starts and schedules runs, and
covers **file vs SQLite** durability and a best-effort **file → SQLite**
migration.

## AbstractFramework ecosystem (context)

AbstractGateway is one component in the larger **AbstractFramework** ecosystem:
- **AbstractRuntime** (required): durable runs + workflow registry + stores
- **AbstractCore, AbstractAgent, AbstractMemory** (installed with the gateway): Runtime owns the LLM/tool/media integration boundary; Gateway uses its discovery and run facades for prompt-cache controls, generated and edited media, voice, audio and music, and KG-backed bundle execution

Related repos:
- AbstractFramework: https://github.com/lpalbou/AbstractFramework
- AbstractCore: https://github.com/lpalbou/abstractcore
- AbstractRuntime: https://github.com/lpalbou/abstractruntime

## Prerequisites

- Python `>=3.10` (see `pyproject.toml`)
- Workflows: none needed to start. The gateway serves its shipped bundles
  ([shipped-workflows.md](docs/shipped-workflows.md)); you can point it at your
  own `.flow` bundles or upload them after startup
  (`POST /api/gateway/bundles/upload`, see below)

## Install

```bash
# Remote-light server package (HTTP/SSE + runner + stores + KG memory)
pip install abstractgateway

# Native Apple local engines
pip install "abstractgateway[apple]"

# Native/container GPU local engines, also used by the NVIDIA Docker image
pip install "abstractgateway[gpu]"

# Desktop menu bar / system tray icon for `serve` (macOS, Windows, Linux)
pip install "abstractgateway[tray]"
```

With the base install and a configured provider stack, Gateway can surface
run-scoped direct TTS, STT, image generation, image edit, and music generation
for higher apps through one shared capability contract.

## 0) Fastest start on your own machine

```bash
abstractgateway serve
```

With no auth configured, this binds `127.0.0.1:8080`, enables user auth,
creates `default/admin`, keeps data in your OS's per-user data folder, and
prints a one-time `First run: open http://127.0.0.1:8080/console#claim=...`
link that signs you into the console and opens the first-run guide. See
[first-run.md](docs/first-run.md), including `abstractgateway claim` and
`abstractgateway service install`. To let other devices on your network reach
it, use `abstractgateway network set lan` (see
[configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet)).
The rest of this guide uses explicit configuration.

## 1) Run with explicit configuration (file-backed stores)

File-backed stores are the default and easiest for development.

```bash
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"

# Optional: set only for a custom bundle registry. When unset, Gateway uses
# the packaged shipped bundle directory containing basic-agent.
# export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"

# User accounts: the sign-in path for the console and the browser apps.
export ABSTRACTGATEWAY_USER_AUTH=1

abstractgateway serve --host 127.0.0.1 --port 8080
```

On first local start, Gateway creates `default/admin`, writes the browser-login
token to `$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token` (mode `0600`),
prints that token, and prints a one-time console sign-in link. Use the token
with user `admin` for browser apps. `abstractgateway serve --no-print-token`
keeps the token out of the startup output (it stays in the file); on a
non-loopback bind it is hidden by default and `--print-token` shows it.
`ABSTRACTGATEWAY_AUTH_TOKEN` is a shared server/operator bearer token; it is
not a browser sign-in token.

Browser origins other than `http://localhost:*` and `http://127.0.0.1:*` are a
setting: `abstractgateway network set --allowed-origins https://your.host`.

OpenAPI docs (Swagger UI): `http://127.0.0.1:8080/docs` (use **Authorize** with a Gateway user token)

Smoke checks:

```bash
curl -sS "http://127.0.0.1:8080/api/health"

curl -sS -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  "http://127.0.0.1:8080/api/gateway/bundles"
```

If `bundles.items` is empty (see also
[troubleshooting.md](docs/troubleshooting.md#get-apigatewaybundles-returns-no-bundles)), either:
- point `ABSTRACTGATEWAY_FLOWS_DIR` at the shipped bundle directory or another
  directory containing `*.flow` files (or a single `.flow` file), or
- upload a bundle via the API:

```bash
curl -sS -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  -F "file=@./my-bundle@0.1.0.flow" \
  -F "overwrite=false" \
  -F "reload=true" \
  "http://127.0.0.1:8080/api/gateway/bundles/upload"
```

You can also create a local env file and inspect readiness with:

```bash
abstractgateway-config init --env-file .env
abstractgateway-config status
```

## 2) Start a run (bundle mode)

First, discover entrypoints from `GET /api/gateway/bundles`. Then start a run:

```bash
curl -sS -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","input_data":{"prompt":"Hello"}}' \
  "http://127.0.0.1:8080/api/gateway/runs/start"
```

Notes:
- If a bundle has multiple entrypoints and no default, you must pass `flow_id`.
- See [api.md](docs/api.md) for ledger replay/stream and durable commands.

## 2b) (Optional) Schedule a run (bundle mode)

To launch a workflow periodically, start a **scheduled parent run**:

```bash
curl -sS -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","flow_id":"ac-echo","input_data":{"prompt":"Ping"},"start_at":"now","interval":"1h","repeat_count":3}' \
  "http://127.0.0.1:8080/api/gateway/runs/schedule"
```

Tip: to stop a schedule, cancel the scheduled parent run (`POST /api/gateway/commands`, type `cancel`).

## 3) Split API vs runner (recommended for upgrades)

By default, `abstractgateway serve` starts the HTTP API **and** the runner loop in the same process.

To restart the HTTP API without pausing durable execution, run two processes sharing the same `ABSTRACTGATEWAY_DATA_DIR`:

```bash
# Process 1 (runner worker, no HTTP deps needed):
abstractgateway runner

# Process 2 (HTTP API only):
abstractgateway serve --no-runner --host 127.0.0.1 --port 8080
```

## 3b) Docker / Compose

For a containerized remote-light deployment with
`AbstractRuntime`, Runtime-owned provider/tool and
multimodal support, KG memory, and provider/session prompt-cache controls
included. Remote embeddings are available in this profile when `embedding.text`
points at a remote provider, an OpenAI-compatible embeddings endpoint, or a
remote AbstractCore server; local HuggingFace/sentence-transformer embeddings
require `abstractgateway[embeddings]`.

```bash
docker run --rm --name abstractgateway \
  -p 8080:8080 \
  -v "$PWD/runtime:/data" \
  -e ABSTRACTGATEWAY_DATA_DIR=/data \
  -e ABSTRACTGATEWAY_USER_AUTH=1 \
  ghcr.io/lpalbou/abstractgateway:latest
```

See [deployment.md](docs/deployment.md) for Compose, provider keys, and image
customization.

On first start, the container creates `default/admin` and writes the token to
`runtime/auth/bootstrap-admin-token`. NVIDIA hosts can try
`ghcr.io/lpalbou/abstractgateway:0.4.3-gpu` with the compose overlay in
`docker/abstractgateway-server/compose.nvidia.yml`.
It is experimental until a real CUDA build/smoke gate is part of release
validation.
Apple MLX inference should run natively on macOS rather than in Docker because
Linux containers do not get access to Apple's Metal/MLX runtime. The container
can still use native macOS inference through an OpenAI-compatible endpoint:
point `OPENAI_BASE_URL` at Docker Model Runner on
`http://model-runner.docker.internal/engines/v1` or `mlx_lm.server`. For named
local providers, set `LMSTUDIO_BASE_URL=http://host.docker.internal:1234/v1` or
`OLLAMA_BASE_URL=http://host.docker.internal:11434` when the native Ollama model
path uses MLX. For native non-Docker installs, use
`pip install "abstractgateway[apple]"` on Apple Silicon, and
`pip install "abstractgateway[gpu]"` on GPU workstations or NVIDIA Docker builds.

## 4) What’s stored in `ABSTRACTGATEWAY_DATA_DIR` (file backend)

When `ABSTRACTGATEWAY_STORE_BACKEND=file` (default), the gateway persists (via `abstractruntime` stores):
- `run_<run_id>.json` (checkpointed run state)
- `ledger_<run_id>.jsonl` (append-only step records)
- `commands.jsonl` and `commands_cursor.json` (durable inbox + runner cursor)
- `artifacts/` (offloaded blobs/attachments)
- `dynamic_flows/` (gateway-generated wrapper flows, e.g. schedules)
- `workspaces/` (per-run workspaces created at run start when `workspace_root` is not provided)

## 5) Enable SQLite-backed stores

SQLite-backed stores eliminate directory scanning and move run/ledger/inbox data into indexed tables.

Artifacts remain file-backed under `ABSTRACTGATEWAY_DATA_DIR/artifacts/`.

```bash
export ABSTRACTGATEWAY_STORE_BACKEND=sqlite

# Optional; when omitted, defaults to: <ABSTRACTGATEWAY_DATA_DIR>/gateway.sqlite3
export ABSTRACTGATEWAY_DB_PATH="$PWD/runtime/gateway/gateway.sqlite3"
#
# Safety invariant: when using sqlite, the DB file must live under ABSTRACTGATEWAY_DATA_DIR.
# The gateway will refuse to start if ABSTRACTGATEWAY_DB_PATH points outside (prevents UAT/prod cross-wiring).

abstractgateway serve --host 127.0.0.1 --port 8080
```

## 6) Migrate an existing file-backed data dir → SQLite

This is a **best-effort** local migration (`abstractgateway migrate`) that reads:
- `run_*.json`
- `ledger_*.jsonl`
- `commands.jsonl`
- `commands_cursor.json`

and writes a single SQLite DB file. It does **not** delete the original files.

```bash
cp -a runtime/gateway "runtime/gateway.file-backup.$(date +%Y%m%d-%H%M%S)"

abstractgateway migrate --from=file --to=sqlite \
  --data-dir runtime/gateway \
  --db-path runtime/gateway/gateway.sqlite3
```

## Related docs

- Docs index: [README.md](docs/README.md)
- First run: [first-run.md](docs/first-run.md)
- FAQ: [faq.md](docs/faq.md)
- Troubleshooting: [troubleshooting.md](docs/troubleshooting.md)
- Web and terminal consoles: [console.md](docs/console.md)
- Architecture: [architecture.md](docs/architecture.md)
- Configuration (env vars + optional deps): [configuration.md](docs/configuration.md)
- Deployment: [deployment.md](docs/deployment.md)
- API overview: [api.md](docs/api.md)
- Security: [security.md](docs/security.md)
- Operator tooling (optional): [maintenance.md](docs/maintenance.md)

---

## docs/troubleshooting.md

# AbstractGateway — Troubleshooting

Each entry starts from a symptom you can see, then gives the likely causes,
how to confirm them, the fix, and where the full explanation lives. For
conceptual questions, see [faq.md](docs/faq.md).

Two commands answer most questions:

```bash
abstractgateway-config status          # data dir, auth mode, login service, running gateway
curl -sS http://127.0.0.1:8080/api/health
```

## Starting and signing in

### `serve` says "Refusing to start: no sign-in would protect this gateway"

**Cause.** `--host` points beyond this computer (for example `0.0.0.0`) and
the gateway was started with neither user accounts nor a token.

**Fix.** Choose the exposure with the network setting instead of `--host`;
user accounts are turned on for you:

```bash
abstractgateway network set lan        # or: internet --acknowledge-internet
abstractgateway serve
```

Or keep the gateway on this computer: `abstractgateway serve` (loopback) or
`--host 127.0.0.1`. See
[configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet).

### `serve` refuses a weak token

**Cause.** A shared `ABSTRACTGATEWAY_AUTH_TOKEN` shorter than 15 characters
or easy to guess, on a non-loopback bind or with public wildcard origins.

**Fix.** Use a long random token, or use user accounts
(`ABSTRACTGATEWAY_USER_AUTH=1`). See [security.md](docs/security.md).

### The one-time sign-in link does not work

**Checks and fixes.**

- A link works **once** and for **10 minutes**. Mint a new one with
  `abstractgateway claim --open`.
- It only works from a browser **on the gateway machine** (a loopback
  connection without proxy headers). From another computer, sign in with a
  user id and token instead.
- `abstractgateway claim` exits with code `2` when the running gateway uses a
  shared token without user accounts: that gateway cannot redeem links. Sign
  in with the token, or start the gateway with user accounts.

See [first-run.md](docs/first-run.md#3-get-a-new-sign-in-link).

### I lost the admin token

It is kept in `<data dir>/auth/bootstrap-admin-token` (mode `0600`).
`abstractgateway-config status` prints the data dir. For a loopback gateway
you can also sign in with `abstractgateway claim --open` and rotate tokens
from the console's **Users & Entities** tab.

### `401`, `403`, `429` or `413` from `/api/gateway/*`

| Status | Likely cause | Fix |
|---|---|---|
| `401` | missing or invalid `Authorization: Bearer <token>` or session | sign in again; check the token file |
| `401` with `user_accounts_off_admin_only` | a non-admin account while user accounts are off | turn user accounts on, or sign in as an admin ([security.md](docs/security.md)) |
| `403` (origin not allowed) | the browser page's origin is not in the allowlist | add it with `abstractgateway network set --allowed-origins https://…` |
| `403` on an admin route | the signed-in principal is not an admin | use an admin account |
| `429` | repeated failed sign-ins from the same client address (lockout) | wait for the backoff; check `trust_proxy` behind a proxy |
| `413` | the body exceeds `ABSTRACTGATEWAY_MAX_BODY_BYTES` (or the attachment / bundle limit) | send less, or raise the limit ([security.md](docs/security.md#limits-abuse-resistance)) |

## Network access

### A change of network mode "needs a restart"

A listening socket cannot move. After `abstractgateway network set …`, the
status reads `restart_required: true` until the gateway restarts:
`abstractgateway network restart`, the tray's **Restart AbstractGateway…**, or
stop and start `serve`.

If `network status` says the restart **cannot** apply the setting, the
running gateway was started with `--host/--port` (they win over the
setting), or its login item pins them: run `abstractgateway service enable`
once, then restart.

### `lan` or `internet` is refused (HTTP 409)

The `reason_code` says why:

- `user_auth_required`: the gateway runs without user accounts (a shared
  token only, or `ABSTRACTGATEWAY_USER_AUTH=0`).
- `auth_disabled`: it was started with authentication or read protection off
  (`ABSTRACTGATEWAY_SECURITY=0`, `ABSTRACTGATEWAY_PROTECT_WRITE=0`,
  `ABSTRACTGATEWAY_PROTECT_READ=0`).
- `acknowledgement_required`: `internet` needs
  `--acknowledge-internet` (or the confirmation in the console or tray).

Start the gateway without those variables (a plain `abstractgateway serve`)
and set the mode again. See [security.md](docs/security.md#network-exposure).

### Another computer cannot open the console

- Check `abstractgateway network status`: the mode must be `lan` or
  `internet`, applied (no restart pending).
- Use an address from `abstractgateway network addresses`; the machine's
  firewall must allow the port.
- The console accepts the gateway's own LAN origins discovered at start. An
  address that appeared later (another Wi-Fi network) needs a restart.
- Behind a reverse proxy or tunnel, add its public origin with
  `abstractgateway network set --allowed-origins https://your.host`.

## Workflows and runs

### `GET /api/gateway/bundles` returns no bundles

- `ABSTRACTGATEWAY_FLOWS_DIR` points at an empty directory. Unset it to serve
  the shipped bundles ([shipped-workflows.md](docs/shipped-workflows.md)), or
  upload a bundle with `POST /api/gateway/bundles/upload`.
- A bundle can be present but not served: the `skipped` array names it with
  the reason (for example a `min_runtime` floor or a compile error).

### A run stays RUNNING and nothing happens

- `GET /api/health` reports the runner (`runner.runners[].status`). With
  `serve --no-runner`, start `abstractgateway runner` on the same data dir.
- `StartRunResponse.runner_warning` is set when no runner is ticking the data
  dir.
- The gateway may be **paused** (`"paused": true` on `/api/health`, a banner
  in the console): resume it from the tray, the console, or
  `POST /api/gateway/host/resume`.

### "LLM nodes but no default provider/model is configured"

Configure the text route, for example:

```bash
abstractgateway-config set-default input.text \
  --provider lmstudio --model qwen/qwen3.5-9b --base-url http://127.0.0.1:1234/v1
```

Or pick a default model in the console (**Multimodal**, or **Use as default**
on a downloaded model in **Models**). See
[configuration.md](docs/configuration.md#capability-defaults).

### A run fails because a model's weights are missing

The error names the capability route that selected the model. Download it
from the console's **Models** tab or with
`abstractgateway models download <provider> <artifact>`, or choose another
default. See [model-downloads.md](docs/model-downloads.md).

### "LLM/tool execution requires AbstractCore integration", "Visual Agent nodes require AbstractAgent", or `memory_kg_*` nodes ask for AbstractMemory

These packages are part of the base install. Check the environment the
gateway runs in:

```bash
pip show abstractgateway AbstractRuntime abstractcore abstractagent AbstractMemory
```

For KG memory, keep the default `lancedb` backend; `sqlite` works only when
the installed AbstractMemory exposes `SQLiteTripleStore`.

### `/voice/tts` or `/audio/transcribe` answer "capability unavailable"

Configure the voice routes (`output.voice`, `input.voice`) in the console's
**Multimodal** tab, or the Gateway-scoped voice variables for a remote
backend (`ABSTRACTGATEWAY_VOICE_TTS_ENGINE`,
`ABSTRACTGATEWAY_VOICE_REMOTE_BASE_URL`, …). Local voice engines need the
`apple` or `gpu` extra. See [configuration.md](docs/configuration.md).

### Catalog routes return only `gateway_static` defaults

The request reached a gateway without the capability packages you expected,
often another `abstractgateway serve` still running from another
environment on the same port. Stop it and start the one from your current
environment.

## Engines, models and apps

### Install buttons are disabled or answer `403`

Installs run on the gateway machine, so they follow the
[`allow_engine_install`](docs/configuration.md#allow_engine_install) setting: on
by default for a loopback gateway and for someone at the gateway machine;
off by default for a browser on another computer. An admin can turn it on.
Installs also require an admin account.

### An engine install stops in `needs_admin` or `needs_tools`

This is expected when a step needs an administrator password or the Apple
command-line tools. Use **Continue with administrator password** or
**Install tools** in the console, or
`abstractgateway engines continue <job-id>`. On a headless machine, run the
command the job shows and press **Re-check**. See [engines.md](docs/engines.md).

### A download says "Stalled" or ends "failed"

- `stalled`: no bytes for 15 seconds; the job keeps trying and resumes by
  itself.
- `failed`: `ended_reason` says what happened (a dropped connection, a Hub
  error, a full disk, a gateway restart) and what a new download reuses.
  Start the download again.
- A parent job id (`grp_…`) answers `404` after a gateway restart; its
  children stay readable with `GET /api/gateway/jobs`.

See [model-downloads.md](docs/model-downloads.md).

### An app does not install or start

| Reason in the card or API | Fix |
|---|---|
| `network_unavailable` | the npm registry (or PyPI, for Node.js) is unreachable; installed apps keep working offline |
| `no_free_port` | free a port in the app's usual range or set `apps.ports` |
| `crash_loop` | open **Show log** (Technical details) or `abstractgateway apps logs <app>` |
| `installs_not_allowed` | see "Install buttons are disabled" above |
| `app_loopback_only` | the app listens on `127.0.0.1`; open it from the gateway machine |
| `started_outside_gateway` | the app was started elsewhere (dev stack, `npx`); stop it there |

See [apps.md](docs/apps.md).

### The backlog folder is "not available on this gateway"

The saved backlog folder no longer exists (a deleted or unmounted checkout).
Choose **Use the gateway's own folder** in Continuum or the console, or run
`abstractgateway config set triage_repo_root /path/to/checkout`. See
[configuration.md](docs/configuration.md#backlog-folder-exec-runner-and-process-manager-continuum).

## Desktop

### There is no tray icon

`serve` prints `Desktop tray: started (pid …)` or the reason it did not:

| Reason | Fix |
|---|---|
| `missing_dependency` | `pip install "abstractgateway[tray]"` (Linux also needs the GTK/AppIndicator bindings) |
| `headless` | no display (SSH, container, service); expected |
| `dev_reload` | start without `--reload` |
| `runner_only` | the tray belongs to the process that serves the console |

GNOME needs the AppIndicator extension. If the helper started and then
disappeared, read `<data dir>/logs/tray.log`; `GET /api/gateway/host/tray`
reports its exit code, and `POST /api/gateway/host/tray/show` (admin) starts
it again. See [tray.md](docs/tray.md).

### "Start at login" reads "needs repair" (`service status`: `broken`)

The registration points at a program that no longer exists (a moved or
reinstalled gateway), is unreadable or disabled, or pins `--host/--port` so
the Network setting cannot apply. Run:

```bash
abstractgateway service enable     # rewrite the registration for this gateway
abstractgateway service status
```

`other` means the login item belongs to another data folder. See
[first-run.md](docs/first-run.md#4-start-the-gateway-at-login-optional).

## Related docs

- [faq.md](docs/faq.md): conceptual questions and limits
- [first-run.md](docs/first-run.md), [getting-started.md](docs/getting-started.md)
- [configuration.md](docs/configuration.md), [security.md](docs/security.md)

---

## CHANGELOG.md

# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.4.3] - 2026-09-25

Requires AbstractCore 2.15.2 and AbstractRuntime 0.4.35 (installed
automatically). The terminal console is unchanged (`abstractgateway-console`
0.8.0).

### Fixed

- **Installing the Assistant works on a new Mac.** Installing the Assistant
  (and engines) into the gateway's Python failed with "File not found:
  …/Library/Application" whenever the data folder's path contained a space,
  as the macOS `Application Support` folder does. The gateway now pins its
  own packages to their current versions in the install command itself.
- Continuum, when the gateway (or the tray, for a global install) starts it,
  gets its port, bind address and gateway URL as launch flags (`--port`,
  `--host`, `--gateway-url`) instead of environment variables. Continuum
  0.3.1's settings file (`~/.abstractcontinuum/settings.json`) takes
  precedence over the environment, so a saved port, host or gateway URL
  could otherwise have replaced the ones the gateway chose. The other four
  apps still receive them in the environment.
- The web console page no longer carries the source code's maintainer
  comments (design notes, review references, dates); it is about 11% smaller
  (1.25 MB to 1.12 MB). The artifact search box shows `YYYY-MM-DD` as its
  date example.
- When the Models and Engines tabs cannot load, their card names AbstractCore
  2.15.1 as the version to install (it said 2.14.0, which lacks the cancel
  attribution the gateway uses).
- The documentation site no longer publishes the backlog: planning notes
  under `docs/backlog/` stay in the repository and are left out of the site
  build. The one API page that cited a backlog item links to it on GitHub.

### Changed

- Dependency floors: `abstractcore>=2.15.2` (also in the `embeddings` extra)
  and `AbstractRuntime>=0.4.35` (also in the `apple` and `gpu` extras).
  AbstractCore 2.15.2's default MLX model is a repository that exists on
  Hugging Face, so a first MLX download no longer fails.

## [0.4.2] - 2026-09-24

Requires AbstractCore 2.15.1 and AbstractRuntime 0.4.34 (installed
automatically). The terminal console is unchanged (`abstractgateway-console`
0.8.0).

### Added

- **The Assistant as an app card.** AbstractAssistant, the desktop menu-bar
  app, appears after the five browser apps (`kind: "desktop"`, id
  `assistant`). **Install** installs `abstractassistant` into the gateway's own
  Python as a job, with every `abstract*` package kept at its current version.
  **Open** (`POST /apps/assistant/launch`) starts it on the gateway's computer,
  or brings a running one to the front; from another computer the route
  answers 409 `not_on_gateway_machine`. `abstractgateway apps install|launch
  assistant` and the tray's **Install Assistant…** do the same. See
  [docs/apps.md](docs/apps.md#the-assistant-a-desktop-app).

### Changed

- **One Install button per app.** Install only installs; the card then shows
  **Open**, and **Open in Terminal** beside it when the terminal app is
  installed. For Code, when a prebuilt terminal app exists for the computer,
  Install installs the browser app and the terminal app as one job with two
  progress rows (`parts`); Cancel stops both, and a failed terminal part keeps
  the browser app. `POST /apps/{id}/install` accepts `with_terminal` (default
  `true`); app rows carry `kind` and `install_parts`. "Install terminal app"
  alone is under **Technical details**.
- **Tray:** **Install X…** runs the same install and no longer opens the app;
  a notification says when it is installed and the menu offers **Open X**.
- The first-run guide's Apps step is titled "Apps that work with this gateway".
- Dependency floors: `abstractcore>=2.15.1` (also in the `embeddings` extra)
  and `AbstractRuntime>=0.4.34` (also in the `apple` and `gpu` extras).

### Fixed

- The download card in the console keeps its file list open while progress
  updates, and **Cancel** asks for confirmation before stopping a download.
- A failed download shows why it ended (`ended_reason`: a dropped connection,
  a Hub error, a restart), and a cancelled one says who cancelled it and when.
  `POST /models/download/{id}/cancel` accepts `{"via": "console"}` and records
  the admin who asked. See [docs/model-downloads.md](docs/model-downloads.md).
- The model catalog's fit tooltip compares the model's needs with the usable
  memory used by the verdict.

## [0.4.1] - 2026-09-24

Requires AbstractCore 2.15.0 and AbstractRuntime 0.4.33 (installed
automatically). The terminal console ships as `abstractgateway-console` 0.8.0
(see `console-tui/CHANGELOG.md`). The `v0.4.0` tag was not published to PyPI;
0.4.1 is the first release with the changes below.

### Upgrade notes

- **Login service:** run `abstractgateway service enable` once on machines
  where the gateway starts at login, then restart it (`abstractgateway service
  install`, or log out and back in). Registrations now start plain
  `abstractgateway serve` so the Network setting applies; `service status`
  reports older registrations as `broken` / *needs repair*.
- **Settings instead of environment variables:** browser origins, trust proxy,
  the apps settings, the backlog folder and the exec runner are runtime
  settings. The matching environment variables still work as a start-time
  fallback (and, for origins and trust proxy, as a pin), and every surface says
  when one is in effect.
- **Default app ports** follow the framework stack map: Observer 3001,
  Continuum 3002, Code 3003, Entity 3004, Flow 3005.
- **User accounts off:** only admin accounts can sign in to the console and
  the browser apps; existing non-admin sessions are ended at their next use.

### Added

- **Network exposure** (`localhost`, `lan`, `internet`): one setting, changed
  from the console's new **Network** tab, the terminal console, the tray or
  `abstractgateway network status|show|set|addresses|restart`
  (`GET/POST /api/gateway/network`, contract `gateway_network_v1`). `lan` and
  `internet` require user accounts; `internet` also requires an explicit
  acknowledgement. Changes apply at the next start, and `serve --host/--port`
  override the setting. See
  [docs/configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet).
- **Reverse proxy settings:** `allowed_origins` and `trust_proxy`, changed
  from the console (Network → *Advanced: reverse proxy*), the terminal console
  or `abstractgateway network set --allowed-origins … --trust-proxy on|off`,
  applied to the next request without a restart.
- **Browser apps managed by the gateway:** Flow Editor, Code, Observer,
  Continuum and Entity can be installed, started, stopped, updated and opened
  signed in from the console's **Apps** tab, the first-run guide, the tray, the
  API (`/api/gateway/apps`) and `abstractgateway apps …`. The gateway installs
  Node.js when the machine has none, checks every download, supervises the
  apps, and detects apps started outside it. `POST /apps/{id}/open` accepts a
  `path` inside the app. See [docs/apps.md](docs/apps.md).
- **Apps settings** `apps.node`, `apps.ports`, `apps.host`,
  `apps.npm_registry`, `apps.pypi_url`: from the Apps tab, the terminal console
  or `abstractgateway apps config get|set`.
- **Code in the terminal:** "Open in Terminal" opens Code's terminal app on
  the gateway machine, signed in through a one-time code; `abstractgateway
  apps install-tui|tui-command code` do the same from a shell.
- **Engine installs without a terminal:** Ollama and LM Studio install from
  the vendors' signed apps on macOS, llama.cpp from prebuilt wheels, and a
  step that needs the Apple command-line tools or an administrator password
  pauses (`needs_tools`, `needs_admin`) until you continue through the
  operating system's own dialog. New `/api/gateway/engines/*` routes and
  `abstractgateway engines continue|cancel|start|stop`. See
  [docs/engines.md](docs/engines.md).
- **Model downloads with real progress:** bytes, speed, time left and per-file
  rows for Hugging Face, MLX, Ollama, LM Studio and Supertonic; a `stalled`
  state; "Use recommended defaults" as one parent job;
  `POST /models/download/{id}/cancel` and `GET /models/downloads/stream`
  (Server-Sent Events). See [docs/model-downloads.md](docs/model-downloads.md).
- **Model catalog as cards:** one card per model with all its builds, filters
  (search, 4-bit / 8-bit / other, provider, capability, status, fits this
  computer), Hugging Face search, and filters kept in the address.
- **Recommended text model per computer:** on a Mac, AbstractCore picks an MLX
  build by memory; the guide, "Use recommended defaults" and the tray follow
  that pick.
- **Backlog folder and exec runner settings:** a fresh gateway keeps its own
  backlog in `<data dir>/backlog/`; choose another folder with `abstractgateway
  config set triage_repo_root PATH`, the console or Continuum, or for one run
  with `serve --backlog-root PATH` and `--exec-runner on|off`.
  `abstractgateway config get|set|unset` change any runtime setting from a
  terminal. `GET /api/gateway/backlog/status` reports the folder.
- **Tray control centre:** start at login, apps, models (eject, load), network
  mode and addresses, and a console link that signs you in. See
  [docs/tray.md](docs/tray.md).
- **Login service:** `abstractgateway service enable|disable|status` (states
  `on`, `off`, `broken`, `other`), an XDG autostart entry on Linux without a
  systemd user manager, and `--pin-command-line` to keep `--host/--port` on the
  command line.
- **Console:** a full-page first-run guide, engine cards, app cards with one
  action row, a **Technical details** switch, and the header widgets of the
  AbstractFramework UI kit. The create-user dialog follows the user-accounts
  mode.
- `POST /api/gateway/session/claim` reports who minted the link
  (`claim.created_by`: `serve`, `cli` or `tray`).
- `GET /api/gateway/models/installed` rows carry `kind`, `tasks` and
  `tasks_source`.
- `abstractgateway --version`.

### Changed

- Windows login item: a per-user `HKCU\…\Run` value replaces the Startup
  folder shortcut (the shortcut is removed on install).
- Someone at the gateway machine may install engines and apps by default,
  whatever address the gateway listens on; remote callers still need
  `allow_engine_install`.
- Messages that used to ask for an environment variable now name the setting
  or command to use.
- `serve` prints the admin token again on a loopback bind; `serve
  --print-token` / `--no-print-token` control it.

### Fixed

- The saved backlog folder is used by every backlog, report, triage and
  process route and by the exec runner; a folder that disappears answers
  `404` with the reason.
- Model downloads keep working after an MLX model is loaded, and a restart
  from the tray or console no longer carries in-process Hugging Face offline
  flags into the new process.
- A leftover browser app from a gateway that died is stopped on Linux.
- No false "PyTorch was imported" GGUF warning on hosts without Apple silicon
  or llama-cpp-python.
- Engine installer downloads use the per-OS user cache directory.
- `abstractgateway network … --data-dir DIR` uses `DIR`.
- `service uninstall` on Linux works when no unit file exists.
- The consoles explain in words why MTP did not run.

### Security

- With user accounts off, a non-admin account can no longer sign in and change
  the operator's settings (`401 user_accounts_off_admin_only`); creating such
  an account answers `409`.
- The last enabled admin account cannot be deleted, disabled or demoted
  (`409 last_admin`).
- `POST /bundles/{id}/deprecate` and `/undeprecate` apply the shared-registry
  ownership check.
- `lan` and `internet` are refused when the gateway was started with read
  protection off (`ABSTRACTGATEWAY_PROTECT_READ=0`).

## [0.3.0] - 2026-09-23

This release requires AbstractRuntime 0.4.33 and AbstractCore 2.14.0
(installed automatically).

### Added
- **Models and Engines tabs in the web console.** Browse models that fit this
  machine, download or delete them, and see and install local engines
  (Ollama, LM Studio, MLX, llama.cpp, Hugging Face). These are AbstractCore's
  own screens, embedded in the gateway, so the gateway and
  `abstractcore serve` show the same data and the same actions. Download,
  delete and install are admin-only; an install first shows the exact command
  it will run on the gateway host. See [docs/console.md](docs/console.md).
- **The first-run guide uses them.** The engines step lists the real engines
  on this machine with an install button. The model step lists models that
  fit, downloads one, and sets an installed model as the default text model.
- **Routes** (same bodies and payloads as AbstractCore's `/acore/*`):
  `GET /api/gateway/host/profile`, `GET /api/gateway/engines`,
  `GET /api/gateway/engines/{id}`, `POST /api/gateway/engines/{id}/install`,
  `GET /api/gateway/models/catalog`, `GET /api/gateway/models/installed`,
  `POST /api/gateway/models/delete`, `GET /api/gateway/jobs`,
  `GET /api/gateway/jobs/{id}` and `POST /api/gateway/jobs/{id}/cancel`.
  Every POST is admin-only and in the audit log. An AbstractCore older than
  2.14.0 answers 501 with the upgrade command instead of failing.
  See [docs/api.md](docs/api.md#models-and-engines).
- **Commands:** `abstractgateway models list|catalog|search|download|delete|jobs|cancel`
  and `abstractgateway engines status|install|open`, with the same arguments
  and exit codes as `abstractcore models|engines` (0 ok, 1 error, 2 refused).
  They call the running gateway; `--local` runs them in-process instead.
  Job cards in the consoles show these commands.
- **`allow_engine_install`** (runtime config): engine installs from the
  console or API run on the gateway host, so they are on by default only for
  a gateway bound to loopback. Dry runs are always allowed. See
  [docs/configuration.md](docs/configuration.md#allow_engine_install).
- `abstractgateway claim` and `abstractgateway-config claim-url` accept
  `--base-url` as another name for `--url` (the bootstrap installers use it).
- **console-tui (crate `abstractgateway-console` 0.7.0, versioned separately):**
  the terminal console gains screens 9 **Models** and 0 **Engines**, which are
  AbstractCore's shared screens from the `abstractcore-console` crate mounted
  over the gateway's `/api/gateway/models/*`, `/engines/*`, `/host/profile`
  and `/jobs/*` routes. See
  [console-tui/CHANGELOG.md](console-tui/CHANGELOG.md).
- **Zero-configuration first run.** With no auth configured, `abstractgateway serve`
  binds `127.0.0.1`, enables user auth, creates `default/admin`, and prints a
  one-time console sign-in link instead of a token. See
  [docs/first-run.md](docs/first-run.md).
- **One-time sign-in links:** `abstractgateway claim [--open]` and
  `abstractgateway-config claim-url [--open]` mint a single-use, 10-minute link
  (`/console#claim=<code>`); `POST /api/gateway/session/claim` redeems it for an
  admin browser session from a loopback peer only.
- **First-run guide in the web console** (host summary, local engines, default
  model with recommended downloads, apps, CLI equivalents), opened once per
  data folder and reachable later from the **Setup** button.
  `GET /api/gateway/host/first-run` and `POST` (admin) hold its state.
- **`abstractgateway service install|uninstall|status`**: start the gateway at
  login as a macOS LaunchAgent, a Linux systemd user unit, or (experimental) a
  Windows Startup shortcut, with `--dry-run`, free-port selection and a
  persisted port.
- `serve --data-dir`.
- `abstractgateway-config status --json` gains `schema`
  (`gateway_config_status_v1`), `data_dir_source`, `data_dir_reason`,
  `auth_mode`, `auth`, `service`, `claim_pending`, `claims`, `first_run` and
  `serve`; `GET /api/gateway/host/state` gains a `gateway` block with the same
  facts.

### Changed
- **Model downloads run in AbstractCore's job registry.** `POST /models/download`
  and `GET /models/download/{job}` keep their `{ok, job}` envelope and
  behaviour (a queued job reads `running`, a duplicate request joins the
  running job), and the job is also readable at `GET /api/gateway/jobs/{id}`.
  The job now carries AbstractCore's fields as well (`schema`, `job_id`,
  `kind`, `log_tail`, `command`, `cli_equivalent`); `started_at` is an
  ISO-8601 time instead of a Unix timestamp. Jobs started by the
  `abstractcore` CLI on the same machine appear in the job list.
- **Default data folder.** When `ABSTRACTGATEWAY_DATA_DIR` is unset, the gateway
  uses `./runtime` only if it already exists in the working directory, and
  otherwise the per-user data folder (macOS
  `~/Library/Application Support/AbstractGateway`, Linux
  `$XDG_DATA_HOME/abstractgateway`, Windows `%LOCALAPPDATA%\AbstractGateway`).
  The `triage-reports`, `triage-apply`, `backlog-exec-runner` and `data list`
  commands use the same default as `serve` (they previously defaulted to
  `./runtime/gateway`). Set `ABSTRACTGATEWAY_DATA_DIR` to keep any other layout.
- **`serve --host` default.** `127.0.0.1` when no auth setting is present;
  `0.0.0.0` (unchanged) when any auth setting is present.
- **The bootstrap admin token is no longer printed** on loopback starts; it
  stays in `<data dir>/auth/bootstrap-admin-token`. Set
  `ABSTRACTGATEWAY_BOOTSTRAP_PRINT_TOKEN=1` to print it.
- **Windows:** the runner's singleton lock uses `msvcrt.locking`, so two
  gateways on one data folder no longer both run workflows.

## [0.2.30] - 2026-09-23

This release requires AbstractRuntime 0.4.32, AbstractAgent 0.3.13 and
AbstractMemory 0.3.0 (installed automatically). It also folds in the
`[0.2.29]` changes below, which were never published separately.

### Added
- **Stop kill switch.** A `cancel` command (the Stop button) cancels the run
  tree and stops the model call that is executing. If a call of the cancelled
  tree is still running after `stop_kill_switch_s` seconds (runtime config key,
  or `ABSTRACTGATEWAY_STOP_KILL_SWITCH_S`; default `10`, `0` disables), the
  gateway kills that inference in process. The gateway process, other runs and
  the HTTP API keep working. Stopped calls are recorded as `cancelled` ledger
  steps with `cancelled_by` / `killed_by`. See
  [docs/configuration.md](docs/configuration.md#stop-and-the-kill-switch).
- **`abstractgateway models loaded|load|unload`.** List, warm and eject models
  on a running gateway from a shell, through the same routes the consoles use
  (`--url`, `--token`, `--provider`, `--model`, `--force` for a locked model).
- **MTP (speculative decoding) controls.** `speculation` is accepted on
  `/runs/start`, `/runs/schedule`, `/sandbox/generate` and as
  `_runtime.speculation` (`false` = Off, a native-MTP object selects a depth).
  The web console and the console TUI edit the Core-owned default
  (`options.speculation` on the text route) with an MTP selector.
- **Desktop tray icon for `abstractgateway serve`** (install the `tray` extra):
  open the console, pause/resume workflows, unload models, and watch memory,
  GPU and recent runs. See [docs/tray.md](docs/tray.md).
- **Pause / resume execution** (`POST /api/gateway/host/pause|resume`, admin;
  `GET /host/runner`). A paused runner still applies commands, so Stop works.
- **Restart and self-update** from the tray or console (`POST /host/restart`,
  `GET /host/update`, `POST /host/update/check|start`), aware of pip, uv, pipx,
  editable and Docker installs.
- **Host views:** `GET /host/metrics/live` (GPU, memory and execution state in
  one call) and `GET /host/runs` (recent runs across every data plane, admin).
- **Workflows tab in both consoles** listing every registered workflow with its
  versions and entrypoints, plus import, export
  (`GET /api/gateway/bundles/{bundle_id}/download`) and delete. Versions that
  cannot be served are listed in `skipped` with the reason.
- **Out-of-the-box workflows.** A fresh install serves `basic-agent`,
  `coding-agent` (`coder` entrypoint), `deep-research`, `co-scientist`,
  `docs-qa`, and the `react-agent` / `codeact-agent` / `memact-agent` native
  loops. See [docs/shipped-workflows.md](docs/shipped-workflows.md).
- **Durable session replay.** `use_session_history` seeds a run's
  `context.messages` from the session's prior turns (with a message cap), and
  `GET` history-bundle / session-bloc endpoints serve replayable transcripts.
- **Summoned entities.** Persistent entities with their own homes, identity,
  memory and lifecycle: `abstractgateway entity create|list|inspect|verify|chat`
  and `/api/gateway/entities/*` (summon with a queue, chat, visits,
  sleep/wake/pause, diary, skills, voice, task inbox, tool policy). See
  [docs/entities.md](docs/entities.md).
- **Run-level skills selection** and skills/MCP inventories for launch surfaces.
- **One seam for AbstractCore-owned configuration** (`core_config.py`); the
  text reasoning effort is editable from the Gateway.
- `inject_guidance` runner command, durable `emit_event` delivery
  (`payload.durable: true`), and a declared environment-variable registry.

### Changed
- **One gateway-owned workspace per session**, not per run (HTTP API and
  Telegram bridge). The system prompt stays byte-stable across turns, so
  prompt caches are reused.
- **Skills selection never widens an explicit tool ceiling.** If a run passes
  `_runtime.allowed_tools`, include `read_skill` yourself when you want the
  skill tool available.
- **Cancellation, turn grounding and agent loops follow AbstractRuntime 0.4.32
  and AbstractAgent 0.3.13:** cancelled ledger steps have status `cancelled`;
  stored user turns may start with a `<runtime_metadata>` grounding envelope;
  tool loops append messages marked `_af_synthetic`. Clients that render
  transcripts should handle all three.
- The fresh-install capability seed belongs to the install (it is not re-applied
  on every boot), and the capability-defaults read reports its provenance.
- `dp-*` workflow ids are renamed `deep-*`.
- The tray menu has a Workflows section; the `desktop_tray` setting was removed
  (the icon is present whenever `serve` runs on a desktop).

### Fixed
- The ledger stream's `event: done` follows the run's terminal save instead of
  an idle timer, and the runner wakes on events instead of polling, so a
  no-tool chat turn finishes as soon as its answer is saved.
- The shipped `basic-agent` bundle (0.0.5) no longer waits 3 s after answering.
- Workflow publish, promote, upload and reload no longer block health checks.
- `POST /prompt_cache/prepare_modules` forwards `thinking`.
- Runs of catalog-published workflows are listed normally.
- An event-entry flow no longer gets a second derived listener (no duplicate
  messages or tool calls).
- A non-object client `context` is kept as sent.
- A configured `ABSTRACTGATEWAY_BACKLOG_CODEX_BIN` counts as an available
  executor.
- Idle file-store deployments no longer burn CPU, and valid credentials are no
  longer caught by the auth lockout.
- Gateway writes of Core-owned configuration keep the fields they did not name.

### Security
- Writes to the shared workflow registry (upload, delete, reload, deprecate,
  publish) require an admin principal; per-user registries are unchanged.
- `POST /models/download` and `POST /config/capability-defaults/apply-recommended`
  require an admin principal.

## [0.2.29] - 2026-08-27

Never published separately; these changes ship in 0.2.30.

### Added
- **`GET /api/gateway/host/state` — one-call host snapshot.** Memory, GPU,
  resident models, and session prompt caches, plus byte totals, in a single
  authenticated read. Every section is independently best-effort: a missing
  facade method or a failed probe nulls that section and names it in
  `degraded` (with a `reasons` map saying why) instead of failing the
  snapshot; the route never returns a 500. `totals.models_resident`
  (additive) counts only rows with `resident: true` so every client can show
  a truthful "N loaded" — `totals.models` counts every known row,
  configured / cached included, and must not be presented as "loaded".
- **`GET /api/gateway/host/metrics/memory`.** Host RAM/process/device memory
  snapshot relayed from the Runtime host facade, with the same
  `supported: false` degraded style as `GET /host/metrics/gpu`. The snapshot
  exposes both `process.rss_bytes` and `device.allocated_bytes`;
  `device.allocated_bytes` is the signal that verifies an in-process unload
  freed device memory, since freed buffers can keep process RSS unchanged.
- **Frozen `model_residency_row_v1` row schema.** `GET /models/loaded` now
  also returns `rows` — normalized records (`runtime_id`, `task`,
  `provider`, `model`, `source`, `resident`, `state`, `pinned`, `default`,
  `size_bytes`, `size_vram_bytes`, `expires_at`, `context_length`,
  `loaded_at`, `last_used_at`, `locked`, `lockable`, `modalities`,
  `calibrated_context_length`, `context_calibrated`, `host_id`, `host_name`,
  `details`) — and `row_schema`, alongside the unchanged raw `models`
  records. Residency truth is provider-first:
  `provider_resident`/`provider_loaded` outrank runtime lease booleans, state
  strings can confirm residency but never deny it, and unknown values stay
  `null`. The schema is additive-tolerant: fields beyond the original 16 are
  optional and `null` when the runtime does not report them. Rows and the
  `GET /host/state` snapshot (its optional top-level `host` block) carry a
  host identity as the aggregation seam for a proposed multi-machine model
  resource pool
  ([backlog 0093](docs/backlog/proposed/0093_multi_machine_model_resource_pool.md)).
- **Model residency locks.** Admin-only `POST /api/gateway/models/lock` and
  `POST /api/gateway/models/unlock` pin a resident model against unload and
  release that pin, selecting the target like unload does (`runtime_id` or
  `provider`+`model`). Lock requires provider-verified residency: a
  configured or merely-warm model refuses with an
  `error: "model_not_resident"` payload (load with `lock: true` instead),
  and unlock always works — even for a since-evicted model — so locks are
  never stranded. `POST /models/unload` answers **HTTP 409** with
  the normalized `model_locked` refusal payload when the target is locked,
  and the unload request gains `"force": true` to unload anyway; every other
  unload outcome stays in-band at 200. Rows report `locked`/`lockable` so
  clients can render lock state and offer the right verb.
- **`GET /api/gateway/models/context_estimate`.** Context/KV memory estimate
  for a `provider`+`model` (optional `context_length` >= 1), relayed from the
  Runtime host facade with in-band `confidence` (`calibrated` | `estimated` |
  `unknown`) and fields such as `predicted_max_context` (the context that
  fits beside the weights), the tri-state `fits_weights` /
  `fits_requested_context` split, and `budget_bytes` (real-ceiling budget;
  basis and reserve stated in `notes`). Advisory only — no load path gates
  on it. Available to any
  authenticated principal; degrades at 200 with
  `code="context_estimate_unavailable"`/`"context_estimate_error"` like the
  other host relays.
- **A Resources surface in both consoles.** The web console gains a
  `Resources` tab and the console-TUI a `Resources` screen (8): memory/GPU
  meters with
  degradation notes, the resident-model table (modality chips/labels from
  the shared `modality_ui` palette, tri-state residency, lock state, context
  facts with calibration), and session prompt caches with per-session clear.
  The web table defaults to provider-verified RESIDENT rows only — the
  section header counts resident rows, and configured / cached rows
  (labeled "configured — not in memory", Estimate only, no Unload/Lock)
  appear behind a "Show configured / cached (N)" toggle; the TUI totals line
  counts resident rows apart from the row total. Default ≠ loaded: a
  configured capability default is never presented as loaded.
  Admins additionally get warm-up (with an optional lock-after-load and a
  live context-estimate hint), lock/unlock, and unload — a locked model's
  409 refusal triggers an explicit force-unload confirmation instead of a
  dead end. Reads render for every authenticated user; mutation controls are
  admin-gated. The web tab polls `/host/state` every 5s while active
  (stale responses are discarded), the TUI every 4s while the screen is
  active.
- **Session prompt-cache enumeration lane.**
  `GET /api/gateway/sessions/prompt_cache?session_id=` lists the prompt
  caches the runtime actually minted, with session/run/workflow/node
  attribution, and admin-only
  `POST /api/gateway/sessions/{session_id}/prompt_cache/clear_all` unloads
  every cache for a session in one call. This lane is recommended over the
  identity-derived per-session lifecycle endpoints, which are unchanged.
- **Discovery contract additions.** `capabilities.contracts.common` gains
  `host_state` and `session_caches` descriptors, and the `model_residency`
  descriptor now names its `row_schema`, lists the `lock`/`unlock`/
  `context_estimate` endpoints, and carries `modality_ui` — the canonical
  modality color map (`{version: 1, colors: {...}}`, one `{color, label}`
  entry per residency task plus an `unknown` fallback) every residency
  client renders with instead of hardcoding its own palette. `modality_ui`
  is a rendering contract and is served even when the runtime facade is
  absent.

### Changed
- **Host and residency reads are user-level.** `GET /models/loaded`,
  `GET /models/context_estimate`, `GET /host/state`, `GET /host/metrics/*`,
  and `GET /sessions/prompt_cache` serve any authenticated principal.
  Mutations — `POST /models/load|unload|lock|unlock|download` and every
  prompt-cache mutation, including the new `clear_all` — remain admin-only,
  and anonymous requests are still rejected.
- Raised the AbstractRuntime dependency floor to `AbstractRuntime>=0.4.31`
  across the base, `apple`, and `gpu` profiles; that release provides the
  host facade methods (memory snapshot, session-cache enumeration) these
  endpoints relay.

## [0.2.28] - 2026-06-14

### Changed
- Raised the Gateway dependency floors to `AbstractRuntime>=0.4.29`, `abstractagent>=0.3.12`, and `abstractcore[embeddings]>=2.13.38` across the base and hardware profiles so published installs consume the released Runtime/Core/Agent contract from this wave.
- Release packaging now ships only the supported Gateway bundles `basic-agent.flow` and `abstractassistant-orchestrator@0.0.0.flow`; local draft bundles under `flows/bundles/` are ignored by default and no longer ride along into sdists, wheels, or Docker source copies.

## [0.2.27] - 2026-06-06

### Added
- Added `POST /api/gateway/runs/{run_id}/images/upscale`, backed by Runtime's durable `AbstractCoreRunFacade.upscale_image(...)` child-run path.
- Added `upscaled_image` media capability/readiness contract entries and `task=image_upscale` Vision provider-model discovery.
- Added `GET /api/gateway/vision/adapters`, backed by Runtime's public discovery facade, so thin clients can query compatible installed adapters for image/video tasks.
- Direct image/video routes now return plural artifact fields (`image_artifacts`, `video_artifacts`) for batch generation while preserving the existing singular compatibility fields.

### Changed
- Raised the Runtime floor to `AbstractRuntime>=0.4.28` across Gateway base, Apple, and GPU profiles so Gateway installs always include the Runtime `read_pdf` / `write_pdf` nodes and their permissive `pypdf` / `reportlab` dependencies.
- Forwarded newer Runtime/Core/Vision request controls such as image/video batch `count` / `n`, `seeds`, ordered `lora_adapters`, video `flow_shift`, and image-upscaler parameters through Gateway direct media routes.
- Raised the `abstractcore[embeddings]` optional profile floor to `>=2.13.37`, matching Runtime's Core floor used by the base, Apple, and GPU Gateway profiles.

### Fixed
- Added Gateway bundle execution coverage for writing a real PDF artifact, reading it back through Runtime's PDF node, and exposing the extracted text through `On Flow End`.
- Bundle-mode VisualFlow execution preserves Runtime structured LLM `data` outputs through data edges and Break Object while leaving `response` as text.
- Bundle-mode structured LLM outputs can now drive `Answer User` and `Switch` nodes through `Break Object` without dropping the parsed data payload.
- Gateway now reuses Runtime's published workspace-path and file-filter helpers, and the published package/HTTP app versions are aligned to `0.2.27` while the base/Apple/GPU dependency floor for `abstractagent` stays on the latest PyPI release line.
- Gateway provider/model resolution now falls back to the service store base directory when embedded hosts expose stores without a full host config object, keeping backlog-assist and other hosted endpoints usable in lightweight service contexts.

## [0.2.26] - 2026-06-03

### Added
- `abstractgateway serve` now auto-ensures the `default/admin` Gateway user and writes the bootstrap browser-login token when user auth is enabled, matching the Docker first-run path for native pip installs.
- Added runtime-scoped Core config storage for Gateway capability defaults:
  Gateway baseline defaults live in `<ABSTRACTGATEWAY_DATA_DIR>/config/abstractcore.json`
  and user runtime overrides live in
  `<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant>/<runtime>/runtime/config/abstractcore.json`.

### Changed
- Gateway Console now presents provider endpoint profiles as provider connections for OpenAI, Anthropic, OpenRouter, Portkey, LM Studio, Ollama, and custom OpenAI-compatible endpoints, with clearer endpoint/key hints and model discovery.
- Gateway configuration docs now distinguish browser user tokens from the legacy server/operator `ABSTRACTGATEWAY_AUTH_TOKEN`.

### Removed
- BREAKING: removed legacy Gateway `config/capability_defaults.json` overlay support. Gateway capability defaults now use only scoped Core config files (`config/abstractcore.json`). Existing overlay files are ignored; recreate those defaults with `abstractgateway-config set-default ...`.

## [0.2.25] - 2026-05-31

### Changed
- Set Gateway container defaults for host-native LM Studio and Ollama endpoints so named provider discovery does not default to `localhost` inside the container.
- Updated Docker deployment docs to use `LMSTUDIO_BASE_URL` for LM Studio and `OPENAI_BASE_URL` for generic OpenAI-compatible endpoints.

### Fixed
- Fixed Gateway Console capability-default model discovery so the Base URL field is forwarded to the provider model catalog before saving.
- Fixed Docker Compose/OpenAI-compatible documentation drift where `OPENAI_COMPATIBLE_BASE_URL` was shown as the primary AbstractCore discovery variable even though AbstractCore uses `OPENAI_BASE_URL`.

## [0.2.24] - 2026-05-31

### Added
- Added `abstractgateway-config bootstrap-admin` to create or recover a file-backed `default/admin` Gateway user for hosted/container user-auth deployments.
- Added a Gateway Docker entrypoint that bootstraps the admin user token into `/data/auth/bootstrap-admin-token` before starting the server.
- Added first-class GHCR tags for `ghcr.io/lpalbou/abstractgateway:<version>`, `latest`, `<version>-gpu`, and `gpu-latest`, while preserving the legacy `abstractgateway-server` tags during transition.

### Changed
- Gateway Docker and Compose defaults now use `/data`, enable hosted user auth, and build release images from the just-published PyPI wheel instead of local source.
- Gateway startup now accepts hosted user-auth deployments without the legacy shared `ABSTRACTGATEWAY_AUTH_TOKEN`.

### Fixed
- Fixed the PyPI/GHCR release path so container images can start cleanly from the published Gateway wheel and still provide an initial admin login token.

## [0.2.23] - 2026-05-31

### Fixed
- Fixed local-source Gateway container builds so the packaged `basic-agent` workflow bundle is present when Hatch builds the wheel inside the release image.

## [0.2.22] - 2026-05-31

### Added
- Added hosted user-principal auth with `GET /api/gateway/me`, admin-only `/api/gateway/admin/users` CRUD, and a file-backed user registry storing bearer-token hashes.
- Added request-scoped Gateway service routing so hosted user-auth mode maps each principal to a separate GatewayService data plane under `<DATA_DIR>/users/<tenant_id>/<runtime_id>/`.
- Added the built-in Gateway Console at `/console` for browser-session sign-in, account/runtime summary, admin user management, token rotation, and per-principal capability default editing.
- Added per-principal capability-default overlays in hosted user-auth mode so users can set provider/model defaults for their own runtime without mutating the global AbstractCore config.
- Added provider endpoint profiles for Gateway-stored OpenAI-compatible or hosted endpoints. Profiles keep API keys server-side, discover endpoint models on demand, and surface as virtual providers in Gateway defaults and Flow node selectors.

### Changed
- Raised dependency floors to `AbstractRuntime>=0.4.26`, `abstractagent>=0.3.10`, and `abstractcore[embeddings]>=2.13.31` so Gateway installs inherit the latest light-profile, media, and provider-profile contracts.

### Fixed
- Fixed the Gateway Console sign-in page so generated inline JavaScript parses correctly, the sign-in form posts to `/api/gateway/session/login`, and signed-out users see only the same-origin Gateway user/token login card.
- Made `abstractgateway.security` export session and middleware helpers lazily so direct `abstractgateway.users` imports are not order-sensitive.
- Kept the base `pip install abstractgateway` remote-light on Linux while relying on the base `AbstractRuntime` install for MCP and remote multimodal routing. Local sentence-transformer embeddings moved behind `abstractgateway[embeddings]`, and Gateway no longer declares direct base `sentence-transformers` or `numpy` dependencies, avoiding PyTorch/NVIDIA CUDA runtime wheels unless an explicit local-engine profile is selected.
- Kept remote/provider-backed embeddings in the base light profile through `embedding.text` routes and remote AbstractCore delegation, while surfacing embedding setup errors instead of reporting a generic missing integration.
- Gateway admin user routes now fail closed when request principal context is absent while Gateway security is enabled.
- Gateway route-family authorization now keeps operator/admin surfaces and server-workspace file helpers admin-only in hosted user-auth mode while regular users remain able to operate within their own runtime data plane.

## [0.2.21] - 2026-05-29

### Added
- Gateway artifact search/import/export endpoints for thin clients, including scoped artifact lookup by run, session, or all stored artifacts with modality, content type, text, and tag filters.
- Capability discovery now advertises artifact search, workspace import, and workspace export descriptors in the shared thin-client contract.

### Changed

- Removed legacy compatibility install extras (`abstractgateway[http]`, `[server]`, `[multimodal]`, `[memory]`, `[voice]`, `[vision]`, `[telegram]`, `[visualflow]`, `[all]`, `[all-apple]`, `[all-gpu]`, `[server-nvidia]`). The supported install surface is now:
  - `pip install abstractgateway`
  - `pip install "abstractgateway[apple]"`
  - `pip install "abstractgateway[gpu]"`
- Raised dependency floors to `AbstractRuntime[multimodal,mcp-worker]>=0.4.25` and `abstractagent>=0.3.9`.
- KG memory readiness now treats a resolvable fresh persistent AbstractMemory store as available, so empty stores return empty query results instead of hiding Flow authoring surfaces.

### Fixed
- Media model-residency discovery now keeps image editing distinct from image generation when Runtime/Core expose task-specific residency state.

## [0.2.20] - 2026-05-26

### Added
- Direct Runtime-backed video generation routes:
  - `POST /api/gateway/runs/{run_id}/videos/generate` for text-to-video
  - `POST /api/gateway/runs/{run_id}/videos/from_image` for image-to-video
- Thin-client capability contracts and readiness metadata now advertise `generated_video` and `image_to_video`, including `provider_models_task` values and `abstract.progress` child-run progress events.
- Model-residency capability reporting now includes video tasks (`text_to_video`, `image_to_video`, and `video_generation`) when Runtime/Core expose them.

### Changed
- Raised the Runtime floor to `AbstractRuntime[multimodal,mcp-worker]>=0.4.24`.
- Gateway documentation now describes direct video routes, video provider/model catalog tasks, and progress-event handling for long-running media jobs.

## [0.2.19] - 2026-05-26

### Added
- Gateway capability-default routing and configuration helpers so downstream thin clients can discover provider/model defaults without hardcoded fallbacks.
- Run-retention cleanup support for draft and ephemeral Flow runs.

### Changed
- Raised dependency floors to `AbstractRuntime[multimodal,mcp-worker]>=0.4.23` and `abstractagent>=0.3.8`.
- Refined Gateway model-residency and catalog proxy responses around Runtime/Core discovery truth, including the latest MLX-Gen vision and OmniVoice catalog surfaces.
- Refreshed Docker and deployment docs for the new release image tags.

### Fixed
- Removed brittle catalog payload assertions by normalizing Gateway-owned catalog envelopes at the route boundary.

## [0.2.18] - 2026-05-23

### Added
- Catalog and provider discovery routes now include a stable Gateway-owned envelope (`catalog.contract=gateway_catalog_v1`, `catalog.version=1`) plus one canonical `items` array, while preserving legacy lower-layer fields for compatibility.
- Capability discovery now also exposes `common.readiness` (`gateway_surface_readiness_v1`): a compact surface-level summary derived from endpoint descriptors, memory readiness, prompt-cache, media gates, and Runtime/Core truth.

### Changed
- Raised the Runtime floor to `AbstractRuntime[multimodal,mcp-worker]>=0.4.22`.
- Removed VisualFlow directory mode and fully removed the `abstractflow` package dependency from Gateway. VisualFlow JSON is stored/published via Gateway endpoints and executed as `.flow` WorkflowBundles (bundle mode).

## [0.2.17] - 2026-05-22

### Added
- Gateway now exposes Runtime-backed image editing for thin clients through `POST /api/gateway/runs/{run_id}/images/edit`.

### Changed
- Raised the Runtime floor to `AbstractRuntime[multimodal,mcp-worker]>=0.4.21`.
- Gateway capability discovery and thin-client contracts now advertise edited-image and generated-music availability, richer voice `tts|stt|listen` contracts, and Runtime-backed model residency truth instead of hard-coded media support flags.
- Direct STT now forwards `prompt`, `response_format`, `temperature`, and source `format` hints through the Runtime transcription surface.
- Release-facing docs now describe the current higher-app surface more precisely, including the stable route/contract layer and the current best-effort catalog payload limitation.

## [0.2.16] - 2026-05-21

### Changed
- Raised the Runtime floor to `AbstractRuntime[multimodal,mcp-worker]>=0.4.20` across the base, Apple, and GPU install profiles.
- Gateway's legacy prompt-cache snapshot aliases, `GET /api/gateway/prompt_cache/saved` and `POST /api/gateway/prompt_cache/save|load`, now delegate to Runtime's public host facade instead of using provider-private prompt-cache state directly.
- Local bundle runtimes now keep host-local prompt-cache exports under `<DATA_DIR>/prompt_cache_exports` through Runtime's export root policy.

### Fixed
- Removed the last Gateway-side prompt-cache boundary bypass (`runtime._abstractcore_llm_client`, direct provider-instance access, and provider-private `_prompt_cache_store` / GGUF cache hooks) from the public route surface.
- Removed the stale internal Core catalog proxy module after discovery routing fully moved to Runtime's public discovery facade.

## [0.2.15] - 2026-05-21

### Added
- Added Runtime-backed durable bloc prompt-cache control-plane routes under `/api/gateway/blocs/*`, including KV manifest/list/ensure/load/delete/prune helpers for exact-reuse workflows.
- Added Gateway-owned workspace file helper support plus focused route and contract coverage for durable blocs, model residency, notifier behavior, and Runtime-backed capability discovery.

### Changed
- Raised the Runtime floor to `AbstractRuntime[multimodal,mcp-worker]>=0.4.19` and moved Gateway's public provider/media/tool boundary behind Runtime facades rather than direct package imports.
- Updated Apple/GPU install profiles to cascade through Runtime's aggregate extras and excluded internal `tests/`, `flows/`, and backlog notes from source distributions.
- Expanded the docs and capability contract to cover durable blocs, media/model residency, Runtime-backed email/Telegram helpers, and the current Docker/runtime dependency shape.

### Fixed
- Gateway no longer reads AbstractCore config for LLM helper defaults; provider/model resolution now follows request values, Gateway env, and flow defaults with a clear config error when unset.
- Gateway's operator email, Telegram, and notification paths now use Runtime's AbstractCore host facades, while local file/workspace helpers stay owned by Gateway.
- Capability discovery and prompt-cache readiness reporting now better reflect the actual state of generated-media, voice/audio, and provider-backed cache controls.

## [0.2.14] - 2026-05-19

### Fixed
- Gateway now carries explicit modern OpenAI/httpx/anyio dependency bounds in its base install metadata, preventing Python 3.10 resolver backtracking while preserving the Apple/GPU profile cascade into `[all-apple]` and `[all-gpu]` framework dependencies.

### Changed
- Raised the Runtime floor to `AbstractRuntime>=0.4.14` so Gateway profiles consume Runtime's resolver bounds for AbstractCore provider/tool extras.

## [0.2.13] - 2026-05-19

### Fixed
- Gateway's base install now avoids mixing Core's narrow base media/embeddings extras with Core `[all-apple]` and `[all-gpu]` profile dependencies, while still installing the media, compression, and embeddings dependency set needed by the remote-capable base package.
- Gateway's base media dependency set now uses a Python-3.10-compatible `unstructured` line and bounds `python-pptx` to supported modern releases so document-capable installs do not backtrack into broken legacy setup packages.
- Gateway's base web dependency set now prefers current compatible FastAPI/Uvicorn/Requests/urllib3 releases to keep CI and user installs out of unnecessary resolver backtracking.
- Gateway now applies a compatible setuptools lower bound so Apple/GPU installs satisfy Torch's `<82` constraint without resolving into ancient broken setuptools releases.

### Changed
- Raised the Runtime floor to `AbstractRuntime>=0.4.13` so Gateway profiles consume Runtime's updated multimodal dependency metadata, and raised the Music floor to `abstractmusic>=0.1.2`.

## [0.2.12] - 2026-05-19

### Fixed
- Gateway Apple install profiles now preserve the entrypoint contract by cascading `[all-apple]` through Runtime, Agent, Core, Vision, Voice, Music, and Memory dependencies; GPU profiles continue to cascade `[all-gpu]`.

### Changed
- Gateway's base remote-capable install now includes Core embeddings dependencies alongside remote providers, media, tools, tokens, compression, voice/audio, and vision while preserving the published Core dependency floor.

## [0.2.11] - 2026-05-19

### Fixed
- Gateway voice, TTS, STT, and vision catalog routes now use the AbstractCore capability abstractions as the source of truth for provider and provider-model discovery.
- Direct Gateway TTS and STT routes now dispatch through the AbstractCore capability registry, preserving explicitly selected media providers and models through execution.
- Gateway LLM provider/model discovery can proxy configured AbstractCore Server catalog routes while keeping Flow's existing response contract.

### Changed
- Raised dependency floors to Runtime `>=0.4.12`, Core `>=2.13.15`, Flow `>=0.3.11`, Vision `>=0.3.6`, and Voice `>=0.10.3`.

## [0.2.10] - 2026-05-13

### Fixed
- Gateway capability discovery now builds its embedded capability registry with Gateway-scoped media configuration, keeping discovery contracts aligned with the concrete voice, TTS, STT, and image catalog routes.
- Gateway media catalog proxy calls now avoid forwarding unset optional query params, preventing stale `None` values from breaking downstream capability discovery.

### Changed
- Raised dependency floors to Runtime `>=0.4.11`, Core `>=2.13.14`, Flow `>=0.3.11`, Vision `>=0.3.5`, and Voice `>=0.9.4`.


## [0.2.9] - 2026-05-12

### Added
- Gateway discovery now advertises `/api/gateway/audio/transcriptions/models` for STT catalog lookup.
- Added local and proxied STT model catalog responses backed by AbstractCore/AbstractVoice.

### Fixed
- Gateway capability catalogs now map Gateway-scoped voice and vision env vars into the embedded capability registry, so local Gateway deployments expose configured voice/TTS/STT/image models without requiring duplicate lower-level env names.
- Catalog proxy calls now omit unset optional query params instead of forwarding `None` values.

### Changed
- Raised dependency floors to Runtime `>=0.4.10`, Core `>=2.13.13`, Flow `>=0.3.10`, and Voice `>=0.9.3`.

## [0.2.8] - 2026-05-10

### Added

- Capability discovery now advertises
  `capabilities.contracts.common.runs.input_data` and
  `capabilities.contracts.common.runs.history_bundle` so thin clients can
  feature-detect the run input and RunHistoryBundle endpoints from the shared
  Gateway contract.

## [0.2.7] - 2026-05-10

### Updated

- Bumped abstractagent floor to >=0.3.6 to match the new abstractagent release that requires abstractruntime>=0.4.9.

## [0.2.6] - 2026-05-09

### Fixed

- Raised the AbstractVision floor to `abstractvision>=0.3.4` across Gateway
  install profiles so `abstractgateway[gpu]` and the NVIDIA image inherit the
  stable-diffusion.cpp binding constraint that avoids the broken
  `stable-diffusion-cpp-python==0.4.6` Linux sdist.
- Updated release-facing Docker examples and package metadata from `0.2.5` to
  `0.2.6`.
- Release/CI installs now bypass the restored pip dependency cache for editable
  dependency resolution, avoiding stale package indexes immediately after
  lower-package releases.

## [0.2.5] - 2026-05-09

### Changed

- Promoted the base `abstractgateway` install to the remote-light HTTP/SSE
  server profile. It now includes Runtime multimodal support, AbstractAgent,
  AbstractCore remote/media/tools/tokens/compression/vision/voice/audio,
  AbstractVision, AbstractVoice, AbstractFlow compatibility,
  AbstractMemory/LanceDB KG support, FastAPI, multipart uploads, and Uvicorn.
- Raised Runtime and Agent floors to `AbstractRuntime>=0.4.9` and
  `abstractagent>=0.3.6`.
- Simplified install guidance around `abstractgateway`, `abstractgateway[apple]`,
  and `abstractgateway[gpu]`. The older `http`, `server`, `multimodal`,
  `memory`, `voice`, `vision`, `all`, and `server-nvidia` extras remain as
  compatibility aliases.
- The NVIDIA Docker image now installs `abstractgateway[gpu]`; `server-nvidia`
  remains only as a compatibility alias.

## [0.2.4] - 2026-05-08

### Added

- Explicit install profiles for the Gateway package: minimal base,
  `http`, `multimodal`, `server`, `memory`, `apple`, `gpu`, `all-apple`,
  `all-gpu`, and `server-nvidia`.
- `abstractgateway-config` plus `abstractgateway config` for operator status and
  private `.env` bootstrap without taking ownership of AbstractCore provider
  configuration.
- Gateway memory store resolver for AbstractMemory-backed LanceDB, SQLite, and
  in-memory stores, including `/kg/query` store metadata.
- Core catalog proxy endpoints for thin clients:
  `GET /api/gateway/voice/voices`,
  `GET /api/gateway/audio/speech/models`, and
  `GET /api/gateway/vision/provider_models`.
- Added a `server-nvidia` extra plus an experimental CUDA/PyTorch-based
  `abstractgateway-server-nvidia` Docker image recipe for full NVIDIA machines.
- Release and manual GHCR image workflows now publish the light default server
  image and attempt an experimental best-effort NVIDIA full image.

### Changed

- Base installs are now intentionally minimal again:
  `AbstractRuntime>=0.4.8` only.
- Server and multimodal profiles now use the aligned Runtime/Core/Voice/Vision
  floors: `AbstractRuntime>=0.4.8`, `abstractcore>=2.13.12`,
  `abstractvision>=0.3.3`, and `abstractvoice>=0.9.2`.
- Server, native Apple, native GPU, and NVIDIA profiles now require
  `abstractagent>=0.3.5`, so Gateway-hosted agent nodes resolve against the
  same Core/Runtime baseline as Gateway itself.
- Release tests now reset Gateway's process-global service between cases and
  pass explicit provider/model overrides for ledger summary/chat generation
  tests.
- Native Python hardware profiles are full deployment aggregates:
  `abstractgateway[apple]` and `abstractgateway[all-apple]` install the
  Apple-local stack and all relevant non-NVIDIA framework capabilities, while
  `abstractgateway[gpu]` and `abstractgateway[all-gpu]` install the matching
  local GPU stack.
- Gateway-owned runtime handoff now seeds `_runtime.prompt_cache`,
  `_runtime.max_attachment_bytes`, and `_runtime.workflow_bundles_dir` from
  Gateway configuration.
- Gateway LLM helper defaults now resolve through the same deployment cascade as
  runtime execution instead of hardcoded local model fallbacks.
- Docker Compose local builds can override `ABSTRACTGATEWAY_EXTRAS`; the
  default examples use port `8080`, and an NVIDIA compose overlay is available
  for GPU hosts.
- The default Docker server image now composes `abstractgateway[server,memory]`
  so KG workflows and `/kg/query` have the AbstractMemory/LanceDB store package
  available without making memory a base-package dependency.
- The `memory` profile now depends on `AbstractMemory[lancedb]>=0.2.6`.

### Fixed

- `memory_kg_*` effects and `/kg/query` no longer assume LanceDB directly;
  in-memory stores work, SQLite structured queries work when the installed
  AbstractMemory build exposes `SQLiteTripleStore`, and semantic queries fail
  clearly when the selected store has no vector/search capability.
- Dynamic voice/audio/vision catalog discovery now delegates to the AbstractCore
  server catalog boundary when configured, with bounded static fallback when it
  is not.
- Observer/chat/backlog/discovery helpers now return a clear provider/model
  configuration error when no request, Gateway env, or AbstractCore default is
  available.

### Notes

- The default Docker image remains the release-grade light, portable image for
  `linux/amd64` and `linux/arm64`. The NVIDIA image is `linux/amd64` only and
  is experimental/best-effort because vLLM/Torch/Diffusers dependency
  resolution is much heavier than the default server profile and still needs a
  CUDA host smoke gate before production positioning.
- There is no practical MLX Docker image target for Apple Silicon today: MLX
  depends on Apple's Metal stack and Docker Desktop runs Linux containers
  without Metal/MPS device access. Apple local inference should stay native on
  macOS, not containerized; the Gateway container can point at Docker Model
  Runner, native LM Studio, `mlx_lm.server`, or Ollama OpenAI-compatible
  endpoints via `model-runner.docker.internal` or `host.docker.internal`.

## [0.2.3] - 2026-05-08

### Added

- Versioned thin-client capability contracts for Gateway common features, AbstractFlow editor/runtime support, AbstractAssistant media/cache controls, and AbstractCode-facing prompt-cache controls.
- AbstractFlow gateway-first editor contract validation, including VisualFlow CRUD/publish/start/observe coverage and a bundled flow input-schema endpoint.
- Gateway-owned session prompt-cache lifecycle routes:
  - `GET /api/gateway/sessions/{session_id}/prompt_cache/status`
  - `POST /api/gateway/sessions/{session_id}/prompt_cache/prepare`
  - `POST /api/gateway/sessions/{session_id}/prompt_cache/rebuild`
  - `POST /api/gateway/sessions/{session_id}/prompt_cache/clear`
- Generated-media contract fields in capability discovery, including direct-vs-workflow generated-image availability.
- Direct generated-image route, `POST /api/gateway/runs/{run_id}/images/generate`, backed by Runtime/Core image output selectors, artifact storage, and `abstract.media.image.generated` ledger events.
- Backlog completion ledger for the capability contract, Flow editor contract, session prompt-cache lifecycle, and generated-media gateway contract.

### Changed

- Capability discovery now truthfully reports provider-level and session-level prompt-cache controls, plus direct Gateway voice/audio/image endpoints where configured.
- API, configuration, deployment, Docker, README, FAQ, and LLM ingestion docs now describe generated images as both workflow-backed and directly available through the Gateway route when a Runtime/Core image backend is installed and configured.
- Docker/Compose release examples now point at the `0.2.3` server image.

### Fixed

- Fixed stale release-facing docs that said Gateway had no direct image-generation endpoint after the direct route landed.
- Fixed an order-dependent test import leak so the full local pytest suite can run cleanly after the AbstractFlow editor contract tests.

### Notes

- Direct image generation still depends on a configured Runtime/Core/AbstractVision-compatible backend; Gateway does not bundle heavy local image engines.
- Session prompt-cache lifecycle is Gateway-owned naming and orchestration over provider/model controls. It is not a provider-independent local KV cache or full CachedSession persistence system.

## [0.2.2] - 2026-05-06

### Added

- MkDocs Material configuration for the documentation site.
- CI docs build job and release docs gate.
- Release workflow deployment to GitHub Pages via `mkdocs gh-deploy`.
- PyPI-backed GHCR server image publishing for `ghcr.io/lpalbou/abstractgateway-server`.
- CI validation build for the local server Docker image recipe.
- Docker server image, Compose profile, and deployment documentation.
- `docs`, `server`, `vision`, and `multimodal` optional dependency extras.
- Discovery metadata for AbstractCore capability plugins (`voice`, `audio`, `vision`, and future `music`).

### Changed

- Version metadata aligned across `pyproject.toml`, package `__version__`, and FastAPI app metadata.
- The server install profile now mirrors the newer AbstractRuntime/Core multimodal stack: `AbstractRuntime[multimodal]>=0.4.6`, `abstractcore[remote,media,tools,tokens,compression,vision,voice,audio]>=2.13.10`, `abstractvision>=0.3.1`, and `abstractvoice>=0.9.0`.
- The server Docker/Compose profile now documents workflow-backed image generation through AbstractVision, direct Gateway TTS/STT through AbstractVoice, and provider-dependent prompt-cache controls.
- Gateway voice/audio endpoints now accept AbstractVoice's newer local/remote backend environment knobs in addition to the existing Gateway-scoped settings.

### Notes

- Release scope is intentionally explicit: TTS and STT have direct Gateway endpoints; generated images are available through Runtime/Core workflows with AbstractVision installed and configured, but Gateway does not yet expose a direct image-generation HTTP endpoint.
- Prompt-cache support is provider-level control-plane support. This release does not add a Gateway-owned CachedSession lifecycle API.
- `flows/bundles/article@dev.flow` was inspected and left untracked. It is a local `dev` bundle generated by the Gateway publisher, not a release artifact.

## [0.2.1] - 2026-02-09

### Changed

- Dependency bumps (see `pyproject.toml`):
  - `AbstractRuntime>=0.4.2` (and `AbstractRuntime[abstractcore]>=0.4.2` for HTTP/voice/telegram/all extras)
  - `abstractagent>=0.3.1`, `abstractvoice>=0.6.3`, `abstractflow>=0.3.7`
  - `abstractcore[media,tools]>=2.11.8` (via `abstractgateway[all]`)
- Documentation refresh for external users:
  - added explicit AbstractFramework ecosystem context
  - updated minimum versions in install snippets to match `pyproject.toml`
  - kept the architecture diagram as the canonical “shape of the system”
- Version metadata alignment:
  - `pyproject.toml`, `src/abstractgateway/__init__.py`, and `src/abstractgateway/app.py` now agree on `0.2.1`

## [0.1.1] - 2026-02-04

### Changed

- Documentation refresh for external users:
  - new FAQ (`docs/faq.md`)
  - clarified quickstart + smoke checks in `README.md`
  - tightened getting started, configuration, security, and API overview docs
  - improved cross-linking in `CONTRIBUTING.md` and `SECURITY.md`
  - refreshed `llms.txt` / `llms-full.txt` for agent ingestion (index + full snapshot)
- Version bump to reflect the documentation release (`0.1.0` → `0.1.1`).

### Notes

- No intentional runtime behavior changes in this release; it is documentation-focused.

## [0.1.0] - 2026-02-03

### Added

- Initial public package for AbstractGateway (`abstractgateway`).

---

## CONTRIBUTING.md

# Contributing

Thanks for your interest in improving AbstractGateway.

This repo is a Python package (`src/` layout) with a FastAPI server, a durable runner worker, and contract tests under `tests/`.

## Quick start (dev)

```bash
python -m venv .venv
source .venv/bin/activate

python -m pip install -U pip
pip install -e ".[dev]"
```

Run the test suite:

```bash
pytest
```

If you only want the fast/unit/contract layer:

```bash
pytest -m basic
```

Notes:
- `integration` and `e2e` tests may require optional dependencies and/or external services (e.g. an LLM provider).
- The CLI entrypoint is `abstractgateway` (see `pyproject.toml`).

### Tests never touch your home or the network

`tests/conftest.py` makes every run hermetic, whatever your shell exports:

- **Home and caches.** `HOME` (and `USERPROFILE` on Windows) points at a
  temporary directory for the whole session and a fresh one for each test, so
  everything the code derives from the home directory lands there: the
  AbstractCore config, models, embeddings and blocs under `~/.abstractcore`,
  the Hugging Face cache (`HF_HOME`, `HF_HUB_CACHE`), the data registry, and the gateway's own data, flows and AbstractCore config store (per-test directories).
  Path settings exported in your shell (the `ABSTRACT*`/`HF_*` directory, file
  and cache variables, `XDG_*`) are cleared for the run. This happens when the
  conftest is imported, before any package or `huggingface_hub` loads; a test
  fails loudly if `huggingface_hub` froze its cache path on your real home.
- **Network guard.** Sockets refuse any non-loopback destination and name
  lookup, and also the live local services on loopback: the gateway (8080), LM
  Studio (1234), Ollama (11434) and 18850. Any other loopback port stays open,
  so `TestClient`, fake servers and scratch-port fixtures work. A refused
  attempt fails the test and is listed under "network guard" at the end of the
  run with the host and port it tried to reach. Point such a test at a fake or
  a scratch port; the `fake_public_dns` fixture answers name lookups for code
  that resolves a host before a faked fetch.
- **Subprocess guard.** A child process has its own sockets, so a real engine
  CLI would slip past the network guard. Launching `lms`, `ollama`, `open` or
  `xdg-open` (through `subprocess`, `asyncio` subprocesses or `os.system`,
  including `sh -c "…"` and `env …` forms) is refused, fails the test, and is
  listed under "subprocess guard" at the end of the run. Fake the CLI instead:
  record the argv in a double, or register a stand-in script with the
  `fake_cli` fixture (`fake_cli("lms", "#!/bin/sh\necho ok\n")` returns its
  path; only that file may run).
- **Opting out, with a reason.** `@pytest.mark.desktop("reason")` marks a test
  that drives the real engine CLIs or the desktop; it is skipped unless you
  run `pytest --allow-desktop` (a `network` test may launch them too). `@pytest.mark.network("reason")` marks a test
  that genuinely needs the network (a Hub lookup, a real download, a live
  provider). Such tests are skipped unless you run `pytest --allow-network`.
  `@pytest.mark.real_home("reason")` marks a test that READS your real home
  (for example installed tokenizers); `HOME` still stays temporary, and the
  test gets the real path as `ABSTRACT_TEST_REAL_HOME`. A marker without its
  reason is a collection error, as is a test module that reads the real-home
  path without the marker. `pytest --markers` lists all three.

## How to contribute

1. **Open an issue** (or a draft PR) describing what you want to change and why.
2. Keep changes **small and reviewable**.
3. Add/adjust tests where it improves confidence.
4. Update docs in the same change so they stay truthful and user-facing:
   - `README.md` is the entrypoint; `docs/README.md` indexes every page in
     `docs/` (add new pages there and to the `nav` in `mkdocs.yml`).
   - `docs/getting-started.md` and `docs/first-run.md` are the step-by-step
     guides; `docs/faq.md` answers recurring questions and
     `docs/troubleshooting.md` holds symptom → cause → fix entries.
   - Describe current behavior. Put what changed, and any upgrade steps, in
     `CHANGELOG.md`.
   - `llms.txt` indexes the docs and is also the corpus of the console's docs
     assistant; update it when a key fact changes, then regenerate the
     snapshot: `python scripts/generate-llms-full.py` (updates `llms-full.txt`).
   - Check the docs site builds: `pip install -e ".[docs]" && mkdocs build`.

### Shipped workflows

Changing the shipped workflow set is a change in two places: the
force-include pins in `pyproject.toml` and the matching `!flows/bundles/<name>`
negation in `.gitignore`. `tests/test_gateway_shipped_default_workflows.py`
fails if a pinned artifact is untracked or does not load, and each shipped
bundle has a contract test (for example
`tests/test_deep_research_bundle_contract.py`).

## Project conventions

- Source of truth is the code in `src/`.
- Keep public docs concise, actionable, and aligned with the current behavior.
- Prefer explicit env var names as used in code (see `docs/configuration.md`).

## Release checklist (maintainers)

1. Update `CHANGELOG.md`.
2. Bump version in:
   - `pyproject.toml`
   - `src/abstractgateway/__init__.py`
   - `src/abstractgateway/app.py` (FastAPI version string)
3. Run `pytest`.
4. Build artifacts (optional): `python -m build`

## Related docs

- Package overview + quickstart: [README.md](README.md)
- Docs index: [docs/README.md](docs/README.md)
- Getting started: [docs/getting-started.md](docs/getting-started.md)
- Architecture: [docs/architecture.md](docs/architecture.md)
- API overview: [docs/api.md](docs/api.md)
- Changelog: [CHANGELOG.md](CHANGELOG.md)
- Security policy: [SECURITY.md](SECURITY.md)
- Code of conduct: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)

---

## SECURITY.md

# Security policy

Thanks for helping keep AbstractGateway and its users safe.

## Reporting a vulnerability

Please **do not** open a public GitHub issue for security vulnerabilities.

Instead, use GitHub’s **private vulnerability reporting** / **Security Advisories** for this repository:
- Go to the repository’s **Security** tab
- Open **Advisories**
- Click **Report a vulnerability** (or create a draft advisory)

If you cannot use GitHub advisories, contact the maintainers privately (e.g. via GitHub profile contact links).

## What to include

To help us triage quickly, include:
- a clear description of the issue and impact
- minimal reproduction steps or a PoC
- affected versions and environments (OS/Python version/config)
- any suggested mitigation or patch

## Coordinated disclosure

We appreciate responsible disclosure and will work with you to:
- confirm the issue
- assess severity and affected versions
- produce a fix and release

Please avoid active exploitation, privacy violations, or destructive testing.

## Related docs

- Security configuration (auth/origin/limits): [docs/security.md](docs/security.md)
- Getting started: [docs/getting-started.md](docs/getting-started.md)

---

## CODE_OF_CONDUCT.md

# Code of Conduct

## Our Standard

AbstractGateway is maintained as a professional software collaboration. Contributors, maintainers, and users are expected
to keep discussions respectful, technically focused, and welcoming to people with different backgrounds and experience
levels.

Examples of expected behavior:

- Use clear, constructive language when giving feedback.
- Assume good faith while still asking for evidence and reproducible details.
- Keep disagreements focused on the code, docs, design, or release process.
- Respect privacy and do not publish private contact details, credentials, logs, or user data.

Examples of unacceptable behavior:

- Harassment, threats, insults, or discriminatory language.
- Sustained off-topic disruption of issues, pull requests, or discussions.
- Publishing private information without explicit permission.
- Pressuring maintainers or contributors to bypass safety, security, or release checks.

## Reporting

Report conduct concerns privately to the maintainer contact listed in the package metadata or through the repository
owner's GitHub profile. Include the relevant links, screenshots, or context when possible.

Maintainers may remove comments, close threads, block accounts, or restrict repository access when needed to protect the
project and its contributors.


See also [CONTRIBUTING.md](CONTRIBUTING.md) and, for security issues, [SECURITY.md](SECURITY.md).

---

## ACKNOWLEDGMENTS.md

# Acknowledgments

AbstractGateway stands on the shoulders of many open-source projects and contributors.

This list is **non-exhaustive**. The canonical dependency list for this package is in `pyproject.toml`.

## Core dependencies

- **AbstractRuntime**: durable run model, workflow registry, file/SQLite stores, and runtime tick loop.
- **AbstractCore** (through AbstractRuntime): providers, tools, media capabilities, the model catalog, engine detection and host jobs.
- **AbstractAgent**: Visual Agent nodes in bundle mode.
- **AbstractMemory** + **LanceDB**: `memory_kg_*` nodes in bundle mode (knowledge graph storage).
- **FastAPI** (via **Starlette**) + **Pydantic**: HTTP API surface and request/response models.
- **Uvicorn**: ASGI server used by `abstractgateway serve`.
- **python-multipart**: multipart upload support for bundle/attachment endpoints.
- **PyYAML**: entity seed documents.

## Optional integrations (feature-dependent)

- **pystray** + **Pillow** (`abstractgateway[tray]`): the desktop tray icon.
- **Node.js** (installed by the gateway from the `nodejs-wheel-binaries` build when needed) and the **npm** registry: the browser apps.
- **AbstractFlow**: workflow authoring and bundling (Gateway runs `.flow` bundles without depending on it).
- **TDLib**: Telegram Secret Chats support when using the TDLib transport.

## Dev/test tooling

- **pytest** and **httpx**: test suite and HTTP client utilities used under `tests/`.
- **hatchling**: Python packaging/build backend.

## Contributors

Thank you to everyone who reports issues, improves documentation, and contributes code.

---

## docs/security.md

# AbstractGateway — Security guide

AbstractGateway secures the **gateway API surface** (`/api/gateway/*`) using an ASGI middleware:
`GatewaySecurityMiddleware` in `src/abstractgateway/security/gateway_security.py`.

Notes:
- `/api/health` is intentionally not protected.
- `/api/triage/action/*` uses signed action tokens and is not under `/api/gateway` (see `src/abstractgateway/routes/triage.py`).
- Vulnerability reporting policy: see [../SECURITY.md](SECURITY.md).

## Default behavior

Security is on by default for `/api/gateway/*`:

- A plain `abstractgateway serve` with no auth posture in its environment binds
  `127.0.0.1` (or the stored network mode), turns **user accounts** on and
  creates the admin account `default/admin`
  ([first-run.md](docs/first-run.md)).
- Choosing `lan` or `internet` with the [network setting](#network-exposure)
  keeps user accounts on.
- An explicit non-loopback `--host` with neither user accounts nor a token
  refuses to start, and so does a weak shared token on a non-loopback bind.

Evidence: startup self-checks in `src/abstractgateway/cli.py`,
`src/abstractgateway/first_run.py`.

Explicit browser-console/browser-app setup:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"
abstractgateway serve --host 127.0.0.1 --port 8080

# Use this with Gateway user admin.
cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token"
```

API clients can send a Gateway user token:

```text
Authorization: Bearer <token>
```

### Tenant and user isolation

In token mode, `ABSTRACTGATEWAY_AUTH_TOKEN` is a gateway-level control-plane
token and maps to the `local-admin` principal. Treat that token as
full authority for the Gateway instance.

Hosted user-auth mode is enabled with `ABSTRACTGATEWAY_USER_AUTH=1` or
`ABSTRACTGATEWAY_AUTH_MODE=users`. In that mode, Gateway bearer tokens resolve
to concrete principals with `tenant_id`, `user_id`, roles/scopes, and a token
fingerprint. `GET /api/gateway/me` returns the resolved principal and routing
mode. The presence of an `auth/users.json` registry file is readiness state; it
does not silently enable hosted user auth unless `ABSTRACTGATEWAY_USER_AUTH_AUTO=1`
is set for compatibility. Admin principals can manage users through:

- `GET /api/gateway/admin/users?kind=human|entity|all` (default `all`)
- `POST /api/gateway/admin/users`
- `GET /api/gateway/admin/users/{user_id}?tenant_id=...`
- `PATCH /api/gateway/admin/users/{user_id}?tenant_id=...`
- `DELETE /api/gateway/admin/users/{user_id}?tenant_id=...`
- `GET /api/gateway/admin/runtime-reservations`
- `POST /api/gateway/admin/runtime-reservations/{runtime_id}/transfer`
- `POST /api/gateway/admin/runtime-reservations/{runtime_id}/purge`

Every user row carries a first-class `principal_kind` field (`"human"` or
`"entity"`); clients must read it (or the `kind` filter) instead of
re-deriving kind from the `roles` convention. Census asymmetry is deliberate:
`GET /api/gateway/entities` is the ENTITY census (homes on disk), while
`?kind=entity` here is the entity-PRINCIPAL census — homes created before
principal minting have no user row, so the two lists can legitimately differ
and neither may be derived from the other.

Entity principals (minted at entity creation) are shaped by the entities
lane, not the users lane: `PATCH` refuses `token`/`rotate_token`/`roles`/
`runtime_id` and `DELETE` refuses outright (HTTP 403 naming the lane). A
rotation would mint a live entity bearer that by design must not exist, and
a delete would remove the name-collision guard protecting the entity's
identity. `enabled` (the door-side disable), `email`, and `scopes` stay
editable. The guard lives in `GatewayUserRegistry` itself, so the config CLI
refuses the same writes.

Gateway stores user token hashes in `<ABSTRACTGATEWAY_DATA_DIR>/auth/users.json`
by default. Generated or rotated bearer tokens are returned once from the admin
create/update response and are never stored in plaintext.

Browser apps should exchange user bearer tokens for Gateway browser sessions
instead of storing bearer tokens. `POST /api/gateway/session/login` accepts a
Gateway user id and user token, validates them against the registry, and sets an
opaque signed session id plus a CSRF token as cookies. The JSON response body
does not expose those values. Gateway stores session records in
`<ABSTRACTGATEWAY_DATA_DIR>/auth/sessions.json` by default.
The session cookie is HTTP-only; the CSRF cookie is readable by the hosting app
so it can send the CSRF header. Both cookies use path `/` and `SameSite=Lax`.
Plain HTTP local-dev responses do not set `Secure`; HTTPS responses, including
requests forwarded with `X-Forwarded-Proto: https`, do set `Secure`.
Non-remembered sessions omit `Max-Age`; remembered sessions include one.
Session-authenticated mutating requests must send:

```text
X-AbstractGateway-Session: <session id>
X-AbstractGateway-CSRF: <csrf token>
```

`POST /api/gateway/session/logout` revokes the session. Disabling, deleting, or
rotating the Gateway user invalidates existing browser sessions for that user.

**Who can sign in depends on whether user accounts are on.** With user
accounts on, every registry account (admin or not) can sign in, and each one
works in its own runtime (below). With user accounts off the gateway runs one
runtime, the operator's, so every signed-in person would share the operator's
runtime, capability defaults, endpoint profiles and workflows. In that mode
only accounts with the `admin` role can hold a browser session:

- `POST /api/gateway/session/login` answers `401` for a non-admin account, with
  `reason_code: "user_accounts_off_admin_only"` and a message naming the two
  ways out: the gateway operator turns user accounts on, or the person signs
  in with an admin account.
- A session that already exists for a non-admin account (for example one
  created while user accounts were on) is refused and removed at its next
  use. Every session path applies the same rule (`principal_barred_from_shared_runtime`
  in `security/sessions.py`), including the browser-app sign-in handover.
- `POST /api/gateway/admin/users` answers `409` (same `reason_code`) instead of
  creating a non-admin account that could never sign in, and
  `PATCH /api/gateway/admin/users/{user_id}` refuses to remove the `admin` role
  from an admin account in this mode.

Admin accounts sign in in both modes. The rule reads the same setting the
service routing reads, so the two cannot disagree.

**The last admin account is protected.** `DELETE /api/gateway/admin/users/{user_id}`,
and a `PATCH` that disables it or removes its `admin` role, answer `409` with
`reason_code: "last_admin"` when the target is the only enabled admin account
left (entity principals never count). Create or enable another admin first.

When user auth is active, the Gateway service composition root routes each
principal to an isolated service/data plane under:

```text
<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant_id>/<runtime_id>/runtime
<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant_id>/<runtime_id>/flows
```

Gateway rejects duplicate `runtime_id` values within the same tenant during
user creation and update. This keeps the default multi-user invariant at
`1 user = 1 runtime`. Deleting a user removes the credential but reserves the
retained runtime id for that principal, so another same-tenant user cannot be
assigned to retained data by accident. Reusing the same runtime id in a
different tenant remains valid.

Admins can intentionally resolve retained runtime reservations through
admin-only lifecycle routes. Transfer assigns a retained runtime to an existing
same-tenant user and reserves that user's previous runtime id. Purge requires an
exact `confirm_runtime_id`, deletes the retained runtime root under
`<ABSTRACTGATEWAY_DATA_DIR>/users/<tenant_id>/<runtime_id>/`, then releases the
runtime id for reuse. Regular users cannot list, transfer, or purge retained
runtime reservations.

Clients must not send authoritative `user_id`, `tenant_id`, `runtime_id`, or
workspace-root values. Runtime fields such as `actor_id` and `session_id`, and
references such as `run_id`, `artifact_id`, and memory `owner_id`, remain
correlation and lookup fields; they do not authorize access by themselves.

In hosted multi-user mode, the request path resolves a principal and routes it
to its own service. Gateway also applies a central
route-family authorization table for operator/admin surfaces. Admin-only route
families include user management, audit, process control, backlog/triage/report
operations, email bridge routes, model residency mutations
(`POST /models/load|unload|lock|unlock|download`), session-wide prompt-cache
clearing (`POST /sessions/{session_id}/prompt_cache/clear_all`), server
workspace file helpers, server-workspace artifact import/export, and global
prompt-cache/bloc mutation routes. Host and residency reads —
`GET /models/loaded`, `GET /models/context_estimate`, `GET /host/state`,
`GET /host/metrics/*`, and `GET /sessions/prompt_cache` — are visibility every
authenticated client needs and serve any authenticated principal; anonymous
requests remain rejected. Regular users remain able to use
their own runtime data plane for run, ledger, artifact upload, discovery, and
runtime-scoped Core capability-default routes.

The route table is intentionally conservative around server filesystem access:
browser-local files should use `/api/gateway/attachments/upload`; server
workspace reads/imports/exports require an admin principal until a stronger
per-user workspace grant model exists.

Capability discovery follows the same policy. Regular users can still discover
ordinary run, ledger, artifact, upload, provider/model catalog, KG, and
runtime-scoped defaults surfaces, but admin-only workspace artifact
import/export and provider prompt-cache controls are advertised as unavailable
with machine-readable `admin_required` metadata. Session-level prompt-cache
keys remain available for users; the private hash includes the current
principal scope, so two users using the same session id/provider/model tuple do
not collide in a shared provider control plane.

Hosted provider secrets are supported through Gateway provider connections.
Connections are stored under the relevant Gateway data plane, expose only
non-secret metadata and a virtual provider id such as `endpoint:office-vllm`,
and inject the raw key only into the transient Runtime provider call. Normal
users can manage user-scoped connections; Gateway-scoped connections require an
admin principal. The current capability-default cascade uses execution-host
Core defaults, then the Gateway/root Core config baseline, then the user's
runtime Core config override under that user's Gateway data plane. A stronger encrypted vault, audit model, and
bridge/delegated-tool propagation policy remain future hardening work.

### Workflow registry ownership

Writing a workflow registry requires owning it. Under hosted user auth the
`/api/gateway/bundles` routes resolve to the calling principal's own bundle
directory, which that user may change freely. The gateway's own directory is
the shared set every user can see and run, so changing it requires an admin
principal.

One check covers every route that writes a registry — `POST /bundles/upload`,
`DELETE /bundles/{bundle_id}`, `POST /bundles/reload`,
`POST /bundles/{bundle_id}/deprecate`, `POST /bundles/{bundle_id}/undeprecate`
and `POST /visualflows/{flow_id}/publish` — so a shared workflow cannot be
replaced through one route while another is restricted. The check runs before
the route looks the bundle up, so a non-admin gets `403` for a bundle that
does not exist as well. Since a non-admin account cannot be signed in while user accounts are off
(above), this check is the second, independent line of defence. `POST /visualflows/{flow_id}/publish` accepts a caller-supplied
`bundle_id`, `bundle_version` and `overwrite`, and installs into the same
registry as `upload`; it is gated on the same rule. Non-admin requests against
the shared registry return `403`. Read routes are unchanged.

`DELETE /bundles/{bundle_id}` returns `409` for the `basic-agent.flow` the
gateway verifies at startup: removal has no undo and would prevent the next
start. Install a replacement bundle first, then remove the old file.

### Shared workflow catalog

Do not share workflows by pointing multiple users at another user's private
bundle directory. Private `/api/gateway/bundles` routes stay scoped to the
current principal's runtime. Shared/default workflows belong in the Gateway
workflow catalog:

- catalog versions are immutable by `scope + tenant + bundle_id +
  bundle_version + sha256`;
- admins move explicit default pointers instead of overwriting existing
  versions;
- catalog ACLs are checked at run start against the authenticated principal's
  tenant, roles, and user id;
- catalog runs execute in the requesting user's runtime by default;
- catalog run policy is Gateway-issued and HMAC-signed before it is handed to
  Runtime state; client-supplied `_runtime.workflow_policy` values are stripped;
- private bundle inspection routes reject catalog-internal bundle ids, so
  catalog flow/schema inspection remains ACL-aware;
- deprecate/block/tombstone changes block new starts without deleting stored
  bundle bytes.

Catalog mutation routes are admin-only under
`/api/gateway/admin/workflow-catalog/*`. User-visible catalog discovery is
available at `GET /api/gateway/workflow-catalog`.

## Origin allowlist (browser/origin defense)

If the request includes an `Origin` header, the middleware allows it only when
it matches the allowlist (glob-style patterns, fnmatch). The allowlist is
`http://localhost:*` and `http://127.0.0.1:*`, the gateway's own LAN origins in
a network mode, plus the **`allowed_origins` setting** (console: Network →
*Advanced: reverse proxy*; TUI: Connection screen; CLI:
`abstractgateway network set --allowed-origins https://gateway.example.com`).
The setting is read per request: a change applies to the next request, no
restart. Each origin is validated (`scheme://host[:port]`, no path, no trailing
slash); `*` and wildcard patterns are accepted only as typed and are flagged.
See [configuration.md](docs/configuration.md#reverse-proxy-allowed-origins-and-trust-proxy).

A gateway started with `ABSTRACTGATEWAY_ALLOWED_ORIGINS` in its environment
uses that list instead (a deployment pin): every surface says "This gateway was
started with ABSTRACTGATEWAY_ALLOWED_ORIGINS in its environment" and reports
`overridden_by_env: true`; the saved setting applies once it starts without it.

Evidence: `GatewayAuthPolicy.allowed_origins`, `_effective_allowed_origins()` and
`_origin_allowed()` in `src/abstractgateway/security/gateway_security.py`;
`live_reverse_proxy()` in `src/abstractgateway/network_exposure.py`.

Important nuance:
- FastAPI’s CORS middleware in `src/abstractgateway/app.py` is permissive, but **origin enforcement for gateway endpoints is done by this security middleware**.
- In a network exposure mode from the settings store, `serve` adds the
  gateway's own discovered LAN origins (IP literals and `<name>.local`). A
  foreign origin, including a DNS-rebinding name that resolves to your LAN IP,
  is still refused (403) unless it is in `allowed_origins`.

## Network exposure

The network exposure setting ([configuration.md](docs/configuration.md#network-exposure-localhost--local-network--internet))
chooses `localhost`, `lan` or `internet`. What changes for someone else on
your network:

- **`localhost`** (default for a first run): the gateway listens on
  `127.0.0.1` only. Nobody else can open a connection; every local process of
  every local user still can, which is why user auth stays on.
- **`lan`**: the gateway listens on every IPv4 interface. Anyone on the same
  network (and anyone on a VPN such as Tailscale whose address is listed) can
  reach the sign-in page and the API. The gate is authentication: `lan` is
  refused unless user auth will be on at the next start; unauthenticated
  requests answer 401, failed credentials are locked out per client address
  with a growing wait, and a browser page from a foreign origin is refused
  (403). `lan` and `internet` are also refused when the gateway was started
  with read protection off (`ABSTRACTGATEWAY_PROTECT_READ=0`: unauthenticated
  reads would be answered as the admin). What `lan` does NOT give you:
  - **encryption**: it is plain HTTP. Passwords, bearer tokens and the
    session cookie cross the network in clear; the session cookie is
    `HttpOnly; SameSite=Lax` but not `Secure` over HTTP. Anyone who can sniff
    the network (shared Wi-Fi, a compromised router) can capture and replay a
    session. Use `lan` on networks you trust, or use a TLS proxy / VPN.
  - **a smaller attack surface**: every admin route is reachable to whoever
    holds an admin credential. Give each person their own account, keep the
    admin token off other machines, and prefer non-admin accounts for daily use.
  - **exposure of the browser apps**: apps started from the Apps page stay
    bound to `127.0.0.1` (the `apps.host` setting, *Where apps listen*), and the sign-in
    handover (`/apps/handover/{code}`) only works on the host it was minted
    for and refuses a loopback-only app to a browser on another machine.
  - **engine and app installs for remote admins**: `allow_engine_install`
    defaults to off on a non-loopback bind for callers on other computers.
    Someone at the gateway machine itself can still install: the request's
    socket peer is loopback or one of this host's own addresses, and it
    carries no proxy header (`Forwarded`, `X-Forwarded-For`,
    `X-Forwarded-Host`, `X-Real-IP`). A remote computer cannot use this
    host's own address as the source of an established TCP connection (the
    handshake reply never reaches it, and the kernel drops outside packets
    with a local source address); a reverse proxy on this host would make
    every visitor look local, which is why a proxied request never counts.
- **`internet`**: the same bind plus an explicit acknowledgement. The gateway
  does **not** terminate TLS and does not configure your router or firewall.
  Put a TLS reverse proxy (Caddy, nginx, Traefik) or a tunnel (Cloudflare
  Tunnel, Tailscale Funnel, ngrok) in front and expose that; add the public
  `https://` origin under *Reverse proxy* (`allowed_origins`), turn on *Trust
  the proxy's client address* (`trust_proxy`) only when your own proxy is in
  front of every request, and rate-limit at the proxy. Forwarding the raw port
  means plain HTTP on the internet: do not.

The mode is applied at the next start and `serve --host/--port` override it;
`GET /api/gateway/network` always says what is configured, what is running,
and why they differ.

## Workspace filesystem scope (blacklist/whitelist)

AbstractGateway supports “thin clients” (browser UIs, bridges) that can trigger **filesystem-ish tools** (e.g. `list_files`, `read_file`, `write_file`). To avoid a thin client expanding server filesystem access, the gateway enforces a **workspace policy**.

Key point: the **main configuration** for filesystem allowlisting/denylisting is set when you **launch the gateway** (operator-controlled env vars). Thin clients can only request broader scopes when the gateway is started in a permissive mode.

### Default (safe): everything outside the run workspace is blocked

- When a run is started via `POST /api/gateway/runs/start` and `workspace_root` is missing (or rejected), the gateway creates a **per-run workspace** under:
  - `<ABSTRACTGATEWAY_DATA_DIR>/workspaces/<uuid>`
- AbstractRuntime applies workspace scoping to filesystem-ish tool arguments. The default is:
  - `workspace_access_mode=workspace_only`
  - absolute paths must stay under `workspace_root`

This means that by default, **all absolute paths are effectively “blacklisted”** except the run’s `workspace_root`.

Evidence:
- Run default workspace injection: `src/abstractgateway/routes/gateway.py` (`start_run`)
- Client scope clamping: `src/abstractgateway/routes/gateway.py` (`_sanitize_run_workspace_policy`, `_client_workspace_scope_overrides_enabled`)
- Runtime tool scoping: `abstractruntime/integrations/abstractcore/workspace_scoped_tools.py`
- Tests: `tests/test_gateway_workspace_policy_enforcement.py`

### Operator-controlled allowlist roots (recommended)

- `ABSTRACTGATEWAY_WORKSPACE_DIR`: base directory used to resolve relative workspace paths and as the default root for `/files/*` helpers.
- `ABSTRACTGATEWAY_WORKSPACE_MOUNTS`: additional allowed roots (newline-separated `name=/abs/path`).

Thin clients can discover the server policy via:
- `GET /api/gateway/workspace/policy`
  Note: it returns **mount names only** (no absolute paths).

Canonical public server paths use `rel/path` for the main workspace root and
`mount_alias/rel/path` for approved mounts. When two allowed roots share the
same basename, Gateway emits deterministic digest-suffixed mount aliases so
the public path string stays stable across discovery, import/export, and
Runtime execution.

### Permissive mode: allow thin clients to choose scope (trusted machines only)

To honor client-provided workspace knobs (`workspace_root`, `workspace_access_mode`, `workspace_allowed_paths`, `workspace_ignored_paths`) beyond the operator roots, enable one of:

- `ABSTRACTGATEWAY_ALLOW_CLIENT_WORKSPACE_SCOPE=1`
- `ABSTRACTGATEWAY_TRUST_CLIENT_WORKSPACE_SCOPE=1`

In this mode, a client can request:
- `workspace_access_mode=all_except_ignored` (“full access” unless explicitly blocked)

Do **not** enable this when serving untrusted browser origins: a compromised thin client can request access to arbitrary server paths.

### Important limitation (all modes)

`execute_command` is **not** an OS sandbox: even if the runtime sets the default working directory under `workspace_root`, the command itself can reference absolute paths or `cd ..`.

## Common security env vars

All are loaded by `load_gateway_auth_policy_from_env()` (see `src/abstractgateway/security/gateway_security.py`).

### Enable/disable

- `ABSTRACTGATEWAY_SECURITY=1|0` (default: enabled)

### Tokens

- `ABSTRACTGATEWAY_AUTH_TOKEN` (single shared secret)
- `ABSTRACTGATEWAY_AUTH_TOKENS` (comma-separated list)
- `ABSTRACTGATEWAY_USER_AUTH=1` or `ABSTRACTGATEWAY_AUTH_MODE=users`: enable
  file-backed user principals and per-principal service routing
- `ABSTRACTGATEWAY_USER_AUTH_AUTO=1`: compatibility mode that also enables
  user auth when the registry file exists
- `ABSTRACTGATEWAY_USERS_FILE`: optional user registry path; defaults to
  `<ABSTRACTGATEWAY_DATA_DIR>/auth/users.json`
- `ABSTRACTGATEWAY_SESSIONS_FILE`: optional browser session registry path;
  defaults to `<ABSTRACTGATEWAY_DATA_DIR>/auth/sessions.json`
- `ABSTRACTGATEWAY_SESSION_TTL_S`: default browser session lifetime in seconds
  (default: 8 hours; bounded)
- `ABSTRACTGATEWAY_REMEMBER_SESSION_TTL_S`: browser session lifetime when an
  app requests "remember me" (default: 30 days; bounded)

### Protect reads vs writes

- `ABSTRACTGATEWAY_PROTECT_WRITE=1|0` (default: `1`)
- `ABSTRACTGATEWAY_PROTECT_READ=1|0` (default: `1`)
- `ABSTRACTGATEWAY_DEV_READ_NO_AUTH=1|0`
  Dev escape hatch: allow unauthenticated reads **from loopback only**.

### Limits (abuse resistance)

- `ABSTRACTGATEWAY_MAX_BODY_BYTES` (default: `10MB`)
  Applies to every mutating request. Oversized requests are **rejected** with
  `413` naming both sizes — bodies are never truncated. The default is sized for
  authored documents (a VisualFlow save is a whole workflow, not a small API
  payload), not just for abuse resistance.
- `ABSTRACTGATEWAY_MAX_ATTACHMENT_BYTES` (default: `25MB`)
- `ABSTRACTGATEWAY_MAX_BUNDLE_BYTES` (default: `75MB`)
- `ABSTRACTGATEWAY_MAX_CONCURRENCY` (default: `64`)
- `ABSTRACTGATEWAY_MAX_SSE` (default: `32`)

### Auth lockout (brute-force safety net)

- `ABSTRACTGATEWAY_LOCKOUT_AFTER` (default: `5`)
- `ABSTRACTGATEWAY_LOCKOUT_BASE_S` (default: `1.0`)
- `ABSTRACTGATEWAY_LOCKOUT_MAX_S` (default: `60.0`)

### Audit log (write requests)

- `ABSTRACTGATEWAY_AUDIT_LOG=1|0` (default: enabled for writes)
- `ABSTRACTGATEWAY_AUDIT_LOG_MAX_BYTES` (default: `50MB`)
- `ABSTRACTGATEWAY_AUDIT_LOG_ROTATIONS` (default: `10`)
- `ABSTRACTGATEWAY_AUDIT_LOG_HEADERS` (comma-separated allowlist; default: `x-client-id,x-client-version,x-forwarded-for`)

### Reverse proxies

- The `trust_proxy` setting (console: Network → *Advanced: reverse proxy* →
  *Trust the proxy's client address*; TUI: Connection screen checkbox; CLI:
  `abstractgateway network set --trust-proxy on|off`). On: `X-Forwarded-For` is
  used for IP attribution (audit log) and lockout tracking. Read per request:
  it applies to the next request. Only when your own proxy sits in front of
  every request; otherwise any client chooses the address the gateway sees.
  The ephemeral tray token never honours it (raw socket peer only).
- A gateway started with `ABSTRACTGATEWAY_TRUST_PROXY` in its environment uses
  that value instead; the status reports `overridden_by_env: true`.

## Production checklist (minimal)

- Run behind TLS (reverse proxy) and bind `--host 127.0.0.1` (proxy in front) or lock down your network if binding `0.0.0.0`.
- Use a strong random token and list exact origins in `allowed_origins` (avoid public wildcards).
- Keep `ABSTRACTGATEWAY_SECURITY=1`.

## Related docs

- Configuration overview: [configuration.md](docs/configuration.md)
- API overview: [api.md](docs/api.md)
- FAQ: [faq.md](docs/faq.md)

---

## docs/architecture.md

# AbstractGateway — Architecture

AbstractGateway is a **durable run gateway** for AbstractRuntime, and the
control plane of an AbstractFramework installation:

- clients **start runs** (and optionally schedule them);
- clients act through **durable commands** (`pause`, `resume`, `cancel`,
  `emit_event`, …);
- clients **replay** the durable ledger and optionally **stream** it (SSE);
- operators manage users, providers, capability defaults, local engines,
  models, browser apps and network exposure from one place (the web console,
  the terminal console, the CLI, or the desktop tray).

This page describes the components in this repository and how they connect.
For the endpoints, see [api.md](docs/api.md); for settings, see
[configuration.md](docs/configuration.md).

## Ecosystem placement (AbstractFramework)

AbstractGateway sits between **clients** (browser apps, terminal apps, the
desktop Assistant, scripts) and **AbstractRuntime**:

- **AbstractGateway** (this package): HTTP/SSE API, durability glue, security,
  and the operator control plane.
- **AbstractRuntime** (required): run model, tick loop, workflow registry and
  stores.
- **AbstractCore** (required, reached through Runtime facades): providers,
  tools, media capabilities, capability-route defaults, the model catalog,
  engine detection and host jobs (downloads, deletes).
- **AbstractAgent** and **AbstractMemory** (required by the default install):
  agent nodes and KG memory for bundle execution.
- Higher-level apps (optional): AbstractFlow (authoring), AbstractCode,
  AbstractObserver, AbstractContinuum, AbstractEntity and AbstractAssistant.

## System overview

```mermaid
flowchart LR
  subgraph Clients["Clients"]
    Browser["Browser: /console and browser apps"]
    TUI["Terminal apps: abstractgateway-console, Code"]
    Tray["Desktop tray helper (separate process)"]
    CLI["abstractgateway CLI"]
  end

  subgraph GW["AbstractGateway process (abstractgateway serve)"]
    Sec["GatewaySecurityMiddleware: auth, origins, limits, audit log"]
    Routes["FastAPI routes /api/gateway/*"]
    Console["/console (web console)"]
    Handover["/apps/handover, /apps/tui-handover (one-time sign-in)"]
    Principals["Principal routing: one data plane per user"]
    Runner["GatewayRunner: command inbox + ticks"]
    Host["Workflow host: .flow bundles + workflow catalog"]
    Settings["Runtime settings: network, apps.*, allow_engine_install, backlog"]
    AppsMgr["Apps manager: Node.js, npm installs, app processes"]
    Jobs["Engine installs and model download jobs"]
    HostCtl["Host control: pause, restart, update"]
  end

  subgraph Lower["Framework packages"]
    RT["AbstractRuntime: Runtime.tick, stores"]
    Core["AbstractCore: providers, catalog, host jobs"]
  end

  Data[("Data dir: runs, ledgers, commands, artifacts, auth, settings")]
  Apps["Browser app servers (127.0.0.1:3001-3005)"]

  Browser -->|HTTP| Sec
  TUI -->|HTTP| Sec
  CLI -->|HTTP or data dir| Sec
  Tray -->|loopback HTTP, ephemeral token| Sec
  Sec --> Routes
  Sec --> Console
  Routes --> Principals --> Host
  Routes --> Settings
  Routes --> AppsMgr
  Routes --> Jobs
  Routes --> HostCtl
  Runner --> Host
  Runner --> RT
  Host --> RT
  RT --> Data
  Routes --> Data
  Jobs --> Core
  Host --> Core
  AppsMgr -->|starts and supervises| Apps
  Handover --> Apps
  Apps -->|same-origin proxy to /api/gateway| Sec
```

## Core components (code-mapped)

- **HTTP app** (`src/abstractgateway/app.py`): mounts the routers under `/api`
  (`/api/gateway/*` is the main surface), `/api/health`, `/console`,
  `/docs` (Swagger UI) and the app handover routes.
- **Security layer** (`src/abstractgateway/security/`): the
  `GatewaySecurityMiddleware` protects `/api/gateway/*` with user or token
  auth, an origin allowlist, request limits, auth lockouts and an audit log.
  Browser sessions, principals and the route-family authorization table live
  in the same package. See [security.md](docs/security.md).
- **Composition root** (`src/abstractgateway/service.py`): builds the stores,
  the workflow host and the runner. With user auth on, each principal is
  routed to its own service and data plane under
  `<data dir>/users/<tenant>/<runtime>/`.
- **Durable stores** (`src/abstractgateway/stores.py`): file-backed (default)
  or SQLite, using AbstractRuntime's RunStore, LedgerStore, CommandStore and
  ArtifactStore.
- **Workflow host** (`src/abstractgateway/hosts/bundle_host.py`): loads `.flow`
  WorkflowBundles and compiles their VisualFlow JSON with
  `abstractruntime.visualflow_compiler`. Bundle mode is the only workflow
  source; store VisualFlows through `/api/gateway/visualflows/*` and publish
  them as bundles.
- **Workflow catalog** (`src/abstractgateway/workflow_catalog.py`): shared,
  immutable workflow versions with admin-managed default pointers and ACLs.
  Catalog runs execute in the caller's runtime; the gateway signs the
  catalog's workflow policy before Runtime receives it.
- **Runner** (`src/abstractgateway/runner.py`): polls the durable command
  inbox, applies commands and ticks RUNNING runs. A filesystem lock
  (`gateway_runner.lock`) prevents double-ticking when API and runner run as
  separate processes; lock state is reported on `GET /api/health`.
- **Runtime settings** (`src/abstractgateway/runtime_config.py`): one store,
  `<data dir>/config/runtime_config.json`, for the settings the consoles, the
  tray and the CLI change: network exposure, `apps.*`, `allow_engine_install`,
  the backlog folder, the backlog exec runner, the process manager and the
  stop kill switch. See [configuration.md](docs/configuration.md).
- **Network exposure** (`src/abstractgateway/network_exposure.py`): resolves the
  bind (`localhost`, `lan`, `internet`) at `serve` time, checks that the auth
  posture allows it, discovers the addresses, and serves the reverse-proxy
  settings the security middleware reads per request.
- **Engines and model downloads** (`src/abstractgateway/engines_install.py`,
  `src/abstractgateway/model_downloads.py`, `routes/engines.py`): engine
  installs are gateway jobs; model downloads, deletes and the catalog come from
  AbstractCore's host job registry and model catalog, which the gateway serves
  unchanged and extends with parent jobs, cancel and an event stream. See
  [engines.md](docs/engines.md) and [model-downloads.md](docs/model-downloads.md).
- **Apps manager** (`src/abstractgateway/apps_manager.py`, `apps_desktop.py`,
  `routes/apps.py`): installs Node.js when needed, installs the browser apps
  from npm, runs them as child processes of the gateway, detects apps started
  elsewhere, installs Code's terminal app, and opens apps signed in through
  one-time handover codes. See [apps.md](docs/apps.md).
- **Host control** (`src/abstractgateway/host_control.py`,
  `self_update.py`): process-wide pause, graceful restart, and in-place
  update checks.
- **Desktop tray** (`src/abstractgateway/tray_supervisor.py`, `tray/`): a
  helper process started by `serve` on a desktop session. It talks to the
  gateway over loopback with a per-process token handed over on stdin. See
  [tray.md](docs/tray.md).
- **Login service** (`src/abstractgateway/os_service.py`, `autostart.py`):
  the per-user LaunchAgent, systemd user unit, XDG autostart entry or Windows
  Run entry that starts plain `abstractgateway serve` at login. See
  [first-run.md](docs/first-run.md#4-start-the-gateway-at-login-optional).
- **Summoned entities** (`src/abstractgateway/entities.py`,
  `routes/entities.py`): persistent entity homes and their lifecycle. See
  [entities.md](docs/entities.md).
- **Operator tooling** (`src/abstractgateway/maintenance/`): reports, triage,
  backlog browsing, the backlog exec runner and the process manager. See
  [maintenance.md](docs/maintenance.md).

## Durable contract (replay-first)

The gateway is **replay-first**:

- the **durable ledger** is the source of truth;
- SSE (`/ledger/stream`) is an optimization; clients reconnect by replaying
  from their last cursor.

```mermaid
sequenceDiagram
  participant C as Client
  participant G as Gateway API
  participant S as Durable stores
  participant R as Runner
  participant RT as AbstractRuntime

  C->>G: POST /api/gateway/runs/start
  G->>S: create run (RUNNING)
  G-->>C: run_id
  C->>G: POST /api/gateway/commands (pause, resume, cancel, emit_event)
  G->>S: append command to the inbox
  loop every poll
    R->>S: read new commands, apply them
    R->>RT: Runtime.tick(run)
    RT->>S: append StepRecords to the ledger
  end
  C->>G: GET /runs/{run_id}/ledger?after=N (replay)
  G-->>C: items + next_after
  C->>G: GET /runs/{run_id}/ledger/stream?after=N (SSE, optional)
```

Evidence: `src/abstractgateway/routes/gateway.py` (ledger endpoints, SSE,
commands) and `src/abstractgateway/runner.py` (command application, ticks).

## Thin-client control plane

Higher-level apps use the gateway instead of importing Runtime or Core:

- `GET /api/gateway/discovery/capabilities` exposes a versioned shared
  contract: run input/history access, media endpoints, voice contracts,
  prompt-cache surfaces, host state, session caches, model residency, and
  `common.readiness` (a compact summary of Gateway-owned surface readiness).
- Provider, model and voice catalogs are routed through the gateway and carry
  a `gateway_catalog_v1` envelope (`catalog` plus canonical `items`) next to
  the lower-layer fields.
- Direct run-scoped media routes cover TTS, STT, image generation, edit and
  upscale, text-to-video, image-to-video, and music.
- Voice listen is a host-capture contract: clients capture audio and then
  upload it or emit an event.
- Model residency is Runtime/provider-owned and Gateway-normalized:
  `GET /models/loaded` and `GET /host/state` relay Runtime's host records and
  add `model_residency_row_v1` rows. The gateway never fabricates residency,
  memory or GPU facts; unavailable sections degrade in-band.
- Reads (`/models/loaded`, `/host/state`, `/host/metrics/*`,
  `GET /sessions/prompt_cache`) serve any authenticated principal; mutations
  (model load/unload/lock/download, prompt-cache clearing, installs) require an
  admin.

## Deployment shapes

```mermaid
flowchart TB
  subgraph Desktop["Your own computer"]
    LS["Login service (optional)"] --> Serve1["abstractgateway serve<br/>API + runner + tray"]
    Serve1 --> AppsLocal["Browser apps on 127.0.0.1"]
  end
  subgraph Server["Server or container"]
    API["abstractgateway serve --no-runner"] --- DD[("shared data dir")]
    Worker["abstractgateway runner"] --- DD
    Proxy["TLS reverse proxy or tunnel"] --> API
  end
```

- **Single process**: `abstractgateway serve` starts the HTTP API and the
  runner. On a desktop session it also starts the tray helper, and it starts
  the browser apps you enabled.
- **Split API and runner**: `abstractgateway runner` (worker) and
  `abstractgateway serve --no-runner` (API) share one data dir, so you can
  restart the API without pausing durable execution.
- **Container**: the GHCR image runs `serve` with user auth; see
  [deployment.md](docs/deployment.md).

Evidence: `src/abstractgateway/cli.py` (flags), `src/abstractgateway/runner.py`
(lock file).

## Security model (summary)

`GatewaySecurityMiddleware` applies to paths starting with `/api/gateway`:

- **Authentication**: Gateway user accounts (browser sessions or user bearer
  tokens), or a shared server/operator token.
- **Origin allowlist**: the loopback origins, the gateway's own LAN origins in
  a network mode, and the `allowed_origins` setting.
- **Abuse resistance**: body size caps, concurrency caps, auth lockouts, audit
  log.

`session_id`, `run_id`, `artifact_id` and memory owner ids are references,
not authorization proofs. With user auth on, each principal runs on its own
data plane; browser apps exchange a user token for an HTTP-only session cookie
plus CSRF token. See [security.md](docs/security.md).

## Evidence (jump-to-code)

- Composition root: `src/abstractgateway/service.py`
- API surface: `src/abstractgateway/routes/` (`gateway.py`, `apps.py`,
  `engines.py`, `network.py`, `entities.py`)
- Runner: `src/abstractgateway/runner.py`
- Stores: `src/abstractgateway/stores.py`
- Security: `src/abstractgateway/security/`
- Settings: `src/abstractgateway/runtime_config.py`
- CLI: `src/abstractgateway/cli.py`

## Related docs

- [getting-started.md](docs/getting-started.md): run the gateway and choose stores
- [configuration.md](docs/configuration.md): every setting and environment variable
- [api.md](docs/api.md): the client contract
- [security.md](docs/security.md): auth, origins, network exposure
- [deployment.md](docs/deployment.md): containers and Compose
- [faq.md](docs/faq.md) and [troubleshooting.md](docs/troubleshooting.md)

---

## docs/faq.md

# AbstractGateway — FAQ

This FAQ answers recurring questions from people integrating or operating
`abstractgateway`. For symptom-by-symptom fixes, see
[troubleshooting.md](docs/troubleshooting.md). For the full API surface, use the
live OpenAPI spec (`/openapi.json`, `/docs`), which is generated from the code.

## Getting started

### What is AbstractGateway?

AbstractGateway is a **durable run gateway** for AbstractRuntime:
- starts runs from `.flow` workflow bundles
- accepts a **durable command inbox** (commands are appended, then applied asynchronously by the runner)
- exposes a **replay-first ledger** API (SSE is optional)
- is the control plane of an AbstractFramework installation: users, providers,
  capability defaults, local engines, models, browser apps and network
  exposure, from the web console, the terminal console, the CLI or the tray

Evidence: `src/abstractgateway/routes/gateway.py`, `src/abstractgateway/runner.py`, `src/abstractgateway/service.py`.

### How does this fit in the AbstractFramework ecosystem?

- **AbstractRuntime** (required): the durable run model + tick loop + stores (declared in `pyproject.toml`).
- **AbstractGateway** (this repo): a deployable HTTP/SSE facade around AbstractRuntime runs (API in `src/abstractgateway/routes/gateway.py`).
- **AbstractCore, AbstractAgent, AbstractMemory** (installed with the gateway): Runtime owns the LLM/tool/media integration boundary; Gateway uses its discovery and run facades for prompt-cache controls, generated and edited media, voice, audio and music, and KG-backed bundle execution (`src/abstractgateway/hosts/bundle_host.py`).
- Higher-level UIs (optional): AbstractFlow (authoring/bundling), AbstractObserver / AbstractCode / thin clients (operations + rendering).

Related repos:
- AbstractFramework: https://github.com/lpalbou/AbstractFramework
- AbstractCore: https://github.com/lpalbou/abstractcore
- AbstractRuntime: https://github.com/lpalbou/abstractruntime

### Do I need AbstractFlow to run workflows?

Not for **bundle mode** (the default).

- Bundle mode loads `.flow` bundles and compiles VisualFlow JSON via `abstractruntime.visualflow_compiler` (no `abstractflow` import).
- You only need `abstractflow` to **author** bundles.

Evidence: `src/abstractgateway/hosts/bundle_host.py` (bundle compilation).

### Can the gateway run VisualFlow JSON files directly?

No. Bundle mode is the only workflow source:

- input: one `.flow` file or a directory of `*.flow` bundles
  (`ABSTRACTGATEWAY_FLOWS_DIR`; the shipped bundles when unset);
- versioning: bundles are addressed as `bundle_id@bundle_version`.

Store VisualFlows through `/api/gateway/visualflows/*` and publish them as a
`.flow` bundle with `POST /api/gateway/visualflows/{flow_id}/publish`.

Evidence: `src/abstractgateway/service.py`, `src/abstractgateway/hosts/bundle_host.py`.

## Security

### Do I need to configure authentication?

Not on your own computer. A plain `abstractgateway serve` with nothing
configured binds `127.0.0.1`, turns user accounts on, creates the admin
account and prints a one-time sign-in link ([first-run.md](docs/first-run.md)).

To let other devices reach the gateway, choose the **network setting**
(`abstractgateway network set lan`, the console's **Network** tab or the
tray); user accounts stay on. A `--host 0.0.0.0` without any auth posture is
refused ([troubleshooting.md](docs/troubleshooting.md#serve-says-refusing-to-start-no-sign-in-would-protect-this-gateway)).

`ABSTRACTGATEWAY_AUTH_TOKEN` is a shared server/operator token that maps to
`local-admin`; it is not a browser sign-in token. Use user accounts for the
console and the browser apps.

Evidence: `src/abstractgateway/cli.py` (`serve`), `src/abstractgateway/first_run.py`.

### What is the difference between the bind address and the allowed origins?

- The bind address decides which network interfaces the server listens on.
  Set it with the network setting (`localhost`, `lan`, `internet`);
  `serve --host` overrides it for one run.
- The origin allowlist decides which browser pages may call `/api/gateway/*`
  (requests that carry an `Origin` header). Add origins with
  `abstractgateway network set --allowed-origins …`;
  `ABSTRACTGATEWAY_ALLOWED_ORIGINS` in the launch environment pins it.

Evidence: CLI flags in `src/abstractgateway/cli.py`, origin checks in `src/abstractgateway/security/gateway_security.py`.

### Why do I get `401` / `403` / `429` / `413` from `/api/gateway/*`?

See the status table in
[troubleshooting.md](docs/troubleshooting.md#401-403-429-or-413-from-apigateway).

### Can I disable security (dev only)?

Prefer keeping security enabled, even in dev.

If you must relax it:
- disable the gateway security layer entirely: `ABSTRACTGATEWAY_SECURITY=0`
- or (safer) allow unauthenticated reads on loopback only: `ABSTRACTGATEWAY_DEV_READ_NO_AUTH=1`
- or fine-tune: `ABSTRACTGATEWAY_PROTECT_READ=0`, `ABSTRACTGATEWAY_PROTECT_WRITE=0`

Evidence: env policy loader in `src/abstractgateway/security/gateway_security.py`.

## Storage

### Where is data stored?

Everything is rooted at `ABSTRACTGATEWAY_DATA_DIR`:

- File backend (default): `run_*.json`, `ledger_*.jsonl`, `commands.jsonl`, `commands_cursor.json`, plus `artifacts/`
- SQLite backend: a single DB file (default `<DATA_DIR>/gateway.sqlite3`) plus `artifacts/`
- Gateway-generated workflows (e.g. schedules): `dynamic_flows/`
- Per-run workspaces (when `workspace_root` is not provided at start): `workspaces/`

Evidence: `src/abstractgateway/stores.py`, `src/abstractgateway/routes/gateway.py` (`start_run` workspace default), `src/abstractgateway/hosts/bundle_host.py` (dynamic flows).

### How do I switch to SQLite? Can I migrate?

- Switch by setting `ABSTRACTGATEWAY_STORE_BACKEND=sqlite` (and optionally `ABSTRACTGATEWAY_DB_PATH`).
- Migrate file → SQLite with `abstractgateway migrate --from=file --to=sqlite ...` (best-effort local migration).

Evidence: `src/abstractgateway/stores.py`, `src/abstractgateway/migrate.py`, CLI wiring in `src/abstractgateway/cli.py`.

## Runs, ledger, commands

### What is the ledger, and what does `after` mean?

- The ledger is an **append-only** list of step records.
- `after` is a cursor meaning “number of records already consumed”; responses return `next_after`.
- SSE streams ledger updates, but clients should always reconnect by replaying from the last cursor.

Evidence: `GET /runs/{run_id}/ledger` and `/ledger/stream` in `src/abstractgateway/routes/gateway.py`.

### How do durable commands work? When do they take effect?

`POST /api/gateway/commands` appends a command record to a durable inbox.
The background runner polls the inbox and applies commands asynchronously.

Supported command types:
`pause|resume|cancel|emit_event|update_schedule|compact_memory`

Evidence: `submit_command` in `src/abstractgateway/routes/gateway.py`, command application in `src/abstractgateway/runner.py`.

### Can I schedule a workflow to run periodically?

Yes (bundle mode).

Use `POST /api/gateway/runs/schedule` to start a scheduled parent run that launches the target workflow as child runs over time.

Notes:
- `interval` supports compact durations like `15m`, `1h`, `2d`.
- If `interval` is set and `repeat_count` is omitted, the schedule repeats forever (until you cancel it).
- To stop the schedule, cancel the scheduled parent run via `POST /api/gateway/commands` with type `cancel`.

Evidence: `ScheduleRunRequest` + `start_scheduled_run` in `src/abstractgateway/routes/gateway.py`.

## Bundles and workflow execution

### How do I run a specific bundle version?

When starting runs in bundle mode you can select versions in two ways:
- pass `bundle_id` + `bundle_version`
- or pass a namespaced `flow_id` like `bundle@version:flow` (this also works for selecting “latest” via `bundle:flow`)

Evidence: bundle selection in `src/abstractgateway/hosts/bundle_host.py` (`start_run`).

### Where does a bundle's default model come from?

From the execution-host `input.text` capability route (the console's
**Multimodal** tab, **Use as default** in **Models**, or
`abstractgateway-config set-default input.text …`). A flow can also pin a
provider and model on its `llm_call` or `agent` nodes. When nothing is
configured, the run fails with a clear configuration error; see
[troubleshooting.md](docs/troubleshooting.md#llm-nodes-but-no-default-providermodel-is-configured).

Evidence: `src/abstractgateway/provider_defaults.py`, `src/abstractgateway/hosts/bundle_host.py`.

### Why do tool calls not execute?

In bundle mode, tool execution is controlled by:

- `ABSTRACTGATEWAY_TOOL_MODE=approval` (default): safe tools execute immediately; dangerous/unknown tools pause for explicit approval.
- `ABSTRACTGATEWAY_TOOL_MODE=passthrough`: approval required for *all* tools (including safe ones); after approval, the runtime executes the tool batch in-process.
- `ABSTRACTGATEWAY_TOOL_MODE=delegated`: tools are not executed locally; workflows enter a durable `JOB` wait for external executors.
- `ABSTRACTGATEWAY_TOOL_MODE=local` (or `local_all`): tools execute inside the gateway process without approval (dev only; unsafe).

Evidence: tool executor selection in `src/abstractgateway/hosts/bundle_host.py`.

### How do I enable generated images, edited images, generated music, or other Runtime-managed multimodal outputs?

Use the base install for the Gateway control plane and remote/provider-backed
routes:

```bash
pip install abstractgateway
```

The base install includes Runtime-owned tool and multimodal integration and can
proxy to configured remote/provider routes. Remote embeddings are supported
through the `embedding.text` capability route when it points at OpenAI,
OpenRouter, Portkey, LM Studio, vLLM, another OpenAI-compatible endpoint, or a
remote AbstractCore server. Local sentence-transformer embeddings and
hardware-local image, audio, voice, and music engines are explicit opt-ins so a
light Linux install does not pull PyTorch/CUDA packages. Use
`abstractgateway[apple]` or `abstractgateway[gpu]` only when this Gateway host
should execute those local engines itself.

Generated images are available both inside Runtime workflows and through
Gateway's direct run-scoped endpoint:

```text
POST /api/gateway/runs/{run_id}/images/generate
POST /api/gateway/runs/{run_id}/images/edit
POST /api/gateway/runs/{run_id}/images/upscale
POST /api/gateway/runs/{run_id}/videos/generate
POST /api/gateway/runs/{run_id}/videos/from_image
```

The direct image and video endpoints use Runtime/Core output selectors and
store the result as a run artifact, so they still require a configured
Runtime-compatible vision/video backend. Image dimensions are optional
passthrough overrides; clients should not inject a default `512x512` request
because supported sizes depend on the selected provider/model. Image/video
routes also accept optional batch `count` / `n`, `seeds`, and ordered
`lora_adapters`; video routes additionally accept `flow_shift`, and batch
responses return `image_artifacts` / `video_artifacts` alongside the
compatibility singular artifact fields. Use
`GET /api/gateway/vision/adapters` when a thin client needs the compatible
installed adapter catalog for a selected provider/model/task. For long media
runs, stream the returned `child_run_id` ledger and watch `abstract.progress`
records. Image progress is best-effort and may only show start/complete when the
backend does not expose step progress.

Generated music is exposed through Gateway's direct Runtime child-run route and
its thin-client discovery/catalog contract:

```text
POST /api/gateway/runs/{run_id}/music/generate
GET /api/gateway/audio/music/providers
GET /api/gateway/audio/music/models
```

Higher apps should feature-detect music from
`capabilities.contracts.flow_editor.media.generated_music` or
`capabilities.contracts.assistant.media.generated_music`.

### What is `voice.listen` in the capabilities contract?

`voice.listen` is not a live server-side microphone transport. It is a
higher-app contract that tells clients how to handle local capture:

- capture audio on the client or host device
- either upload it to `POST /api/gateway/runs/{run_id}/audio/transcribe`
- or emit the configured event/command into the run contract

This keeps live capture UX owned by higher apps such as Assistant or Observer
while Gateway stays responsible for durable runs, artifacts, and transcription.

### Are catalog responses normalized by Gateway?

Catalog routes carry a Gateway-owned envelope: `catalog` (contract
`gateway_catalog_v1`, version, kind, scope and source) plus a canonical
`items` array. Read those. The lower-layer fields (`models`, `providers`,
`provider_models`, `profiles`, `voices`) stay in the payload for
compatibility, and their shapes differ from route to route.

The capabilities contract also carries `common.readiness`, a compact summary
of Gateway-owned surface readiness. Deeper backend and provider diagnostics
belong to Runtime and AbstractCore, and the gateway reports them only when
those layers expose them. See [api.md](docs/api.md#discovery-endpoints-optional).

### What does Gateway session prompt-cache orchestration include?

The `/api/gateway/prompt_cache/*` routes expose provider/model prompt-cache
controls when the active AbstractCore integration supports them. Gateway also
provides session lifecycle routes under
`/api/gateway/sessions/{session_id}/prompt_cache/*` for status, prepare,
rebuild, and clear using deterministic session keys.

This is Gateway-owned naming and orchestration over provider controls, not a
provider-independent local KV cache or full CachedSession persistence system.

### Which KG memory backend should I use?

Keep the default `lancedb` backend for durable, vector-capable memory; use
`memory` for process-local dev/test memory; set
`ABSTRACTGATEWAY_MEMORY_STORE_BACKEND=sqlite` only when your installed
AbstractMemory exposes `SQLiteTripleStore`. A fresh persistent store is
reported as available: structured queries return no matches until a flow
asserts triples.

Evidence: `src/abstractgateway/memory_store.py`.

## Desktop tray

### How do I get the menu bar / tray icon, and why is there none?

Install the extra (`pip install "abstractgateway[tray]"`) and start the
gateway with `abstractgateway serve` on a desktop session. The icon has no
on/off setting: while the gateway serves a desktop that can show it, it is
there. The boot log says `Desktop tray: started (pid …)` or names the reason
it is absent (`missing_dependency`, `headless`, `dev_reload`, `runner_only`);
the console's **Resources** tab shows the same. On Linux the GTK/AppIndicator
bindings are needed; GNOME also needs the AppIndicator extension. Details:
[tray.md](docs/tray.md) and
[troubleshooting.md](docs/troubleshooting.md#there-is-no-tray-icon).

### What does "Pause Workflows" actually stop?

New workflow steps. Runs, schedules and messages from connected apps are
still accepted and wait; a step already inside an LLM or tool call finishes
first; the console and the API keep answering; summoned entities' own loops
are separate processes and keep their schedule. Pause persists across
restarts until you resume (tray, console banner, or
`POST /api/gateway/host/resume`).

## Deployment

### How do I run API and runner as separate processes?

Run:

```bash
abstractgateway runner
abstractgateway serve --no-runner --host 127.0.0.1 --port 8080
```

The runner uses a lock file (`gateway_runner.lock`) to prevent double-ticking on the same data dir. A locked-out runner keeps retrying acquisition in the background; a newly started process asks a live holder to yield (newest process wins), and the holder heartbeats the lock file so `GET /api/health` can report whether anyone is actually ticking the data dir (`runner.runners[].status`).

Evidence: CLI flag `--no-runner` in `src/abstractgateway/cli.py`, lock lifecycle (`_run`/`_acquire_singleton_lock`/`runner_status`) in `src/abstractgateway/runner.py`.

## Related docs

- Docs index: [README.md](docs/README.md)
- Troubleshooting: [troubleshooting.md](docs/troubleshooting.md)
- Getting started: [getting-started.md](docs/getting-started.md)
- API overview: [api.md](docs/api.md)
- Security: [security.md](docs/security.md)
- Configuration: [configuration.md](docs/configuration.md)
- Architecture: [architecture.md](docs/architecture.md)
- Operator tooling (optional): [maintenance.md](docs/maintenance.md)

---

## docs/maintenance.md

# AbstractGateway — Operator tooling (optional)

`/api/gateway/*` includes “operator tooling” endpoints used by higher-level UIs and workflows (reports inbox, triage queue, backlog helpers, process manager, file/attachment helpers, …). These features are **not required** to use AbstractGateway as a durable run gateway.

This document groups the main non-core features and how to enable them safely.

## Safety model (read this first)

Some endpoints can:
- write files under `ABSTRACTGATEWAY_DATA_DIR`
- read files from configured workspace mounts
- start/stop local processes (process manager)
- execute queued backlog tasks (backlog exec runner)

Only enable these features on **trusted machines** and keep gateway auth enabled.
Security enforcement for `/api/gateway/*` is in `src/abstractgateway/security/gateway_security.py`.

## Reports inbox + triage queue

Implemented in `src/abstractgateway/routes/gateway.py` and `src/abstractgateway/maintenance/*`.

Key endpoints:
- `POST /api/gateway/bugs/report`
- `POST /api/gateway/features/report`
- `GET /api/gateway/reports/bugs` / `GET /api/gateway/reports/features`
- `POST /api/gateway/triage/run`
- `GET /api/gateway/triage/decisions`

CLI helpers:
- `abstractgateway triage-reports` (scan inbox → decision queue; optional draft writing)
- `abstractgateway triage-apply <decision_id> approve|reject|defer`

Notification helpers used by `triage-reports --notify`:
- Telegram: `ABSTRACT_BACKLOG_TELEGRAM_CHAT_ID` or `ABSTRACT_TRIAGE_TELEGRAM_CHAT_ID`
- Email recipients: `ABSTRACT_BACKLOG_EMAIL_TO` or `ABSTRACT_TRIAGE_EMAIL_TO`
- Optional email account override: `ABSTRACT_BACKLOG_EMAIL_ACCOUNT` or `ABSTRACT_TRIAGE_EMAIL_ACCOUNT`

Evidence: CLI wiring in `src/abstractgateway/cli.py`.

## Backlog browsing/editing

The gateway also exposes endpoints that read/write backlog Markdown files in a folder that contains `docs/backlog/*`.

They work out of the box: without a setting the gateway uses its own folder, `<data dir>/backlog/`, created with a starter overview and item template on first use. To point it at a project checkout:

```bash
abstractgateway config set triage_repo_root /path/to/your/repo   # saved; or Continuum Settings, or the console
abstractgateway serve --backlog-root /path/to/your/repo          # this run only
```

See [configuration.md](docs/configuration.md#backlog-folder-exec-runner-and-process-manager-continuum) for the resolution order and the three doors.

Evidence: `resolve_backlog_root` in `src/abstractgateway/runtime_config.py`, used by `src/abstractgateway/routes/gateway.py` (process manager + backlog endpoints) and `src/abstractgateway/maintenance/backlog_exec_runner.py`.

## Backlog execution runner (high risk; disabled by default)

The backlog exec runner consumes queued execution requests under `<DATA_DIR>/backlog_exec_queue/` and executes them (optionally using the `codex` CLI).

Enable (applies at once on a running gateway):

```bash
abstractgateway config set backlog_exec_runner on
abstractgateway config set executor codex        # codex | claude | cursor-agent | abstractcode
```

or `abstractgateway serve --exec-runner on` for one run, or Continuum → Settings → Gateway administration.

Additional knobs (see `BacklogExecRunnerConfig.from_env()`):
- `ABSTRACTGATEWAY_BACKLOG_EXEC_POLL_S`
- `ABSTRACTGATEWAY_BACKLOG_EXEC_WORKERS`
- `ABSTRACTGATEWAY_BACKLOG_CODEX_BIN`
- `ABSTRACTGATEWAY_BACKLOG_CODEX_MODEL`
- `ABSTRACTGATEWAY_BACKLOG_CODEX_REASONING_EFFORT` (`low|medium|high|xhigh`)
- `ABSTRACTGATEWAY_BACKLOG_CODEX_SANDBOX`
- `ABSTRACTGATEWAY_BACKLOG_CODEX_APPROVALS`

Evidence: `src/abstractgateway/service.py` (runner startup), `src/abstractgateway/maintenance/backlog_exec_runner.py`.

## Process manager (dev-only; disabled by default)

The process manager can start/stop a small allowlisted set of local processes and tail logs. It is intended for **trusted dev machines**.

Notes:
- **Process control** (`/api/gateway/processes`, start/stop, log tail) is **repo-root scoped** for safety and assumes a monorepo-style checkout (scripts like `./build.sh`, `./agw-uat.sh`, …).
- **Env-var management** (`/api/gateway/processes/env`) is **repo-root independent** and works in packaged installs (it persists under `ABSTRACTGATEWAY_DATA_DIR`).

Enable:

```bash
abstractgateway config set process_manager on

# Process control only: the AbstractFramework checkout it manages
abstractgateway config set triage_repo_root "$PWD"
```

Process control stays off while the backlog folder is the gateway's own default folder (it is not a checkout).

Optional config path:

```bash
export ABSTRACTGATEWAY_PROCESS_MANAGER_CONFIG="$PWD/runtime/gateway/processes.json"
```

Endpoints:
- `GET /api/gateway/processes` (requires the backlog folder set to a checkout: `triage_repo_root`)
- `POST /api/gateway/processes/{id}/start|stop|restart|redeploy`
- `GET /api/gateway/processes/{id}/logs/tail`
- `GET /api/gateway/processes/env` (metadata only; never returns values; does not require repo root)
- `POST /api/gateway/processes/env` (write-only set/unset for allowlisted keys; does not require repo root)

Evidence: `src/abstractgateway/routes/gateway.py` (endpoint guards) and `src/abstractgateway/maintenance/process_manager.py`.

### Env var allowlist (write-only)

Env var editing is allowlist-only and values are write-only (they are never returned to the client). Overrides are persisted on the gateway host under:
- `<ABSTRACTGATEWAY_DATA_DIR>/process_manager/env_overrides.json`

When the gateway starts with the process manager on (`process_manager` setting), it loads and applies persisted overrides to its own `os.environ` (best-effort).

To extend the allowlist, update:
- `src/abstractgateway/maintenance/process_manager.py` → `managed_env_var_allowlist()`

## File + attachment helpers (thin-client support)

The gateway exposes helpers used by thin clients and workflows:
- Workspace policy: `GET /api/gateway/workspace/policy`
- File access: `GET /api/gateway/files/search|read|skim`
- Attachments: `POST /api/gateway/attachments/ingest` and `POST /api/gateway/attachments/upload`

Workspace scope is **operator-controlled at gateway launch**:

- Default (safe): thin clients cannot expand filesystem scope. If a run is started without `workspace_root`, the gateway creates a per-run workspace under `<ABSTRACTGATEWAY_DATA_DIR>/workspaces/<uuid>`, and filesystem-ish tool calls are scoped to that workspace (`workspace_access_mode=workspace_only`).
- Allowlist additional roots for file helpers via `ABSTRACTGATEWAY_WORKSPACE_DIR` + `ABSTRACTGATEWAY_WORKSPACE_MOUNTS`.
- Permissive mode (trusted machines only): enable client-provided `workspace_*` overrides (including `all_except_ignored`) via `ABSTRACTGATEWAY_ALLOW_CLIENT_WORKSPACE_SCOPE=1` (or `ABSTRACTGATEWAY_TRUST_CLIENT_WORKSPACE_SCOPE=1`).

Note: `/api/gateway/files/*` + `/api/gateway/attachments/ingest` ignore client-provided scope overrides unless client overrides are enabled.

Server-side workspace mounts (operator-controlled):

```bash
# newline-separated: name=/absolute/path
export ABSTRACTGATEWAY_WORKSPACE_MOUNTS=$'repo=/abs/path/to/repo\\ndata=/abs/path/to/data'
```

Evidence: `_workspace_mounts()` and related policy helpers in `src/abstractgateway/routes/gateway.py`, tests in `tests/test_gateway_workspace_policy_enforcement.py`.

## Bridges (Telegram, email)

Background bridges can ingest external messages and start durable runs (thin-client semantics), and may also emit events for specialized workflows.

Enable (Telegram):
- `ABSTRACT_TELEGRAM_BRIDGE=1`
- transport + credentials depend on configuration (see `src/abstractgateway/integrations/telegram_bridge.py`):
  - Bot API (default when token is present): `ABSTRACT_TELEGRAM_BOT_TOKEN=...`
  - TDLib (E2EE): `ABSTRACT_TELEGRAM_TRANSPORT=tdlib` + TDLib setup
- access control (fail-closed defaults):
  - DMs default to allowlist: set `ABSTRACT_TELEGRAM_ALLOWED_USERS=...` (numeric Telegram user_id; discover via `/whoami`)
  - Groups default to disabled (opt-in via `ABSTRACT_TELEGRAM_GROUP_POLICY=allowlist|open`)
- Optional: override which workflow to run per message:
  - `ABSTRACT_TELEGRAM_BUNDLE_ID=...`
  - `ABSTRACT_TELEGRAM_FLOW_ID=...`
  - Default (when unset): shipped `basic-agent` bundle entrypoint.
- Tool approvals:
  - `ABSTRACTGATEWAY_TOOL_MODE=approval` (default): safe tools run in-process; dangerous/unknown tools require `/approve` or `/deny`.
  - `ABSTRACTGATEWAY_TOOL_MODE=passthrough`: approval required for *all* tools (including safe ones); after approval, the runtime executes the tool batch in-process.
  - `ABSTRACTGATEWAY_TOOL_MODE=delegated`: tools are not executed locally; workflows enter a durable `JOB` wait for external executors.
- optional knobs:
  - Telegram-only routing override: `ABSTRACT_TELEGRAM_MODEL` (and optionally `ABSTRACT_TELEGRAM_PROVIDER`)
  - Durable history limit: `ABSTRACT_TELEGRAM_MAX_HISTORY_MESSAGES`
  - `/reset` controls: `ABSTRACT_TELEGRAM_RESET_DELETE_MESSAGES`, `ABSTRACT_TELEGRAM_RESET_DELETE_MAX`, `ABSTRACT_TELEGRAM_RESET_MESSAGE`

Enable (Email):
- `ABSTRACT_EMAIL_BRIDGE=1`
- IMAP credentials + polling config (see `src/abstractgateway/integrations/email_bridge.py`)

Evidence: bridge startup in `src/abstractgateway/service.py` (`start_gateway_runner`).

## Email inbox endpoints (AbstractObserver Inbox → Email)

If email accounts are configured on the gateway host, the gateway exposes account-scoped endpoints used by AbstractObserver to list/read/send emails:
- `GET /api/gateway/email/accounts`
- `GET /api/gateway/email/messages`
- `GET /api/gateway/email/messages/{uid}`
- `POST /api/gateway/email/send`

These endpoints proxy through Gateway's Runtime comms facade and never accept arbitrary IMAP/SMTP host/user secrets from the browser.

Configuration notes:
- Multi-account: set `ABSTRACT_EMAIL_ACCOUNTS_CONFIG=/path/to/emails.yaml` (recommended).
- Single-account env fallback: `ABSTRACT_EMAIL_IMAP_*` / `ABSTRACT_EMAIL_SMTP_*`.

Evidence: `/api/gateway/email/*` routes in `src/abstractgateway/routes/gateway.py` which call the Runtime AbstractCore comms facade.

## Related docs

- API overview (core client contract): [api.md](docs/api.md)
- Security: [security.md](docs/security.md)
- FAQ: [faq.md](docs/faq.md)

---

## docs/entities.md

# Summoned entities

The gateway owns the lifecycle of **summoned entities** — persistent
identities (like "Castor") that live across sessions, users, and workplaces;
each summon is a re-adoption of the same self. An entity is not a chatbot configuration: it is a home
directory holding everything the entity is and has lived, plus the lifecycle
surface to create, inspect, verify, and summon it.

This page uses human words first, API names in parentheses.

## What an entity is

A summoned entity lives in two files at its **home**:

- **Its memory** (`memory.sqlite3`) — the involuntary record: everything that
  happens to it, its standing feelings about anything it has experienced
  (people, tools, ideas, places, even a time of day), and its always-on
  identity core planted from a spark document. Things happen to you and you
  are forced to remember; that is what forges who you are.
- **Its diary — "the book"** (`home.sqlite3`) — the voluntary record: what
  the entity *elects* to write. First-person, hash-chained, written only by
  the entity itself, and **never deletable — there is no delete surface
  anywhere in the code, on disk, in the CLI, or over HTTP.**

Next to those live the **attested seed** (`spark.yaml`, stored byte-verbatim
at creation — the spark is engrammed once and kept for life) and the
gateway's `manifest.json` (an internal record: the engraved owner key —
`entity:<name>`, or `entity:<name>@<home-id>` on homes created with that
form, kept for life because journals are append-only — plus creation time, the spark hash, and reserved fields for
the future key/signature work).

**The entity's ID — the handle — is `<name>@<gateway ip>`**, e.g.
`castor@192.168.1.146`: the name at its home
gateway's current LAN address (or the operator-declared
`ABSTRACTGATEWAY_DECLARED_ADDRESS`). That is what every operator surface
shows as "Entity ID". The manifest string above is a birth marker, never
the id; the address is never written at rest, so moving the gateway never
touches a record.

**Copying the home directory moves the entity.** The `home_id` in the
manifest names the birth home and is kept on copy; re-homing is deferred to
the keys work.

## Lifecycle surface

CLI (there is deliberately no delete verb):

```bash
abstractgateway entity create --name Castor [--spark spark.yaml] [--data-dir ./runtime]
abstractgateway entity list
abstractgateway entity inspect Castor      # who it is, what it wrote, how it feels
abstractgateway entity card Castor         # the identity card: a page to know your companion
abstractgateway entity verify Castor       # both attestation chains + the spark hash
```

HTTP (rides the same auth as every `/api/gateway/*` endpoint):

| Method | Path | What it does |
|--------|------|--------------|
| POST | `/api/gateway/entities` | Create: lint the spark, store it verbatim, plant the identity core (`engram`), write the manifest. Idempotent for the same spark; a changed document is **refused** (409) — identity does not silently drift. |
| GET | `/api/gateway/entities` | List homes. |
| GET | `/api/gateway/entities/{name}` | Inspect: the folded identity core, recent diary gists, top standings, and the wake reasons — open questions (curiosity), open problems (something wrong), incubating ideas (direction). Pure reads — inspecting never counts as the entity *using* its memory. |
| GET | `/api/gateway/entities/{name}/card` | The identity card ("something to know our companion"): the engine compositor's sections — identity, age+context, current state (a window, never a point), likes/dislikes (G+ and G− separate; ambivalence preserved), open/resolved questions (resolution is the entity's own act), key moments, discoveries — each with provenance, plus gateway overlays (name/age, operator state, mind substrate, host moments). `?as_of=<seq>` anchors the card at a journal moment ("who was he at seq 500"). Pure reads. |
| GET | `/api/gateway/entities/{name}/verify` | Verify the book's hash chain, the graph projections against the book, the spark document against the engrammed marker, and the manifest. |
| POST | `/api/gateway/entities/{name}/summon` | Summon the entity into a work session (below). |
| POST | `/api/gateway/entities/{name}/chat/open` | Open a hosted conversation (the web chat backend; same turn loop as `entity chat`). Auto-yields the entity's own-time loop like `--pause-loop`; one live session per home; a refused prelude aborts verbatim. |
| POST | `/api/gateway/entities/{name}/chat/{chat_id}/turn` | One honest turn. `tools_ran` in the response is driver-authored data — what actually executed, never derived from the reply prose. |
| POST | `/api/gateway/entities/{name}/chat/{chat_id}/close` | End the visit: reflection pass (feelings move there), close summary, the own-time loop woken if the open yielded it. |
| GET | `/api/gateway/entities/{name}/chat` | Is a visit open on this home right now? (one life, one summon) |
| GET | `/api/gateway/entities/{name}/replay` | The observable life as a bounded stream (NDJSON): every memory-journal moment (formations, recalls, feelings, belief changes) plus gateway host markers (summons, refused preludes), in one strict sequence. |
| GET | `/api/gateway/entities/{name}/replay/stream` | The same stream as a live tail (SSE; `Last-Event-ID` resumes exactly). History scrub and realtime are one format — a viewer that renders one renders both. |

Progressive disclosure: `inspect` shows diary **gists** only. The verbatim
prose stays in the book and is fetched by the entity itself during a session
(`DIARY_READ`), never bulk-exported by inspection.

### The task inbox (tasks left with an entity)

Tasks are durable facts in the home — append-only events in
`<home>/task_inbox.jsonl`, folded at read (state is never rewritten in
place; two writer processes exist — the door and the own-time loop — so
every write is one flock-guarded appended line):

| Method | Path | What it does |
|--------|------|--------------|
| GET | `/api/gateway/entities/{name}/tasks` | The folded inbox: tasks with current status (`pending\|taken\|done\|parked`), chronological. `exists: false` when no inbox was ever created. |
| POST | `/api/gateway/entities/{name}/tasks` | Leave a task (admin): `{title, brief?, workflow?, backlog_ref?}`. `origin`/`by` are STAMPED from the authenticated principal — the body carries no origin field to forge. Marker-first (`task_inbox_changed`); a marker failure refuses the write. |
| POST | `/api/gateway/entities/{name}/tasks/{task_id}/status` | Advance a task (admin): `{status, note?}`. Validated before the marker; unknown tasks 404. |

A visit can leave tasks at close: `POST .../visit/{run_id}/close` accepts
`tasks: [{title, brief?, …}]`, recorded with origin `visit:<run_id>` only
after the close COMPLETED (recording failures surface as a labeled warning
in the response — the finished close is never misreported as a 5xx).

### Skills (what an entity is taught)

Beyond the always-verbatim capability map, an entity's skills are an
operator SELECTION in the home (`<home>/skills.yaml`: `[{name, phases?}]`,
phases from the ruled four, absent = selected everywhere), resolved
server-side against the abstractskill shelf through the same trust gate as
every other lane — default-requested, never trust-bypassed:

| Method | Path | What it does |
|--------|------|--------------|
| GET | `/api/gateway/entities/{name}/skills` | One resolved truth for every UI: the stored selection, roster rows (name, description, trust_level, requires_review, tree_hash, source) with labeled verdicts for anything unresolvable, and the capability-matrix payload (a global selection renders all four phases with identical cells). |
| PUT | `/api/gateway/entities/{name}/skills` | Replace the selection (admin). Marker-first (`skills_selection_changed`, old/new names+phases — never skill bodies); the response is the resolved view so a typo or blocked skill is visible the moment it is written. |

`POST /api/gateway/entities` accepts the same selection at **birth**
(`skills: [{name, phases?}]`) so a new entity carries teaching from day
one. Selections pin by NAME and resolve to the current shelf state at
read/summon time — a shelf re-pin reaches homes on their next resolution,
never by bulk push. Delivery into entity prompts (progressive disclosure:
names ride the base, bodies activate on demand) waits on the runtime's
composition-slot election; the selection file rests in the home until then.

The entity roster (`GET /entities`) carries `pending_tasks`
render-when-present: the field exists only for homes that have an inbox —
an entity never handed a task shows no field, not a zero. The file schema
is the cross-package contract for the runtime's day-open reader (the R-C
loop half): event lines `{"event": "added"|"status", "task_id", …}` — see
`abstractgateway/entity_tasks.py` for the authoritative shapes. The inbox
records facts; whether a task opens the work phase is decided by the entity's
loop.

## Drive ratios (cognition health)

`GET /entities/{name}/cognition` carries a `drives` block — memory's
`cognition_health()` fold over the home's full ladder: questions
open/resolved, problems open/repaired, interests open/explored, each with
a `ratio` that is `null` when the category is empty (a life with no
questions has *no* ratio, never a fabricated 100%). Ratios are data —
never-100% is the design (an entity with nothing open has no pull
forward), so consoles render an amber cue at saturation, not a success
state. Render-when-present: the key is absent (with a labeled `#FALLBACK`
warning) when the engine predates the read or the read fails.

The roster (`GET /entities`) carries the same `drives` block for **warm
homes only** — homes already open in this gateway process. The roster is
deliberately file-cheap and never opens a store; a cold home shows no
field (absent ≠ zero), and `/cognition` always serves the block (and
warms the home). The gateway console renders the two ratio bars in the
entity Overview panel from the same `/cognition` read.

## Summoning

Summoning opens a work session *as* the entity:

1. The gateway renders the **identity header** ("summon prelude": who you
   are, your values in ordinal precedence, your recent diary lines, your
   standing feelings). This is a pure read.
2. **A refused prelude aborts the summon.** If the budget cannot fit the
   identity core, the request fails (409) with the reason verbatim — *"a
   truncated core is a different person"*. There is no fallback to a
   truncated header.
3. The run starts with the **reserved-seats posture**: identity is always
   present in the working set (`self_fraction > 0`), and presence never
   counts as use — the lifetime counters keep measuring lived experience.
4. The prelude leads the run's system prompt; the work brief is the prompt.

```bash
curl -X POST http://localhost:8080/api/gateway/entities/castor/summon \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"prompt": "Review the backup strategy for the home lab."}'
```

The response carries the `run_id`, the session id, and the rendered prelude.
The run behaves like any other gateway run (ledger stream, waits, cancel).

## The deposit gate (actor by channel, never by payload)

Every write to an entity's home passes a door that derives the **actor from
the channel the request arrived on** — never from request payloads:

- `workplace` — summoned work sessions. Routine feelings only (small
  amplitudes); no identity writes; no belief retraction; no diary forgery
  (the diary is written through the entity's own elected act, `DIARY_WRITE`,
  whose author is bound at construction).
- `entity-reflection` — runs the home itself spawns (the future heartbeat /
  reflection loop). This is where identity evolves.
- `operator` — the authenticated admin surface (CLI, admin HTTP).

Mechanics: the summon endpoint signs a stamp (HMAC, per-data-root secret)
over the entity, channel, session, **and run id**; the routing layer verifies
it before any home opens. A payload claiming a privileged actor fails loudly.
The stamp authenticates "minted by this gateway" — remote workplace
authentication is the deferred key/signature work. This is the
[AI-fingerprints](https://medium.com/@lpalbou/the-rise-of-cognitive-architectures-and-the-need-for-ai-fingerprints-fcee286c0c33)
direction applied at the door: identity verified at the boundary, never
self-claimed.

The gate also enforces:

- **The privacy boundary**: a session's recall ladder may only contain the
  entity's own scopes plus that session's scope — never another user's.
- **The summon posture and the identity floor**: a recall budget that omits
  `self_fraction` gets the posture default; an explicit `self_fraction <= 0`
  is rejected ("identity is always present for a summoned entity"). Below
  the hard floor (5% and at least one reserved seat) nobody goes; reducing
  identity presence below the default — *hyperfocus*, a conscious and risky
  tradeoff — is the entity's own act (entity-reflection channel only,
  reversible by construction: per-session, never persisted). A stripped
  entity may act out of character; workplaces cannot request it.
- **Anchors**: journal time-travel anchors beyond this entity's own journal
  are rejected.
- **Verified participants**: who is present in the session is stamped by the
  door from the authenticated principal and flows into recall and formation
  (the situation contract) — payload claims are dropped.

## Observing a life (the replay stream)

The replay endpoints serve the memory engine's frozen stream (one envelope
shape; the journal is the stream) merged with gateway **host markers** —
moments that are deliberately invisible to the entity's own journal because
nothing happened memory-side (a prelude render is a pure read): summons,
refused preludes. Markers are gateway bookkeeping (like run ledgers), stored
outside the home directory, and take fractional sequence positions so they
interleave without ever colliding with the journal.

Privacy: the engine redacts diary display blocks at the source
(`{"redacted": "diary"}`). These HTTP endpoints serve the OPERATOR audience,
so the serving end resolves that marker into the entry's **gist** — the
entity's one-sentence summary, or the first ~120 characters when no explicit
gist exists (`_operator_diary_display`).
So the operator sees the diary's topology (the entity wrote *something*, it
connects to *something*) **and a gist of what it was about** — never the full
verbatim prose, which stays in the book and is fetched one entry at a time
through the operator diary door (`GET .../diary/{entry_id}`, a marker-first
recorded read). The redaction marker itself is never served raw.

### Reading a record's verbatim

`GET /api/gateway/entities/{name}/records/{graph_id}/verbatim` serves the
full stored text behind a memory record (the digest is prompt currency;
the verbatim is the lossless original). Three shapes:

- **Lived records** (episodes, notes): served from the home's artifact
  store, lossless.
- **Identity records** (values/purposes/traits): their verbatim IS the
  attested spark document — the endpoint serves the spark text itself.
- **Born-digest records** (interests, dreams): born as words — their
  digest is their complete text, never a compression. Served as-is with
  `born_digest: true` ("the words you see are all the words there are").
- **Diary projections**: served to the **operator** from the book. The read
  is marker-first: a `diary_read` host
  marker (entry id, kind, visibility) lands in the entity's replay stream
  before the words return, so the disclosure is recorded in the entity's
  biography. Private entries are included. Born-digest diary kinds
  (interest/dream) serve their digest as the verbatim.

### The operator diary door (reads are visible events)

`GET /api/gateway/entities/{name}/diary/{entry_id}?reason=...` serves a
book entry — private included — to the **operator** channel. The book
already lives unencrypted on the operator's machine; this door makes each
read *recorded* rather than silent. `reason` is **optional** (default
"operator review"; the identity, the act and the timestamp are the audit
record), and every disclosure lands a `diary_read` host marker (entry id +
reason) in the entity's replay stream before the words return — the
entity's biography shows who read it and why. Failed lookups disclose
nothing and are not marked. Both full-content doors (this one and the
record-verbatim endpoint above) serve the operator; the boundary that
stays closed is the *effect* layer (a workplace channel cannot read the
book), not the operator's HTTP surface.

## What can never be relaxed

- Never-purge and only-entity-writes are **structural** (absent code paths
  and construction-bound authorship), not policy checks.
- A refused prelude aborts the summon.
- Actor strings are made true at the door; everything downstream trusts them.

---

## docs/deep-research.md

# `deep-research` Shipped Workflow

Gateway packages `deep-research@0.1.7.flow` as a supported shipped bundle. It
is available from the normal bundle registry alongside `basic-agent` when the
packaged bundle directory is used. It replaces the `dp-research` bundle id,
which is no longer shipped.

## Contract

- Bundle id: `deep-research`
- Version: `0.1.7`
- Entrypoint flow id: `deep-research`
- Interfaces: `abstractcode.agent.v1`, `abstractresearch.deep.v1`
- Editable source flows: `abstractflow/examples/flows/deep-*.json`

The workflow exposes a small product-facing input contract:

- `request`: what should be researched.
- `viewpoint`: the angle, thesis, audience stance, or evaluation lens.
- `effort`: `quick`, `standard`, or `thorough`.
- `provider` / `model`: optional overrides. Leave blank to use Gateway/Core
  defaults.

All other knobs are derived from `effort`, including review-round count,
investigation agent iteration cap, deadline/source-budget guidance,
source/citation policy, export title, and export prefix. The derived review
round count is enforced by a root `For` control node: each round runs
investigation, persists the latest evidence, runs adversarial review, persists
reviewer guidance, and feeds that guidance into the next investigation pass.

Derived `deadline_minutes` and `max_sources` are carried into prompts and audit
objects; they do not preempt an in-flight provider call.

## Tool And Export Policy

Research agents pin a read-only evidence allowlist:
`web_search`, `fetch_url`, `skim_websearch`, `skim_url`, `read_file`,
`skim_files`.

Adversarial review runs through structured LLM calls without write or shell
tools. Export uses deterministic Runtime nodes: `write_file`, `write_pdf`, and
`write_docx`.

Export paths append a sanitized run timestamp to the effort-derived output
prefix (`reports/deep-*-research`) to avoid overwrites. The final manifest is
built after file writes and includes actual Markdown/PDF/DOCX paths, byte
counts, PDF/DOCX hashes, and content types.

## Related docs

- [shipped-workflows.md](docs/shipped-workflows.md): every bundle a fresh install serves
- [api.md](docs/api.md): starting runs and streaming their ledger

---

## docs/apple-local-gateway-flow.md

# Apple Silicon local Gateway + Flow quickstart

This guide starts a local-only AbstractGateway and AbstractFlow editor on an Apple
Silicon Mac. It uses PyPI packages, not a source checkout.

## 1. Create an environment

```bash
python3 -m venv ~/.venvs/abstractframework-local
source ~/.venvs/abstractframework-local/bin/activate
python -m pip install -U pip
```

## 2. Install Gateway and Flow

```bash
pip install "abstractgateway[apple]" abstractflow
```

`abstractgateway[apple]` is the native Apple profile. It cascades into the local
AbstractCore, AbstractRuntime, AbstractVision, and AbstractVoice capability
packages needed by Gateway.

## 3. Configure local engines

Use Gateway env vars for Gateway internals, then set framework model defaults through capability
routes owned by the execution host.

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_DATA_DIR="$HOME/.abstractgateway-local"

# Local image generation on MPS through AbstractVision/Diffusers.
export ABSTRACTGATEWAY_VISION_BACKEND="diffusers"
export ABSTRACTGATEWAY_VISION_MODEL_ID="runwayml/stable-diffusion-v1-5"
export ABSTRACTGATEWAY_VISION_DIFFUSERS_DEVICE="mps"

# Alternative: Apple-local MLX-Gen through AbstractVision q4 presets.
# Pre-download with: abstractvision download flux2-klein-4b --provider mlx-gen
# export ABSTRACTGATEWAY_VISION_BACKEND="mlx-gen"
# export ABSTRACTGATEWAY_VISION_MODEL_ID="mlx-gen/flux2-klein-4b"

# Local voice generation and transcription through AbstractVoice.
export ABSTRACTGATEWAY_VOICE_TTS_ENGINE="piper"
export ABSTRACTGATEWAY_VOICE_STT_ENGINE="faster_whisper"
export ABSTRACTGATEWAY_VOICE_STT_MODEL="base"
```

Set the default text route:

```bash
abstractgateway-config set-default input.text \
  --provider lmstudio \
  --model qwen/qwen3.6-35b-a3b \
  --base-url http://127.0.0.1:1234/v1
```

Optional prefetch for voice models:

```bash
abstractvoice-prefetch --piper en
abstractvoice-prefetch --stt base
```

## 4. Start Gateway

```bash
abstractgateway serve --host 127.0.0.1 --port 8080
```

In another terminal with the same venv:

```bash
source ~/.venvs/abstractframework-local/bin/activate
export ABSTRACTGATEWAY_DATA_DIR="$HOME/.abstractgateway-local"
export GATEWAY_ADMIN_TOKEN="$(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")"
```

Check that catalogs are not just defaults:

```bash
curl -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/gateway/voice/voices

curl -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/gateway/audio/speech/models

curl -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/gateway/audio/transcriptions/models

curl -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  "http://127.0.0.1:8080/api/gateway/vision/provider_models?task=text_to_image"

curl -H "Authorization: Bearer $GATEWAY_ADMIN_TOKEN" \
  http://127.0.0.1:8080/api/gateway/vision/models
```

If these return only `gateway_static` defaults, stop the Gateway process and
restart the one from this venv. Check that no other `abstractgateway serve`
process (for example from a global install) still holds port 8080.

## 5. Start Flow

```bash
npx @abstractframework/flow --gateway-url http://127.0.0.1:8080 --port 3005
```

Open `http://127.0.0.1:3005` and sign in as Gateway user `admin` with
`$GATEWAY_ADMIN_TOKEN`.

You can also install and open Flow from the console's **Apps** tab, which
signs you in for you ([apps.md](docs/apps.md)).

In the editor, media nodes use Gateway catalogs:

- Generate Image: image provider/model
- Generate Voice: voice profile/clone plus TTS model
- Transcribe Audio: STT model
- Listen Voice: voice-input wait metadata
