# AbstractGateway

> Durable Run Gateway host for AbstractRuntime and the control plane of AbstractFramework: start runs + append durable commands; clients replay/stream the durable ledger (replay-first); operators manage users, providers, models, local engines, browser apps and network exposure from the web console, the terminal console, the CLI or the desktop tray.

This repository contains the `abstractgateway` Python package (CLI + FastAPI server + durable stores + runner worker + web console + desktop tray helper).
This file is also the documentation corpus the console's docs assistant answers from, so the key facts below are deliberately detailed.
The main HTTP surface is `/api/gateway/*` (FastAPI routes in `src/abstractgateway/routes/gateway.py`).
The live HTTP contract is the runtime OpenAPI schema: `GET /openapi.json` (Swagger UI: `GET /docs`).

Format note: follows the [llms.txt specification](https://llmstxt.org/).

Key invariants / defaults (evidence in code):
- Security is enabled by default for `/api/gateway/*` (user principals via `ABSTRACTGATEWAY_USER_AUTH=1` or `ABSTRACTGATEWAY_AUTH_MODE=users`, a shared bearer token via `ABSTRACTGATEWAY_AUTH_TOKEN`/`ABSTRACTGATEWAY_AUTH_TOKENS`, an origin allowlist, request limits, auth lockouts, an audit log). `/api/health` is intentionally public. With no auth configured, `abstractgateway serve` binds `127.0.0.1` (or the stored network mode) and enables user auth automatically; an explicit non-loopback `--host` without auth configuration fails fast, and any explicit auth setting keeps the `0.0.0.0` default bind. When user auth is enabled it ensures `default/admin` exists and writes `<ABSTRACTGATEWAY_DATA_DIR>/auth/bootstrap-admin-token` (0600; printed on a loopback bind, `serve --print-token`/`--no-print-token` overrides) (`src/abstractgateway/security/gateway_security.py`, startup checks in `src/abstractgateway/cli.py`).
- First run (`src/abstractgateway/first_run.py`, `host_paths.py`, `os_service.py`): unset `ABSTRACTGATEWAY_DATA_DIR` resolves to `./runtime` when it exists in the working directory, else the per-user data folder (macOS `~/Library/Application Support/AbstractGateway`, Linux `$XDG_DATA_HOME/abstractgateway`, Windows `%LOCALAPPDATA%\AbstractGateway`). One-time console sign-in links (`/console#claim=<code>`, 10 minutes, single use, SHA-256 at rest under `<DATA_DIR>/auth/claims/`) come from `serve`'s banner, `abstractgateway claim` or `abstractgateway-config claim-url [--open]`, and are redeemed by `POST /api/gateway/session/claim` from a loopback peer only (no proxy headers). The console then opens a first-run guide (state: `GET/POST /api/gateway/host/first-run`). `abstractgateway service install|uninstall|enable|disable|status` registers a per-user login service (LaunchAgent, systemd user unit or XDG autostart entry, experimental Windows `HKCU\\...\\Run` entry); `enable`/`disable`/`status` share `abstractgateway.autostart` with the desktop tray's "Start AbstractGateway at login" item (states on | off | broken | other). `abstractgateway-config status --json` reports `data_dir_source`, `auth_mode`, `service`, `claim_pending`, `first_run`, `serve`. See [docs/first-run.md](./docs/first-run.md).
- Network exposure (`src/abstractgateway/network_exposure.py`, contract `gateway_network_v1`): one runtime setting chooses `localhost` (127.0.0.1), `lan` (0.0.0.0, requires user accounts) or `internet` (0.0.0.0, requires user accounts and `acknowledge_internet`), changed from the console's Network tab, the terminal console's Connection screen, the tray's Network menu or `abstractgateway network status|show|set|addresses|restart` (`GET/POST /api/gateway/network`, `POST /api/gateway/network/restart`). A mode applies at the next start (`restart_required`); `serve --host/--port` override it (`overridden_by_cli`). Refusals are HTTP 409 with `reason_code` `user_auth_required`, `auth_disabled` (started with security, write or read protection off) or `acknowledgement_required`. The same setting holds the reverse-proxy values `allowed_origins` (added to `http://localhost:*`, `http://127.0.0.1:*` and, in a network mode, the gateway's own LAN origins) and `trust_proxy`, applied to the next request; `ABSTRACTGATEWAY_ALLOWED_ORIGINS` in the launch environment pins the origins (`overridden_by_env`); for `trust_proxy` the saved setting wins and `ABSTRACTGATEWAY_TRUST_PROXY` is only a fallback while nothing is saved (one resolver for lockouts, audit and the same-machine rule). The gateway does not terminate TLS. See [docs/configuration.md](./docs/configuration.md).
- Callers on this computer (`src/abstractgateway/security/same_machine.py`): one rule decides who sits at the gateway computer (engine/app installs under `allow_engine_install`, `open_supported` and `caller_is_this_machine` on the workspace routes). The caller's address is the socket peer; `serve` pins uvicorn's `forwarded_allow_ips` to `127.0.0.1,::1` (an environment `FORWARDED_ALLOW_IPS` is ignored with a warning), so `X-Forwarded-For` is believed only from a peer on this computer. The browser apps' servers relay requests with the browser's address in `X-Forwarded-For` (overwritten) and the marker `X-AbstractFramework-App-Proxy`; a browser on another computer going through an app is not local. Fail-safes: the marker without `X-Forwarded-For` is never local; with `trust_proxy` on (the saved setting first, the launch environment only when nothing is saved) a marked request is never local; a forwarded header from a peer not on this computer is never local. See [docs/security.md](./docs/security.md).
- Runtime settings (`<data dir>/config/runtime_config.json`, `src/abstractgateway/runtime_config.py`) are changed through the console, the terminal console, Continuum, the tray or the CLI (`abstractgateway config get|set|unset`, `abstractgateway apps config get|set`, `POST /api/gateway/admin/runtime-config`): `network`, `apps.node|ports|host|npm_registry|pypi_url`, `allow_engine_install` (default on for a loopback bind and for a caller on the gateway machine itself), `agents.default_workflow.<interface>`, `agents.streaming_default` (default off), `skills.shelf`, `workspace_builtin_deny` (default on), `triage_repo_root` (backlog folder, default `<data dir>/backlog`), `backlog_exec_runner` (default off), `process_manager` (default off), `stop_kill_switch_s`. `serve --backlog-root PATH` and `--exec-runner on|off` override the backlog settings for one run. A write that names an unknown setting is refused whole (400, nothing saved); writers share a file lock.
- Default agent workflow (`src/abstractgateway/agent_defaults.py`): `agents.default_workflow.<interface>` = `[private:|catalog:]bundle[@version]:flow` (source `stored` or `default`; no environment variable). Unset: `abstractcode.agent.v1` -> the shipped `basic-agent` default entrypoint (unavailable when absent); `abstractassistant.agent.v1` -> none (the Assistant uses its built-in orchestrator). `POST /runs/start` and `POST /runs/schedule` accept `flow_id: "@default"` + `interface` (400 without `interface` or with `bundle_id`/`bundle_version`; 409 naming the setting and its source when the default cannot run; never a silent fallback), resolved at every start. Every start answers `resolved_workflow {workflow_id, bundle_id, bundle_version, flow_id, registry_scope, name, source: gateway_default|client, interface}`, also stored as `input_data.workflow_selection`. `GET /bundles` and `GET /workflow-catalog` carry `default_agent_workflows` and `default_agent_workflows_unavailable` (with the reason); entrypoint rows carry `is_agent_default` / `agent_default_interfaces`. The Telegram bridge and the backlog advisor use the same resolver. Doors: console Workflows (*Default agent workflow*, *Make agent default*), console TUI Runtimes -> Runtime knobs (★ in Workflows), `abstractgateway config get|set|unset agents.default_workflow.<interface>`. See [docs/configuration.md](./docs/configuration.md).
- Skills shelf (`src/abstractgateway/skills_shelf.py`): `skills.shelf` (saved) > legacy environment > the gateway's own copy `<data dir>/skills/registry`, seeded at each start from AbstractSkill's curated shelf (never overwriting an edit) > a framework checkout; a saved folder that does not work is reported unavailable (no fall-through). `GET /skills` -> `{skills, shelf, shelf_source: stored|env|seeded|checkout|none, bundled_version, warnings}` (plain sentences); `POST /admin/skills/reseed` (admin) refreshes the copy and answers the seed report. Doors: console Apps -> *Skills shelf* (*Refresh the curated shelf*), console TUI Runtime knobs, `abstractgateway config get|set|unset skills.shelf`.
- Browser apps (`src/abstractgateway/apps_manager.py`, `/api/gateway/apps/*`, `abstractgateway apps list|install|update|launch|stop|open|logs|jobs|runtime|install-tui|tui-command|config`): Flow Editor, Code, Observer, Continuum and Entity install from npm (Node.js 24 installed into the data dir when no Node.js 18+ exists), run as supervised child processes on `127.0.0.1` (usual ports Observer 3001, Continuum 3002, Code 3003, Entity 3004, Flow 3005), start again with the gateway until stopped, and open signed in through one-time handover links (`/apps/handover/{code}`). One Install also installs Code's prebuilt terminal app when one exists for the computer; "Open in Terminal" opens it signed in on the gateway machine. Apps started outside the gateway are detected on their usual ports. The desktop Assistant (`abstractassistant`) is an app card with Install and Open on the gateway's computer; opened from the console or the tray it starts signed in as the person who clicked: the gateway writes a one-time code (2 minutes, single use) into a 0600 file `<data dir>/handover/<random>.json` (`{schema: abstractgateway.desktop_handover.v1, code, base_url, expires_at, user_id}`), starts it with `--gateway-url <url> --gateway-handover-file <file>`, and the Assistant trades the code at `POST /api/gateway/apps/desktop-handover` (no sign-in; direct loopback callers only, 403 when relayed, 410 used or expired) for a remembered session. A running Assistant receives no code (quit it and open it again). Install, update, start and stop need an admin. See [docs/apps.md](./docs/apps.md).
- Desktop tray (`abstractgateway[tray]`, `src/abstractgateway/tray/`, `tray_supervisor.py`): `serve` starts a helper process on a desktop session (absent only for `missing_dependency`, `headless`, `dev_reload`, `runner_only`, `no_tray_flag` for `serve --no-tray`); it talks to the gateway over loopback with an ephemeral token and offers Open Console (signed in), Apps, Workflows, Pause/Resume, Models (eject, load), Activity window, Start at login, Network, Copy Address, Check for Updates, Restart, Quit. Host control routes: `GET /api/gateway/host/runner|tray|update|runs|metrics/live`, `POST /api/gateway/host/pause|resume|restart|shutdown|tray/show|update/check|update/start`. See [docs/tray.md](./docs/tray.md).
- Models & engines (served from AbstractCore through `core_config.py` -> Runtime `config_facade`; the gateway never detects engines or sizes models itself): `GET /api/gateway/host/profile` (`host_profile_v1`), `GET /engines?probe=1` (`engines_status_v1` + `install_allowed`/`install_policy`), `POST /engines/{id}/install {dry_run, force, location}` (admin; real installs need runtime-config `allow_engine_install`; 403 `not_allowed`, 409 `busy`; jobs pause in `needs_admin` / `needs_tools` until `POST /engines/jobs/{id}/continue`; see [docs/engines.md](./docs/engines.md)), `GET /models/catalog?q=&engine=&fits=1&hub=1&tag=` (`model_catalog_v1` with fit verdicts), `GET /models/installed?provider=` (`models_installed_v1`), `POST /models/delete {provider, artifact, dry_run, force}` (admin; 404 not installed, 409 `delete_blockers`), `GET /jobs?kind=&status=` (`host_jobs_v1`), `GET /jobs/{id}` (`host_job_v1`, 404 unknown), `POST /jobs/{id}/cancel` (admin). All jobs (downloads included) run in AbstractCore's host job registry; `cli_equivalent` names `abstractgateway`. Download jobs report `state` (queued, resolving, downloading, verifying, installing, done, failed, cancelled, stalled), bytes, speed, time left, per-file rows and `ended_reason`; `POST /models/download {"recommended": true}` returns a parent job (`grp_...`); `POST /models/download/{id}/cancel` and `GET /models/downloads/stream` (SSE) complete the surface ([docs/model-downloads.md](./docs/model-downloads.md)). An AbstractCore too old for these routes answers 501 `abstractcore_too_old`, a missing one 503. CLI twins: `abstractgateway models list|catalog|search|download|delete|jobs|cancel` and `abstractgateway engines status|install|open` (HTTP to the running gateway by default, `--local` in-process; exit 0/1/2). The web console embeds AbstractCore's screens as the **Models** (`catalog`) and **Engines** (`engines`) tabs and in the first-run guide. See [docs/api.md](./docs/api.md) and [docs/console.md](./docs/console.md).
- Token mode maps the shared Gateway bearer token to `local-admin`. That token is a server/operator token, not a browser sign-in token. With user accounts off, only admin accounts can hold a browser session (`401 user_accounts_off_admin_only`); the last enabled admin account cannot be deleted, disabled or demoted (`409 last_admin`). Hosted user-auth mode resolves Gateway user bearer tokens to principals, exposes `GET /api/gateway/me`, provides admin-only `/api/gateway/admin/users` CRUD plus retained-runtime lifecycle routes under `/api/gateway/admin/runtime-reservations`, stores user token hashes under `<ABSTRACTGATEWAY_DATA_DIR>/auth/users.json` by default, rejects duplicate runtime ids within the same tenant, reserves retained runtime ids when users are deleted, and routes each principal to a separate GatewayService data plane under `<DATA_DIR>/users/<tenant_id>/<runtime_id>/`.
- Gateway serves a built-in control-plane console at `/console` with the tabs Users & Entities, Runtimes, Workflows, Providers, Multimodal, Sandbox, Resources, Models, Engines, Apps and Network, a Technical details switch, a docs assistant, About, and a Setup button that reopens the first-run guide. The Workflows tab holds the default agent workflow per interface and the *Stream replies by default* switch; Resources and Models show the process-wide held memory with its basis, what holds it, and pending or failed ejects. It uses Gateway browser sessions, lets admins manage user records (including optional email metadata), rotate tokens, transfer or purge retained runtime reservations, and lets signed-in users configure provider connections for OpenAI, Anthropic, OpenRouter, Portkey, LM Studio, Ollama, and custom OpenAI-compatible endpoints. Provider URLs/API keys live in the Providers tab; capability Defaults only select configured virtual providers and discovered models, without storing bearer tokens in browser storage. LLM/embedding default pickers use Core `capability_route` filters such as `input.image,output.text` and `embedding.text`; generated-media defaults use capability plugin catalogs. `input.voice` and explicit/covered `input.video` route defaults gate STT and video fallback; Gateway/Core do not silently use installed packages as hidden fallbacks.
- Browser apps should exchange Gateway user bearer tokens through `POST /api/gateway/session/login`, then use `X-AbstractGateway-Session` plus `X-AbstractGateway-CSRF` for mutating session-authenticated requests. The login response body does not expose the session id or CSRF token. Gateway stores revocable browser sessions under `<ABSTRACTGATEWAY_DATA_DIR>/auth/sessions.json` by default and invalidates them when the backing user is disabled, deleted, or token-rotated.
- Gateway browser session cookies use an HTTP-only session cookie, a separate CSRF cookie, `SameSite=Lax`, path `/`, no `Secure` flag on HTTP, and `Secure` under HTTPS. Non-remembered sessions omit `Max-Age`; remembered sessions include it. Origin checks reject untrusted origins and allow only configured hosted app origins.
- Hosted user-auth mode applies execution-host Core defaults, then the Gateway/root baseline Core config, then per-runtime Core config overrides under each user's runtime data plane. These scoped Core config files select provider/model/base URL defaults for the user's runtime without mutating other users. Gateway provider connections store reusable endpoint descriptions, provider families, base URLs, optional model allowlists, and write-only API keys; discovery exposes them as virtual providers such as `endpoint:office-vllm`, and Runtime receives only transient resolved provider/base URL/key parameters.
- A central Gateway route-family policy keeps operator/admin surfaces, model residency and weights mutations (`POST /api/gateway/models/load|unload|lock|unlock|download|delete`, `POST /api/gateway/engines/{id}/install`, `POST /api/gateway/jobs/{id}/cancel`), session-wide prompt-cache clearing (`POST /api/gateway/sessions/{session_id}/prompt_cache/clear_all`), and server-workspace file helpers admin-only. Host and residency reads (`GET /api/gateway/models/loaded`, `GET /api/gateway/models/context_estimate`, `/host/state`, `/host/metrics/*`, `GET /api/gateway/sessions/prompt_cache`) serve any authenticated principal; anonymous requests are rejected. Browser-local files should use upload routes; server workspace read/import/export routes require admin until per-user workspace grants are defined. The exception is a run's own folder: `GET /runs/{run_id}/workspace` (`workspace_root`, `kind` session|run|launch_folder, `session_id`, `exists`, `host {hostname, caller_is_this_machine}`, `open_supported` = admin at the gateway computer), `/workspace/files?path=&recursive=&limit=` (`entries`, `truncated`, `hidden {outside_links, blocked, other}`) and `/workspace/content?path=` (streams with `Content-Disposition: inline`, `nosniff`, `Content-Security-Policy: sandbox`, `Range`) serve the person who started the run (another user's run: 404); paths are relative (`..`/absolute 400, a link leading outside 403), the marker file is never shown, and the workspace policy and the built-in deny list are re-applied at every call.
- Workspace guard (`src/abstractgateway/run_workspace_guard.py`, called from the host's `start_run`): every run (HTTP routes, Telegram/email/agora bridges, entity summons, sandbox routes, scheduled runs) works in a folder (its conversation's gateway-made `<data dir>/workspaces/session-…` when it named none) and gets the built-in deny rules: `workspace_builtin_deny_prefixes` (the gateway's data folder and `~/.ssh`, `~/.aws`, `~/.gnupg`, `~/.config/gcloud`, `~/.kube`, `~/Library/Keychains`, `~/.abstract{gateway,code,assistant,continuum,core}`) plus one `workspace_builtin_allow` (the run's own folder inside the data folder), whole-folder rules enforced by the runtime without being written into the prompt; client-sent values are dropped. An admin can turn the run side off (`workspace_builtin_deny`); the workspace browser always hides them. `POST /runs/start`, `POST /runs/schedule` and entity summons refuse a `workspace_root` inside the data folder (except the caller's own conversation folder) or outside the allowed roots. Shell commands are not confined by the deny rules. See [docs/security.md](./docs/security.md).
- Host state & model residency surface: `GET /api/gateway/host/state` is a one-call snapshot of memory, GPU, resident models, and session prompt caches with byte totals (plus a top-level `host` identity block when the runtime reports one; `memory.device.process_held_bytes` with `process_held_basis` = what this process holds across MLX, llama.cpp, transformers and embeddings and how it was measured; `memory.resident`/`memory.held` = which models hold it; `residency_diagnostics` = `{pending_ejects, last_switch_ejects}`, `{}` when none); sections degrade independently in-band (`degraded` + `reasons`), never a 500. `GET /api/gateway/host/metrics/memory` and `/host/metrics/gpu` are the per-probe snapshots (`supported: false` with a reason when unavailable). `GET /api/gateway/models/loaded` returns raw `models` plus normalized `rows` in the additive-tolerant `model_residency_row_v1` schema (incl. `locked`/`lockable`, `modalities`, `calibrated_context_length`/`context_calibrated`, `host_id`/`host_name`; provider residency booleans outrank runtime lease booleans, state strings can confirm but never deny residency, unknown stays null). `totals.models_resident` counts only rows with `resident: true` — the truthful "N loaded" (`totals.models` counts every known row, configured/cached included). Admin-only `POST /api/gateway/models/lock|unlock` pin/release a resident model; lock requires provider-verified residency (a configured or merely-warm model refuses with `error: "model_not_resident"` — load with `lock: true` instead), unlock always works so locks are never stranded; `POST /models/unload` answers HTTP 409 with the `model_locked` refusal payload for a locked model unless the request carries `"force": true`. `GET /api/gateway/models/context_estimate?provider=&model=&context_length=` (user-level; `context_length` >= 1) relays the runtime's context/KV memory estimate with in-band `confidence` (`calibrated`|`estimated`|`unknown`), the tri-state `fits_weights`/`fits_requested_context` split, `predicted_max_context` (the context that fits beside the weights), and `budget_bytes` (basis + reserve stated in `notes`); advisory only — no load path gates on it. Discovery advertises `contracts.common.host_state`, `contracts.common.session_caches`, and `model_residency` with `row_schema`, the lock/unlock/context_estimate endpoints, and the canonical `modality_ui` color map (v1) all residency clients render with. Both consoles surface this as a Resources view (web tab; console-TUI screen 8) — reads for every authenticated user, mutations admin-gated; the web table defaults to provider-resident rows only, with configured/cached rows ("configured — not in memory") behind a counted toggle, and the TUI totals line counts resident rows apart from the row total. See [docs/api.md](./docs/api.md).
- Gateway discovery is permission-aware for those high-trust surfaces: ordinary users see admin-only workspace artifact import/export and provider prompt-cache controls marked unavailable with `admin_required` metadata. Session prompt-cache key hashes include a private principal scope so users do not collide on the same session/provider/model tuple.
- Shared/default workflows use the Gateway workflow catalog (`/api/gateway/workflow-catalog` plus admin-only `/api/gateway/admin/workflow-catalog/*`), not another user's private `/api/gateway/bundles` surface. Catalog versions are immutable, admins move default pointers and ACLs, and users start catalog workflows with explicit `registry_scope: "tenant_catalog"` so execution happens in their own runtime. Gateway signs catalog workflow policy before passing it to Runtime, and private bundle routes reject catalog-internal ids.
- Bundle mode is the only workflow source (`ABSTRACTGATEWAY_WORKFLOW_SOURCE=bundle`) and loads `.flow` bundles from `ABSTRACTGATEWAY_FLOWS_DIR` (`src/abstractgateway/service.py`, `src/abstractgateway/hosts/bundle_host.py`). VisualFlows are stored through `/api/gateway/visualflows/*` and published as `.flow` bundles.
- Stop: a `cancel` command stops the executing model call; after `stop_kill_switch_s` (runtime config / `ABSTRACTGATEWAY_STOP_KILL_SWITCH_S`, default 10, 0 disables) a still-running inference of the cancelled tree is killed in process (never the gateway process). Cancelled ledger steps have status `cancelled` (`cancelled_by` / `killed_by`). Runs accept `speculation` (MTP: `false` or a native-MTP object) on `/runs/start`, `/runs/schedule` and `/sandbox/generate`. Runs of one session reuse one gateway-owned workspace folder. See [docs/configuration.md](./docs/configuration.md).
- `GET /api/gateway/about` (public, no sign-in): `{abstractframework: <version or null>, abstractgateway, packages: {name: version}}`, versions only. The console top bar's About, the terminal console's About (`F1`, `?`, `abstractgateway-console --about`) and the tray's About show them with the AbstractFramework project details.
- Dependencies: AbstractRuntime 0.5.0 or later (live token deltas and the built-in deny rules; the gateway refuses to build its workflow host on an older runtime and names the version to install) and AbstractSkill 0.3.0 or later (the curated shelf), both installed automatically.
- Live replies (`src/abstractgateway/live_deltas.py`): a run started with `input_data._runtime.stream: true` (400 unless a boolean; `agents.streaming_default` applies to interactive `/runs/start` only) also sends `event: llm.delta` / `event: llm.delta_end` frames (no `id:` line; `call_id` = the llm_call step id; `channel` content|reasoning; `snapshot: true` frames on (re)connect; `reason` completed|failed|cancelled|unavailable + `detail`; `synthetic: true` when the gateway closed a call because the run ended) on `GET /runs/{id}/ledger/stream`, including the deltas of every run below the streamed one (`run_id`, `root_run_id`). No size cap. Split deployments pass them through `<data dir>/live/<root>.deltas.jsonl` (0600, deleted at the run's end). `GET /discovery/capabilities` -> `capabilities.streaming {deltas: true, default}`. See [docs/api.md](./docs/api.md).
- Both consoles carry a Workflows surface (web tab; console-TUI step 6) listing every registered workflow with its published/draft version counts, its versions and entrypoints, and a separate `Not loaded` block naming versions the gateway is not serving and why. It supports import (`POST /api/gateway/bundles/upload`), export (`GET /api/gateway/bundles/{bundle_id}/download` — original bytes plus an `X-AbstractGateway-Bundle-Sha256` header), and delete of a single version or a whole workflow.
- Writing a workflow registry requires owning it: the gateway's own bundle directory is shared by every user, so `POST /api/gateway/bundles/upload`, `DELETE /api/gateway/bundles/{bundle_id}`, `POST /api/gateway/bundles/reload`, the deprecate routes and `POST /api/gateway/visualflows/{flow_id}/publish` require an admin principal there and return `403` otherwise; under hosted user auth each principal writes its own registry freely. Reads are user-level. `DELETE` returns `409` for the boot-verified `basic-agent.flow`.
- A bundle version that misses its `metadata.min_runtime` floor or fails to compile is excluded from the runnable set and reported: `GET /api/gateway/bundles` returns a `skipped` array with the reason and on-disk path, and `POST /api/gateway/bundles/reload` reports the same. Install responses carry `loaded` (`true` served, `false` proven not served, `null` unverified because no reload was requested).
- When `ABSTRACTGATEWAY_FLOWS_DIR` is unset, Gateway serves the bundles packaged with the wheel: `basic-agent` (default chat agent), `coding-agent` (verify-gated coder; `coder` entrypoint), `deep-research`, `co-scientist`, `docs-qa`, `abstractassistant-orchestrator`, and the `react-agent`/`codeact-agent`/`memact-agent` native loops. The packaged set is the force-include list in `pyproject.toml`. See [docs/shipped-workflows.md](docs/shipped-workflows.md).
- SQLite safety invariant: when `ABSTRACTGATEWAY_STORE_BACKEND=sqlite`, the DB file (`ABSTRACTGATEWAY_DB_PATH`, default `<DATA_DIR>/gateway.sqlite3`) must be under `ABSTRACTGATEWAY_DATA_DIR` (fail-fast; prevents cross-wiring durable state) (`src/abstractgateway/config.py`).
- The system is replay-first; SSE streams are an optimization, not a source of truth (`src/abstractgateway/routes/gateway.py`).
- Two entry points, one store: AbstractCore owns capability route defaults (provider/model/base URL, the `output.text` reasoning effort, and route options), provider API keys, the `email` mail connection, and the `maintenance` triage LLM settings; Gateway surfaces them and keeps no copy, with `ABSTRACT_EMAIL_*` / `ABSTRACT_TRIAGE_LLM_*` as override rungs above the store. Every Gateway read/write of AbstractCore-owned config goes through one seam, `src/abstractgateway/core_config.py`; auth, bundles, workspaces, run policy, and integrations are Gateway-owned. `endpoint:<id>` profiles are a shared namespace where an AbstractCore `provider_profiles` entry wins over a Gateway one of the same id. See [docs/configuration.md](docs/configuration.md).
- Gateway LLM helper defaults resolve explicit request values, optional flow pins, and execution-host capability route defaults such as `output.text`. Missing provider/model is a clear config error, not a hardcoded local fallback (`src/abstractgateway/provider_defaults.py`, `src/abstractgateway/core_config.py`).
- Optional operator endpoints include account-scoped email inbox routes (`/api/gateway/email/*`) used by AbstractObserver (Inbox → Email) (`src/abstractgateway/routes/gateway.py`).

Multimodal notes:
- Capability discovery for thin clients: `GET /api/gateway/discovery/capabilities` (best-effort; reports installed optional capabilities like `voice`, `tools`, `visualflow`, `vision_fallback`, `media`, plus the versioned shared run contract including input-data/history-bundle read endpoints, direct media feature gates, voice `tts|stt|listen`, prompt-cache surfaces, and model-residency truth).
- Vision adapter discovery for thin clients: `GET /api/gateway/vision/adapters` returns compatible installed adapters for a provider/model/task combination through Runtime's public discovery facade.
- The shared artifact contract includes run artifact listing/content, session-visible artifact listing, cross-run/session/run artifact search, workspace-path import, and artifact-to-workspace export. `/api/gateway/artifacts/search` returns the row fields plus `artifact_envelope_v1`, exact stats/facets when `include_stats=true`, bounded paging, canonical descriptor filters, and a UI-oriented `artifact_kind` filter that keeps Voice, Music, Sound, and unclassified Audio distinct; single canonical kinds use Runtime catalog filters, while multi-kind unions may be Gateway post-filters until Runtime has OR filters. Browser-local files use upload; Gateway workspace paths use import.
- Generated-media artifacts projected from child runs into parent runs preserve Runtime descriptors and structured metadata. Direct STT transcript artifacts include descriptor/source refs for the source audio artifact, language/prompt hints, provider/model when available, and redacted route parameters.
- Direct Gateway voice/audio durable endpoints (when `abstractvoice` is installed on the gateway host):
  - STT: `POST /api/gateway/runs/{run_id}/audio/transcribe`
  - TTS: `POST /api/gateway/runs/{run_id}/voice/tts`
  - TTS stream: `POST /api/gateway/runs/{run_id}/voice/tts/stream` (JSON Lines; advertised through `capabilities.contracts.assistant.voice.tts.streaming`; successful streams still return a Runtime child-run artifact)
- Direct Gateway image durable endpoints:
  - image generate: `POST /api/gateway/runs/{run_id}/images/generate`
  - image edit: `POST /api/gateway/runs/{run_id}/images/edit`
  - image upscale: `POST /api/gateway/runs/{run_id}/images/upscale`
- Direct Gateway video durable endpoints:
  - text-to-video: `POST /api/gateway/runs/{run_id}/videos/generate`
  - image-to-video: `POST /api/gateway/runs/{run_id}/videos/from_image`
- Direct Gateway music durable endpoint:
  - music: `POST /api/gateway/runs/{run_id}/music/generate`
- Generated, edited, and upscaled images plus text-to-video/image-to-video are available through Runtime workflows and the direct run-scoped routes when the corresponding Runtime/Core vision backend is configured. Image, image-edit, image-upscale, text-to-video, and image-to-video direct routes expose child-run progress on the returned child run ledger as `abstract.progress`; image progress is best-effort and may be limited to start/complete for backends that do not report step progress. Image dimensions and upscaler controls are optional passthrough overrides and must not be injected as universal client defaults. Direct image/video routes also preserve task-specific batch and adapter fields: `count` / `n`, `seeds`, ordered `lora_adapters`, and video `flow_shift`, and batch responses return `image_artifacts` / `video_artifacts` plus the compatibility singular artifact field.
- Dynamic catalogs for thin clients are exposed at `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`, and `GET /api/gateway/vision/provider_models`; they proxy AbstractCore Server catalog routes when configured.
- Catalog routes carry a Gateway-owned stable envelope: `catalog.contract=gateway_catalog_v1`, `catalog.version=1`, and canonical `items`, next to the lower-layer fields kept for compatibility.
- The shared client contract also exposes `common.readiness` as `gateway_surface_readiness_v1`: a compact surface-level summary derived from Gateway descriptors, media gates, memory, prompt-cache, and Runtime-backed model-residency truth.
- Gateway does not invent selected-backend or degraded-state truth; it reports what Runtime/Core expose.
- `voice.listen` is a host-capture contract for higher apps, not a server microphone socket; clients capture locally and then emit an event or upload audio for STT.
- KG memory is selected through Gateway env (`ABSTRACTGATEWAY_MEMORY_STORE_BACKEND=lancedb|memory`, plus `sqlite` when the installed AbstractMemory build exposes `SQLiteTripleStore`) and queried through `POST /api/gateway/kg/query`; store implementations remain owned by AbstractMemory, Gateway targets AbstractMemory's TripleStore API, and fresh empty stores are reported as available when the backend resolves.
- Text embeddings use the execution-host `embedding.text` capability route. Remote/provider-backed embeddings work in the base light install; local HuggingFace/sentence-transformer embeddings require `abstractgateway[embeddings]`. Gateway does not persist a separate embedding provider/model file; split deployments proxy embedding generation to the remote AbstractCore server.
- `/api/gateway/prompt_cache/*` exposes admin-only provider/model prompt-cache controls; `/api/gateway/prompt_cache/saved|save|load` are Runtime-backed host-local export/import admin aliases; `/api/gateway/sessions/{session_id}/prompt_cache/*` adds principal-scoped Gateway-owned session naming/status/prepare/clear/rebuild orchestration over those provider controls. `GET /api/gateway/sessions/prompt_cache` (user-level) enumerates the 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 them per session in one call — the recommended lane for observing and reclaiming session cache state.
- Docker scope ([docs/deployment.md](./docs/deployment.md)): `ghcr.io/lpalbou/abstractgateway:latest` is the release-grade lightweight base-package image; `ghcr.io/lpalbou/abstractgateway:gpu-latest` / `:<version>-gpu` installs `abstractgateway[gpu]` and is experimental/best-effort until a CUDA smoke gate exists. The container defaults to `ABSTRACTGATEWAY_USER_AUTH=1`; its entrypoint creates `default/admin` when missing and writes the first-login token under `<ABSTRACTGATEWAY_DATA_DIR>/auth/bootstrap-admin-token`. Apple/MLX inference should run natively on macOS and be reached from the Gateway container via Docker Model Runner (`http://model-runner.docker.internal/engines/v1`) or a host bridge OpenAI-compatible URL.

Regeneration:
- `python scripts/generate-llms-full.py` regenerates `llms-full.txt` from the Markdown doc links in this `llms.txt`.

## Ecosystem

- [AbstractFramework](https://github.com/lpalbou/AbstractFramework): ecosystem meta-repo (this gateway is one component)
- [AbstractRuntime](https://github.com/lpalbou/abstractruntime): durable runs + tick loop + stores (required dependency)
- [AbstractCore](https://github.com/lpalbou/abstractcore): lower-level provider/tool/media integrations consumed through Runtime facades
- [AbstractFlow](https://github.com/lpalbou/abstractflow): authoring + bundling `.flow` WorkflowBundles (optional)

## Start here

- [README.md](./README.md): install + quickstart + doc map
- [docs/README.md](./docs/README.md): docs index (what to read for each topic)
- [docs/first-run.md](./docs/first-run.md): zero-configuration local start, one-time console sign-in link, first-run guide, start at login
- [docs/getting-started.md](./docs/getting-started.md): step-by-step setup (bundle mode + file/SQLite + split API/runner)
- [docs/troubleshooting.md](./docs/troubleshooting.md): symptoms, causes and fixes (sign-in, network modes, runs, installs, downloads, tray, login service)
- [llms-full.txt](./llms-full.txt): single-file snapshot of the Markdown docs linked in this file

## Project docs

- [CHANGELOG.md](./CHANGELOG.md): release notes (human-oriented)
- [CONTRIBUTING.md](./CONTRIBUTING.md): dev setup + tests + contribution workflow
- [SECURITY.md](./SECURITY.md): responsible vulnerability reporting process
- [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md): contributor behavior and reporting
- [ACKNOWLEDGMENTS.md](./ACKNOWLEDGMENTS.md): dependency acknowledgments (see `pyproject.toml` for canonical list)

## Core docs

- [docs/getting-started.md](./docs/getting-started.md): run the gateway + choose file vs SQLite stores + split API/runner
- [docs/api.md](./docs/api.md): client contract + curl examples (replay-first ledger + durable commands)
- [docs/security.md](./docs/security.md): user accounts + sessions + origin allowlist + network exposure + limits + audit log
- [docs/configuration.md](./docs/configuration.md): runtime settings (network, reverse proxy, apps, backlog, engine installs), capability defaults, env vars, install extras, CLI flags
- [docs/console.md](./docs/console.md): the web console at `/console`, the `abstractgateway-console` terminal app (`cargo install abstractgateway-console`), and `abstractgateway models loaded|load|unload`
- [docs/apps.md](./docs/apps.md): browser and terminal apps (Flow, Code, Observer, Continuum, Entity) and the desktop Assistant, installed with one Install, started and opened signed in from the gateway; Node.js installed for you
- [docs/engines.md](./docs/engines.md): installing local engines (Ollama, LM Studio, MLX, llama.cpp) without a terminal; when the Apple tools or an administrator password are needed
- [docs/model-downloads.md](./docs/model-downloads.md): model downloads with progress, stalls, cancel, parent jobs and the event stream
- [docs/tray.md](./docs/tray.md): the desktop tray icon (apps, models, pause, start at login, network, restart, update)
- [docs/deployment.md](./docs/deployment.md): Docker/GHCR/Compose deployment profile
- [docs/architecture.md](./docs/architecture.md): components + durability model + deployment shapes (diagrams)
- [docs/shipped-workflows.md](./docs/shipped-workflows.md): the workflow bundles a fresh install serves out of the box (coder, deep research, co-scientist, basic-agent, native loops) + how to run one
- [docs/faq.md](./docs/faq.md): common questions and limits

## Key code entrypoints

- [pyproject.toml](./pyproject.toml): packaging metadata + dependency extras + CLI entrypoint
- [src/abstractgateway/app.py](./src/abstractgateway/app.py): FastAPI app + middleware + routers + health endpoint
- [src/abstractgateway/routes/gateway.py](./src/abstractgateway/routes/gateway.py): `/api/gateway/*` endpoints (runs, ledger, bundles, commands, tooling)
- [src/abstractgateway/security/gateway_security.py](./src/abstractgateway/security/gateway_security.py): auth/origin/limits/audit log middleware
- [src/abstractgateway/service.py](./src/abstractgateway/service.py): composition root (config + stores + host + runner + optional bridges)
- [src/abstractgateway/runner.py](./src/abstractgateway/runner.py): durable command inbox polling + run ticking
- [src/abstractgateway/stores.py](./src/abstractgateway/stores.py): file vs SQLite store wiring
- [src/abstractgateway/config.py](./src/abstractgateway/config.py): env var parsing + sqlite safety invariant
- [src/abstractgateway/cli.py](./src/abstractgateway/cli.py): `abstractgateway serve|runner|migrate|triage-*|...`
- [src/abstractgateway/hosts/bundle_host.py](./src/abstractgateway/hosts/bundle_host.py): `.flow` bundle loading + VisualFlow compilation + tool/LLM wiring switches

## Optional

- [docs/maintenance.md](./docs/maintenance.md): operator tooling (triage/backlog/process manager/file helpers; high trust required)
- [docs/entities.md](./docs/entities.md): summoned entities (homes, lifecycle, summoning, replay)
- [docs/deep-research.md](./docs/deep-research.md): the shipped `deep-research` workflow contract
- [docs/apple-local-gateway-flow.md](./docs/apple-local-gateway-flow.md): Apple Silicon local Gateway + Flow setup
- [scripts/generate-llms-full.py](./scripts/generate-llms-full.py): generator for `llms-full.txt`
- [tests/](./tests/): test suite (`pytest`, most tests are marked `basic`)
