# AbstractFramework - llms-full
> Full text of key files from this repo. Sections are separated by `--- <path> ---`.

--- README.md ---
# AbstractFramework

**Write once. Generate everything.**

A modular, open-source ecosystem for building **durable, observable, multimodal** AI systems. Text, voice, image, video, music — one unified interface, any provider, any model, local or cloud.

AbstractFramework is an ecosystem of composable packages for building AI systems that work in operational reality:

- **Durable by default**: workflows **pause and resume** safely (survive crashes and restarts)
- **Observable**: an append-only **ledger** so any UI can reconstruct state by replaying history
- **Controlled actions**: explicit boundaries for **tool execution**, approvals, and evidence
- **Multimodal**: capability plugins (voice, vision, music) that stay out of your way until you need them

Think of it as an **agentic OS**: durable runs + replay-first observability + multimodal capabilities — write once, run across providers and deployment modes.

> **Prerequisites**: none for the one-line install below (it provisions Python and, optionally, Node.js). For a manual install: Python 3.10–3.13, Node.js 18+ for browser UIs, and an LLM backend (Ollama, LM Studio, vLLM, or a cloud API key).

---

## Quick start

The installer sets up the gateway in your user account (no admin password, no system Python),
asks whether to start it at login, starts it on `127.0.0.1:8080`, and opens its web console in
your browser already signed in. A first-run guide then sets up a local engine (Ollama, LM Studio,
MLX, llama.cpp), downloads a model that fits your machine, and lists the apps.

- **Mac, no Terminal:** download and double-click
  [AbstractFramework-Installer.pkg](https://github.com/lpalbou/AbstractFramework/releases/latest/download/AbstractFramework-Installer.pkg).
  The package is not signed with an Apple Developer ID, so the first time macOS blocks it: open
  **System Settings > Privacy & Security**, click **Open Anyway** next to the installer's name and
  confirm. A Terminal window then shows each step; press Return at its one question.
- **macOS / Linux, one line:**

  ```bash
  curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh -s -- --interactive
  ```

- **Windows 10 22H2+ / 11** (PowerShell):

  ```powershell
  powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"
  ```

Every failure says what to do next; running the installer again repairs or upgrades in place. To
remove it, double-click `Uninstall AbstractFramework.command` (in
`~/Library/Application Support/AbstractFramework/Installer` after a package install), or run
`curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/uninstall.sh | sh`.
Step by step, the failure table, options (`--with-apps`, `--with-ollama`, `--print`, …):
[Install](docs/install.md). Something not working: [Troubleshooting](docs/troubleshooting.md).

Already have Python? Either entry point works the same way: start it, then open the link it prints.

```bash
pip install abstractcore && abstractcore serve        # http://127.0.0.1:8000/console#claim=…
pip install abstractgateway && abstractgateway serve  # http://127.0.0.1:8080/console#claim=…
```

Both consoles have **Models** (browse models that fit this machine, download, delete) and
**Engines** (detect and install local engines) tabs; every action also shows its command-line
equivalent (`abstractcore models …`, `abstractcore engines …`, `abstractgateway models …`).

---

## Two entrypoints

Start lightweight with just the LLM library, or go all-in with a production gateway. Both paths lead to the same ecosystem.

### 1) AbstractCore — LLM SDK + OpenAI-compatible `/v1` server

Start here if you need a lightweight LLM library for scripts, notebooks, or existing applications. No infrastructure required — just `pip install` and call. Add multimodal capabilities with plugins as you grow.

- 9+ providers with identical API (local + cloud)
- Universal tool calling, structured output, streaming
- Media handling (images, PDFs, audio, video)
- OpenAI-compatible HTTP server mode (`/v1`)
- Multimodal via capability plugins (Voice, Vision, Music)

```bash
pip install abstractcore
```

```python
from abstractcore import create_llm

llm = create_llm("ollama", model="qwen3:4b-instruct")
resp = llm.generate("Explain durable execution in 3 bullets.")
print(resp.content)
```

`abstractcore serve` starts the `/v1` server on `127.0.0.1:8000` and prints a one-time link to its
web console (Overview, Models, Engines, Providers). The same Models and Engines screens are
available from the command line (`abstractcore models catalog|list|download|delete`,
`abstractcore engines status|install`) and in the terminal console
(`cargo install abstractcore-console`).

AbstractCore gives you one interface for provider switching, tools, structured output, and media — as a Python SDK or via `/v1` for any OpenAI-compatible client.

### 2) AbstractGateway — durable run control plane (HTTP/SSE APIs)

Start here if you're building persistent AI applications — agents that run for hours, workflows that survive crashes, scheduled tasks. The gateway is your AI control plane: durable runs with ledger replay/streaming and thin clients that can attach/detach across devices.

- Durable execution that survives crashes and restarts
- Append-only ledger (replay-first) for auditability
- Scheduled workflows (cron-style, recurring)
- Multi-client: terminal, browser, tray, Telegram, email
- Start on one device, continue on another

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

With no auth configured, `abstractgateway serve` binds `127.0.0.1:8080`, enables user auth,
creates `default/admin` in the per-user data folder, and prints a one-time sign-in link
(`http://127.0.0.1:8080/console#claim=…`, valid 10 minutes, this machine only). Open it to reach the
web console and its first-run guide. `abstractgateway claim` mints a new link;
`abstractgateway service install` starts the gateway at login.

To choose the data folder, the allowed browser origins or your own workflow bundles, set the
environment explicitly:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
export ABSTRACTGATEWAY_WORKFLOW_SOURCE=bundle
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"
# export ABSTRACTGATEWAY_FLOWS_DIR="$PWD/bundles"   # serve your own bundle registry

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

Out of the box this serves a ready set of workflows — a verify-gated coding
agent, `deep-research`, and `co-scientist` among them. See
[shipped workflows](https://github.com/lpalbou/AbstractGateway/blob/main/docs/shipped-workflows.md).

The `admin` user token is kept in `<data dir>/auth/bootstrap-admin-token`; use it to sign in to
AbstractFlow, AbstractCode Web or AbstractObserver, or to the console without a claim link.
`ABSTRACTGATEWAY_AUTH_TOKEN` remains a legacy server/operator bearer token; it is not a browser
sign-in token.

Monitor runs from a browser, or from a terminal with the gateway console:

```bash
npx @abstractframework/observer   # open http://localhost:3001

cargo install abstractgateway-console   # Rust 1.87+
ABSTRACTGATEWAY_AUTH_TOKEN=<token> abstractgateway-console --url http://127.0.0.1:8080
```

Container images are published for the gateway and the AbstractCore server:
`ghcr.io/lpalbou/abstractgateway:0.4.3` and `ghcr.io/lpalbou/abstractcore-server:2.15.2`.

For artifact and runtime-resource investigation, see
[Runtime artifacts and retrieval](docs/guide/runtime-artifacts.md).

---

## Author once, run everywhere (AbstractFlow)

AbstractFlow lets you author complex agentic orchestration as portable `.flow` bundles:

1. Open the Flow Editor (`npx @abstractframework/flow`)
2. Build a workflow: LLM steps, tool steps, branching, loops, subflows
3. Export a `.flow` bundle into your own bundle directory and point `ABSTRACTGATEWAY_FLOWS_DIR` at it (or publish it through the Gateway API)
4. Run it from any gateway-backed client (Observer, AbstractAssistant, Code Web UI, your app)

**AbstractAgent** provides ready-made agent patterns (ReAct, CodeAct, MemAct) that can be used inside flows or standalone. The workflows Gateway ships with are authored the same way — their editable sources are documented in [shipped workflow sources](https://github.com/lpalbou/AbstractFlow/blob/main/docs/shipped-workflow-sources.md).

---

## Monitor and schedule with AbstractObserver

- **Observe**: replay the full ledger of any run, or watch one live over SSE
- **Control**: cancel, resume, or inspect runs from the browser
- **Schedule**: durable schedules (cron-style) owned by the gateway — they survive restarts

---

## Package map

The ecosystem, grouped by layer. Each name links to the package's repository.

### Foundation

| Package | What it is |
|---|---|
| [abstractcore](https://github.com/lpalbou/AbstractCore) | Unified LLM interface: 9+ providers, tools, structured output, media, embeddings, `/v1` server, capability plugins |
| [abstractsemantics](https://github.com/lpalbou/AbstractSemantics) | Central semantics registry (predicates + entity types) with JSON-Schema helpers |
| [abstractmemory](https://github.com/lpalbou/AbstractMemory) | Durable, append-only agent memory: usage-weighted graph + journal — recall, formation, consolidation (the entity mind engine) |

### Durable execution

| Package | What it is |
|---|---|
| [abstractruntime](https://github.com/lpalbou/AbstractRuntime) | Durable execution kernel: runs, effects, waits, append-only ledger, artifacts; the VisualFlow compiler (visual graphs → executable workflows); the entity identity lane (homes, chat/life/visit drivers) |
| [abstractagent](https://github.com/lpalbou/AbstractAgent) | Agent patterns (ReAct / CodeAct / MemAct) composing Runtime + Core |
| [abstractflow](https://github.com/lpalbou/AbstractFlow) | Visual workflow editor + portable `.flow` bundles — author once, run anywhere |

### Control plane

| Package | What it is |
|---|---|
| [abstractgateway](https://github.com/lpalbou/AbstractGateway) | Deployable control plane: durable runs over HTTP/SSE, scheduling + run commands (cancel/steer), workflow catalog, artifact/ledger serving, multi-user auth with per-user runtimes, the summoned-entity lifecycle (create / summon / visit / state / blueprint), and the operator consoles (web + TUI) |

### Multimodal capabilities

| Package | What it is |
|---|---|
| [abstractvoice](https://github.com/lpalbou/AbstractVoice) | Voice I/O (TTS / STT), local and remote backends |
| [abstractvision](https://github.com/lpalbou/AbstractVision) | Model-agnostic generative vision (images, optional video) |
| [abstractmusic](https://github.com/lpalbou/AbstractMusic) | Text-to-music / text-to-audio (Core capability plugin) |
| [abstract3d](https://github.com/lpalbou/abstract3d) | Local-first 3D generation |
| [abstractcamera](https://github.com/lpalbou/AbstractCamera) | Camera control and capture tools |
| `abstractsound`, `abstractvideo`, `abstractspatial`, `abstractgeometry`, `abstractcognition` | Reserved capability packages (namespaces held; APIs landing incrementally) |

### Apps and clients

| App | What it does | Install |
|---|---|---|
| [AbstractCode](https://github.com/lpalbou/AbstractCode) | Terminal agentic dev client (Rust, on the AbstractTUI engine) — durable sessions, tool approvals, `/workflow` support | `cargo install abstractcode`, or a prebuilt binary from the [GitHub release](https://github.com/lpalbou/AbstractCode/releases) |
| [AbstractAssistant](https://github.com/lpalbou/AbstractAssistant) | macOS tray client — gateway-native, workflow picker per session, voice support | `pip install abstractassistant` |
| [AbstractObserver](https://github.com/lpalbou/AbstractObserver) | Browser UI — monitor, control, and schedule gateway runs | `npx @abstractframework/observer` |
| [AbstractEntity](https://github.com/lpalbou/AbstractEntity) | Summoned-entity manager — roster, blueprint (cognition map + editing), chat drawer, live replay | `npx @abstractframework/entity` |
| [AbstractContinuum](https://github.com/lpalbou/AbstractContinuum) | Continuous iterative development and deployment console | `npx @abstractframework/continuum` |
| **Gateway consoles** | Operator consoles for a running gateway: web at `/console` (first-run guide, Models, Engines, providers, users), terminal via `abstractgateway-console` | built into `abstractgateway`; `cargo install abstractgateway-console` |
| **Core consoles** | Consoles for AbstractCore: web at `/console` of `abstractcore serve`, terminal via `abstractcore-console` (config, Models, Engines) | built into `abstractcore`; `cargo install abstractcore-console` |
| **Code Web UI** | Browser client of AbstractCode (gateway-backed) | `npx @abstractframework/code` |
| **Flow Editor** | Visual workflow authoring in the browser | `npx @abstractframework/flow` |

### Shared libraries

| Package | What it is |
|---|---|
| [abstracttui](https://github.com/lpalbou/AbstractTUI) | Rust terminal-UI engine built on fine-grained reactive signals |
| [abstractuic](https://github.com/lpalbou/AbstractUIC) | Reusable UI kit for framework clients (React components + Web Components) |
| [abstractskill](https://github.com/lpalbou/AbstractSkill) | Shared library for Agent Skills (`SKILL.md` folders: load, trust-gate, activate) |

---

## Install the pinned ecosystem profile

### Light / Apple / GPU profiles

Choose how the framework runs based on your hardware and constraints. All profiles keep the same interfaces; they mainly change which **local inference stacks** are available.

**Light (default)** — endpoint-only inference (cloud APIs or local OpenAI-compatible servers), no in-process ML engine stacks:

```bash
pip install abstractframework
```

**Apple** — native Apple Silicon local stacks (MLX/Metal) in addition to endpoint providers:

```bash
pip install "abstractframework[apple]"
```

**GPU** — native GPU local stacks (CUDA/ROCm) in addition to endpoint providers:

```bash
pip install "abstractframework[gpu]"
```

| Profile | Command | Platforms | Python |
|---|---|---|---|
| Light | `pip install abstractframework` | macOS, Linux, Windows | 3.10–3.13 |
| Apple | `pip install "abstractframework[apple]"` | macOS 14+ on Apple Silicon | 3.10–3.13 (F5-TTS voice cloning needs 3.11+) |
| GPU | `pip install "abstractframework[gpu]"` | Linux / Windows with a CUDA or ROCm GPU | 3.10–3.13 (F5-TTS voice cloning needs 3.11+) |

### Release matrix (abstractframework 0.3.2)

`abstractframework` pins every Python package with `==`, so one version of the
meta-package always installs the same stack. The browser apps and Rust tools are
distributed through npm and crates.io; the versions below are the ones released
and tested together.

| Registry | Package | Version |
|---|---|---|
| PyPI | `abstractgateway` | 0.4.3 |
| PyPI | `abstractassistant` | 0.5.0 |
| PyPI | `abstractcore` | 2.15.2 |
| PyPI | `AbstractRuntime` | 0.4.35 |
| PyPI | `abstractagent` | 0.3.13 |
| PyPI | `AbstractMemory` | 0.3.0 |
| PyPI | `abstractsemantics` | 0.0.5 |
| PyPI | `abstractvoice` | 0.11.4 |
| PyPI | `abstractvision` | 0.3.29 |
| PyPI | `abstractmusic` | 0.1.15 |
| npm | `@abstractframework/flow` | 0.3.20 |
| npm | `@abstractframework/code` | 0.4.2 |
| npm | `@abstractframework/observer` | 0.1.12 |
| npm | `@abstractframework/continuum` | 0.3.1 |
| npm | `@abstractframework/entity` | 0.2.1 |
| crates.io | `abstractcode` | 0.5.1 |
| crates.io | `abstractgateway-console` | 0.8.0 |
| crates.io | `abstractcore-console` | 0.2.0 |
| crates.io | `abstracttui` | 0.6.0 |
| GHCR | `ghcr.io/lpalbou/abstractgateway` | 0.4.3 (`gpu-latest` / `<version>-gpu` experimental) |
| GHCR | `ghcr.io/lpalbou/abstractcore-server` | 2.15.2 |

Optional add-ons that are not part of any profile install separately:
`pip install abstract3d` (0.3.1), `pip install abstractcamera` (0.2.0) and
`pip install abstractskill` (0.2.1).

See [docs/install.md](docs/install.md) for the full install chooser, `uv`/venv guidance,
`abstractframework doctor`, and the generated installer manifest contract.

---

## Documentation

| Page | What it covers |
|---|---|
| [docs/README.md](docs/README.md) | Documentation hub — pick your starting point |
| [docs/install.md](docs/install.md) | Light / Apple / GPU install chooser and first checks |
| [docs/getting-started.md](docs/getting-started.md) | Two entry points + first end-to-end run |
| [docs/architecture.md](docs/architecture.md) | Layered model, durable execution primitives, comparisons |
| [docs/configuration.md](docs/configuration.md) | Minimal config, where defaults live, Core vs Gateway |
| [docs/glossary.md](docs/glossary.md) | Shared terminology (run, ledger, effect, wait, bundle, …) |
| [docs/faq.md](docs/faq.md) | Common questions, comparisons, limits |
| [docs/troubleshooting.md](docs/troubleshooting.md) | Symptoms, checks and fixes for install, sign-in, network and provider problems |
| [docs/api.md](docs/api.md) | Meta-package API (pins, helpers, re-exports, `abstractframework doctor`) |
| [docs/workspace-scripts.md](docs/workspace-scripts.md) | Working from source: package tiers, build, status, pull/commit/push scripts |
| [CHANGELOG.md](CHANGELOG.md) | Release history of the meta-package and its pins |
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to work on this repository and propose changes |
| [SECURITY.md](SECURITY.md) | How to report a vulnerability |

---

## Developer setup (from source)

Clone all sibling repos and build everything in editable mode:

```bash
./scripts/clone.sh           # clone every sibling repository next to this one
./scripts/deps.sh            # dependency tiers: what builds and installs first, and why
source ./scripts/build.sh    # Python (editable, into .venv), npm and Rust builds, tier by tier
```

Keep the whole workspace in sync with `./scripts/status.sh` (git overview per tier; `--registry`
compares local versions with PyPI, npm and crates.io), `./scripts/pull.sh`, `./scripts/commit.sh`
and `./scripts/push.sh` (a dry run until you add `--yes`). See
[docs/workspace-scripts.md](docs/workspace-scripts.md) for every script and option.

Then configure providers and models in a console (`abstractcore serve` or
`abstractgateway serve`, then open the printed link), or from the terminal:

```bash
abstractcore --config    # interactive configuration wizard
abstractcore --install   # check every subsystem and download missing models and dependencies
```

---

## License

MIT. See [LICENSE](LICENSE). Credits: [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md). Community
expectations: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).

--- llms.txt ---
# AbstractFramework
> Meta-package + documentation hub for the AbstractFramework ecosystem (durable, observable agents/workflows).

This repo pins ecosystem package versions and provides a tiny helper API in `abstractframework/`.
Most runtime behavior lives in the individual package repos (AbstractCore/Runtime/Agent/Flow/Gateway/Code).
AbstractCode is a Rust terminal client (`cargo install abstractcode`) plus a browser client (`npx @abstractframework/code`); both need a running gateway.

Quick commands:
- Mac installer: download `https://github.com/lpalbou/AbstractFramework/releases/latest/download/AbstractFramework-Installer.pkg` and double-click it; the package is not signed with an Apple Developer ID, so the first time allow it with Open Anyway in System Settings > Privacy & Security; it runs `install.sh --interactive` in Terminal. Remove with `Uninstall AbstractFramework.command` or `curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/uninstall.sh | sh`
- One-line install (gateway + console, no admin, no system Python): `curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh` (Windows: `powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"`); installs uv + Python 3.12 + the pinned gateway as a uv tool, registers the login service, starts it on 127.0.0.1:8080 and opens `/console` through a one-time claim link; `--print` dry run, `--uninstall` to remove
- Network exposure: the gateway listens on this computer only (`localhost`) until you run `abstractgateway network set lan|internet` (or use the console / menu-bar icon); `service install` takes `--port`, and the login item runs plain `serve` so the Network setting applies
- Start either entry point and open the printed one-time link: `abstractcore serve` (http://127.0.0.1:8000/console#claim=…) or `abstractgateway serve` (http://127.0.0.1:8080/console#claim=…); both consoles have Models (catalog + fit verdict, download, delete) and Engines (detect, install) tabs, mirrored by `abstractcore models|engines` and `abstractgateway models|engines`
- Install pinned stack: `pip install abstractframework`
- Check install health: `abstractframework doctor`
- Print installer manifest: `abstractframework manifest`
- Show detected component versions: `python -c "from abstractframework import print_status; print_status()"`

Notes for LLMs working in this repo:
- Prefer `docs/` as the ecosystem-level source of truth.
- Keep changes minimal and ecosystem-level (docs + version pins).
- If you need to change runtime behavior, you likely want the relevant package repo instead of this meta repo.
- Gateway-first hosted browser setups use Gateway user auth, opaque browser
  sessions, and `/console` for account/admin/defaults management. AbstractFlow,
  AbstractCode Web, and AbstractObserver exchange user tokens for app-scoped
  browser sessions and do not persist bearer tokens in browser settings.
  Gateway login responses do not expose session ids/CSRF tokens in JSON, and
  default resolution is Core defaults -> Gateway baseline Core config ->
  per-runtime Core config override.
  Do not treat the bootstrap `ABSTRACTGATEWAY_AUTH_TOKEN` as a browser-user
  login token. Gateway session cookies are HTTP-only for the session id, use a
  separate CSRF cookie, set `Secure` only under HTTPS, expire, logout cleanly,
  and are revoked when the backing user is disabled/deleted/token-rotated.
- Gateway user isolation is enforced through per-principal runtime routing plus
  a central route-family policy. Ordinary users can use their own runs, ledgers,
  artifacts, KG/session memory, private workflows, session artifacts,
  prompt-cache session names, and runtime-scoped Core defaults. Admin-only workspace
  helpers/import/export and provider prompt-cache controls are advertised as
  unavailable in discovery with machine-readable `admin_required` metadata.
  Deleting a user reserves the retained runtime id for that principal so the
  runtime cannot be reassigned to a different same-tenant user while data is
  still retained. Admins may explicitly transfer a retained runtime to an
  existing same-tenant user or purge the retained runtime directory before
  releasing the runtime id for reuse.
- Gateway provider connections are Gateway-owned reusable profiles for OpenAI,
  Anthropic, OpenRouter, Portkey, LM Studio, Ollama, and custom
  OpenAI-compatible endpoints. They include a description, provider family,
  base URL, optional model allowlist, and a write-only API key. Discovery
  exposes them as virtual providers such as `endpoint:office-vllm`; Runtime
  resolves the virtual id into transient provider/base URL/key parameters
  without storing secrets in workflow JSON or browser storage.
- AbstractCore also supports local single-principal provider endpoint profiles
  through `abstractcore config set-provider ...`. These profiles expose the
  same virtual provider shape (`endpoint:<id>`) for Core-only developers, while
  Gateway remains responsible for hosted user/runtime scoping.
- Gateway Console separates provider configuration from defaults: the Providers
  tab stores endpoint URLs/API keys and tests discovery; the Defaults tab only
  maps capability routes to configured virtual providers and discovered models.
  LLM/embedding model pickers use Core `capability_route` filters such as
  `input.image,output.text` and `embedding.text`; generated-media defaults use
  capability plugin catalogs.
- AbstractFlow uses the same route-aware discovery for text model selectors and
  Models Catalog nodes. Gateway `/runs/start` accepts optional `thinking`, and
  Flow LLM Call/Agent nodes can set or pin `thinking` so reasoning controls
  flow through Runtime and AbstractAgent into Core.
- Hosted Flow, Code Web, and Observer use the server-configured Gateway URL on
  non-loopback UI hostnames. Browser-supplied Gateway URL changes are rejected
  unless the app-specific remote override is enabled behind access control.
  Host checks trust `Host` by default; forwarded host headers require an
  explicit trust-proxy env setting.
- Gateway workflow sharing uses the explicit workflow catalog, not another
  user's private `/bundles` directory. Catalog bundle versions are immutable,
  admins move default pointers and ACLs, and users start catalog workflows with
  `registry_scope: "tenant_catalog"` so the run executes in their own runtime.
  Catalog scope is explicit, direct private-bundle routes reject
  catalog-internal ids, and Gateway signs catalog workflow policy before
  passing it to Runtime.
- A packaged Gateway serves shipped workflow bundles out of the box, with no
  manual install: `basic-agent` (default chat agent), `coding-agent` (a
  verify-gated coder, `coder` entrypoint), `deep-research`, `co-scientist`,
  `docs-qa`, `abstractassistant-orchestrator`, and the `react-agent` /
  `codeact-agent` / `memact-agent` native loops. Setting
  `ABSTRACTGATEWAY_FLOWS_DIR` replaces that registry with a custom one.

## Start Here

- [README.md](README.md): Ecosystem overview and install paths.
- [docs/README.md](docs/README.md): Docs index for this repo.
- [docs/install.md](docs/install.md): Mac installer (with the one-time Open Anyway step), one-line bootstrap (install.sh / install.ps1: what it does, options, CLI equivalents, run at login, Network setting, uninstall), Light / Apple / GPU chooser, generated manifest contract.
- [docs/installers/README.md](docs/installers/README.md): Install design: script bootstrap + gateway console (strategy, per-OS journeys, components, OS security/sudo/UAC/execution policy, manifest, operations).
- [docs/getting-started.md](docs/getting-started.md): Practical entry paths (core, runtime, gateway, UIs, bundles).
- [AbstractGateway shipped workflows](https://github.com/lpalbou/AbstractGateway/blob/main/docs/shipped-workflows.md): Workflows a fresh Gateway install already serves (coder, deep research, co-scientist) + how to run one.
- [docs/architecture.md](docs/architecture.md): How the stack fits together (Mermaid component and distribution diagrams).
- [docs/configuration.md](docs/configuration.md): Minimal config, Core vs Gateway defaults, Network exposure, client configuration.
- [docs/api.md](docs/api.md): Meta-package API: release pins, helpers, `abstractframework doctor` and `manifest`.
- [docs/faq.md](docs/faq.md): Common questions, comparisons, limits.
- [docs/troubleshooting.md](docs/troubleshooting.md): Symptoms, checks and fixes (installer blocked by macOS, sign-in links, ports, network access, providers, logs).
- [docs/glossary.md](docs/glossary.md): Shared terminology used across docs.
- [docs/workspace-scripts.md](docs/workspace-scripts.md): Working from source: package inventory (scripts/lib/packages.txt), dependency tiers, build/status/pull/commit/push scripts.
- [docs/adr/README.md](docs/adr/README.md): Cross-package ADR index and accepted platform contracts.

## Scenarios

- [docs/scenarios/README.md](docs/scenarios/README.md): Scenarios index.
- [docs/scenarios/offline-coding-assistant.md](docs/scenarios/offline-coding-assistant.md): Local durable coding assistant.
- [docs/scenarios/gateway-first-local-dev.md](docs/scenarios/gateway-first-local-dev.md): Local gateway + web UIs.
- [docs/scenarios/specialized-agent-flow.md](docs/scenarios/specialized-agent-flow.md): Portable `.flow` agent across clients.
- [docs/scenarios/workflow-bundle-lifecycle.md](docs/scenarios/workflow-bundle-lifecycle.md): Publish/install/deprecate bundles.
- [docs/scenarios/phone-thin-client.md](docs/scenarios/phone-thin-client.md): iPhone via Web/PWA + gateway.
- [docs/scenarios/telegram-permanent-contact.md](docs/scenarios/telegram-permanent-contact.md): Telegram bridge + workflow.
- [docs/scenarios/email-inbox-agent.md](docs/scenarios/email-inbox-agent.md): Email bridge + workflow.

## Guides

- [docs/guide/README.md](docs/guide/README.md): Guides index.
- [docs/guide/deployment-topologies.md](docs/guide/deployment-topologies.md): Supported deployment patterns.
- [docs/guide/deployment-web.md](docs/guide/deployment-web.md): Browser UIs and gateway setup.
- [docs/guide/deployment-iphone.md](docs/guide/deployment-iphone.md): iPhone/PWA notes.
- [docs/guide/gateway-security.md](docs/guide/gateway-security.md): Safe gateway exposure defaults.
- [docs/guide/capability-routing-defaults.md](docs/guide/capability-routing-defaults.md): Provider/model/base URL defaults for input, output, embedding, and rerank routes.
- [docs/guide/runtime-artifacts.md](docs/guide/runtime-artifacts.md): Runtime artifact descriptors, Gateway artifact search, Observer views, ledger/KG boundaries, and safe retrieval practice.
- [docs/guide/workflow-bundles.md](docs/guide/workflow-bundles.md): `.flow` bundles and lifecycle.
- [docs/guide/agent-skills.md](docs/guide/agent-skills.md): Agent Skills (SKILL.md) proposal and how it composes with flows.
- [docs/guide/process-manager-env-vars.md](docs/guide/process-manager-env-vars.md): Write-only env var config from Observer.

## Release Profile (Pinned Versions)

- [pyproject.toml](pyproject.toml): Single source of truth for pinned ecosystem versions and extras.
- [abstractframework/__init__.py](abstractframework/__init__.py): `RELEASE_VERSIONS`, `NPM_RELEASE_VERSIONS`, `get_release_profile()`, `print_status()`.
- [docs/installers/install-manifest.json](docs/installers/install-manifest.json): Generated install manifest, schema v2 (pins, profiles, npm apps, `bootstrap` gateway pin/scripts/flags, console-first `post_install`).
- [scripts/install.sh](scripts/install.sh) / [scripts/install.ps1](scripts/install.ps1): The bootstrap installers.
- abstractframework 0.3.2 pins (PyPI, `==`): abstractgateway 0.4.3, abstractassistant 0.5.0, abstractcore 2.15.2, AbstractRuntime 0.4.35, abstractagent 0.3.13, AbstractMemory 0.3.0, abstractsemantics 0.0.5, abstractvoice 0.11.4, abstractvision 0.3.29, abstractmusic 0.1.15.
- Released with it: npm `@abstractframework/flow` 0.3.20, `code` 0.4.2, `observer` 0.1.12, `continuum` 0.3.1, `entity` 0.2.1 (run with `npx`); crates (`CRATE_RELEASE_VERSIONS`) `abstractgateway-console` 0.8.0, `abstractcore-console` 0.2.0, `abstractcode` 0.5.1, `abstracttui` 0.6.0; images `ghcr.io/lpalbou/abstractgateway:0.4.3`, `ghcr.io/lpalbou/abstractcore-server:2.15.2`.
- Profiles: light (remote-only), apple (`abstractframework[apple]`, macOS 14+ Apple Silicon), gpu (`abstractframework[gpu]`); Python 3.10-3.13 (F5-TTS voice cloning in apple/gpu needs 3.11+).

## Full Context

- [llms-full.txt](llms-full.txt): Concatenated context for copy/paste into an LLM (`python scripts/gen_llms_full.py`).

## Proposals / Research (non-canonical)

- [docs/backlog/overview.md](docs/backlog/overview.md): Root backlog index and active cross-package work.
- [docs/backlog/completed/0141_flow_browser_session_gateway_auth.md](docs/backlog/completed/0141_flow_browser_session_gateway_auth.md): Completed Flow browser-session Gateway auth isolation.
- [docs/backlog/planned/0142_gateway_tenant_isolation_and_shared_runtime.md](docs/backlog/planned/0142_gateway_tenant_isolation_and_shared_runtime.md): Planned Gateway tenant isolation and shared runtime design.
- [docs/backlog/planned/0143_shared_gateway_per_principal_runtime_router.md](docs/backlog/planned/0143_shared_gateway_per_principal_runtime_router.md): In-progress shared Gateway per-principal runtime router work.
- [docs/backlog/planned/gateway-control-plane/README.md](docs/backlog/planned/gateway-control-plane/README.md): Gateway admin/config/workflow-permission control-plane backlog track.
- [docs/backlog/proposed/gateway-control-plane/README.md](docs/backlog/proposed/gateway-control-plane/README.md): Proposed Explorer/Manager package-boundary follow-ups.
- [docs/backlog/completed/0154_multi_user_security_release_blockers.md](docs/backlog/completed/0154_multi_user_security_release_blockers.md): Completed multi-user release-blocking security fixes.
- [docs/backlog/completed/0157_gateway_provider_endpoint_profiles.md](docs/backlog/completed/0157_gateway_provider_endpoint_profiles.md): Completed Gateway-owned provider endpoint profiles and virtual `endpoint:*` discovery.
- [docs/backlog/planned/0164_gateway_docker_ghcr_deployment_track.md](docs/backlog/planned/0164_gateway_docker_ghcr_deployment_track.md): Planned Gateway Docker/GHCR deployment track for light and GPU images.
- [docs/backlog/proposed/gateway-control-plane/0155_hosted_proxy_shared_helper_extraction.md](docs/backlog/proposed/gateway-control-plane/0155_hosted_proxy_shared_helper_extraction.md): Proposed threshold for extracting shared hosted proxy helpers.
- [docs/backlog/planned/074_agent_skills_integration.md](docs/backlog/planned/074_agent_skills_integration.md): Backlog item for Agent Skills integration.
- [docs/backlog/planned/074_agent_skills_integration_plan.md](docs/backlog/planned/074_agent_skills_integration_plan.md): Phased plan for Agent Skills integration.
- [docs/skills/](docs/skills/): Research notes (spec links + ecosystem scan) for Agent Skills.
- [docs/claude/](docs/claude/): Research notes about Claude (non-Agent-Skills capability/skills scan).

## Optional

- [CHANGELOG.md](CHANGELOG.md): Release history of the meta-package and its pins.
- [CONTRIBUTING.md](CONTRIBUTING.md): Working on this repository (tests, doc regeneration, pull requests).
- [SECURITY.md](SECURITY.md): Reporting a vulnerability.
- [ACKNOWLEDGEMENTS.md](ACKNOWLEDGEMENTS.md): Credits.

- [AbstractCore](https://github.com/lpalbou/abstractcore): LLM providers, tools, structured output, media.
- [AbstractRuntime](https://github.com/lpalbou/abstractruntime): Durable execution (runs, effects, waits, ledger, stores).
- [AbstractAgent](https://github.com/lpalbou/abstractagent): Agent patterns (ReAct/CodeAct/MemAct) built on runtime + core.
- [AbstractFlow](https://github.com/lpalbou/abstractflow): npm web editor for VisualFlow authoring on top of AbstractGateway.
- [AbstractGateway](https://github.com/lpalbou/abstractgateway): HTTP/SSE control plane for gateway-first deployments.
- [AbstractCode](https://github.com/lpalbou/abstractcode): Terminal + web host UIs for durable runs.

--- pyproject.toml ---
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "abstractframework"
version = "0.3.2"
description = "Unified installer and documentation hub for the AbstractFramework ecosystem"
readme = "README.md"
license = {text = "MIT"}
authors = [
    {name = "Laurent-Philippe Albou", email = "contact@abstractframework.ai"}
]
keywords = [
    "agentic-os",
    "ai-agents",
    "llm",
    "autonomous-agents",
    "workflows",
    "multi-agent",
    "durable-execution",
    "knowledge-graph",
    "openai",
    "anthropic",
    "ollama",
    "local-llm"
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Operating System :: OS Independent",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Programming Language :: Python :: 3.13",
    "Topic :: Scientific/Engineering :: Artificial Intelligence",
    "Typing :: Typed",
]
requires-python = ">=3.10"

# Base dependencies (remote-first, lightweight).
#
# Design constraints:
# - `pip install abstractframework` should default to *remote inference* (no MLX/vLLM/Diffusers local engine stacks).
# - Hardware-local stacks are opt-in via `abstractframework[apple]` and `abstractframework[gpu]`.
# - Keep the meta-package aligned with the real per-package profiles (especially `abstractgateway`).
dependencies = [
    # Durable Gateway control plane (remote-light by default).
    "abstractgateway==0.4.3",

    # Workflow authoring UI is distributed through npm as @abstractframework/flow.
    "abstractassistant==0.5.0",

    # Pin the core Python stack for release determinism (also pulled transitively).
    "abstractcore==2.15.2",
    "AbstractRuntime==0.4.35",
    "abstractagent==0.3.13",
    "AbstractMemory==0.3.0",
    "abstractsemantics==0.0.5",

    # Capability plugins are remote-first by default; local runtimes live behind
    # their package-specific `apple`/`gpu` (or `all-*`) extras.
    "abstractvoice==0.11.4",
    "abstractvision==0.3.29",
    "abstractmusic==0.1.15",
]

[project.optional-dependencies]
# NOTE: Extras here are additive. The base install already pins and installs the
# full Python ecosystem; `apple`/`gpu` select hardware-local stacks and upgrade
# apps to their matching profiles.
#
# AbstractObserver is distributed as an npm package (`npx @abstractframework/observer`),
# so it is not installable via pip extras.

# Native macOS Python installs. These are intentionally not Docker profiles:
# MLX/Metal-backed engines run on the host Apple runtime. This profile upgrades
# the stack by selecting the hardware-local dependency aggregates (owned by Gateway),
# then adds user-facing apps.
apple = [
    "abstractgateway[apple]==0.4.3",
    "abstractassistant[apple]==0.5.0; platform_system == \"Darwin\"",
]

gpu = [
    "abstractgateway[gpu]==0.4.3",
    "abstractassistant[gpu]==0.5.0",
]

[project.urls]
Homepage = "https://github.com/lpalbou/AbstractFramework"
Documentation = "https://github.com/lpalbou/AbstractFramework/tree/main/docs"
Repository = "https://github.com/lpalbou/AbstractFramework"
Issues = "https://github.com/lpalbou/AbstractFramework/issues"

[project.scripts]
abstractframework = "abstractframework.cli:main"

[tool.setuptools.packages.find]
where = ["."]
include = ["abstractframework*"]

[tool.setuptools.package-data]
abstractframework = ["py.typed"]

[tool.black]
line-length = 100
target-version = ["py310", "py311", "py312"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W"]

[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true

[tool.pytest.ini_options]
testpaths = ["tests"]

--- abstractframework/__init__.py ---
"""
AbstractFramework unified distribution package.

This package provides:
- a single install entrypoint for the full AbstractFramework ecosystem
- lightweight helpers to inspect installed component versions

Most implementation functionality still lives in component projects.
"""

from __future__ import annotations

__version__ = "0.3.2"
__author__ = "Laurent-Philippe Albou"
__license__ = "MIT"

RELEASE_VERSIONS: dict[str, str] = {
    "abstractcore": "2.15.2",
    "abstractruntime": "0.4.35",
    "abstractagent": "0.3.13",
    "abstractgateway": "0.4.3",
    "abstractmemory": "0.3.0",
    "abstractsemantics": "0.0.5",
    "abstractvoice": "0.11.4",
    "abstractvision": "0.3.29",
    "abstractmusic": "0.1.15",
    "abstractassistant": "0.5.0",
}

PACKAGE_DISTRIBUTIONS: dict[str, str] = {
    "abstractcore": "abstractcore",
    "abstractruntime": "AbstractRuntime",
    "abstractagent": "abstractagent",
    "abstractgateway": "abstractgateway",
    "abstractmemory": "AbstractMemory",
    "abstractsemantics": "abstractsemantics",
    "abstractvoice": "abstractvoice",
    "abstractvision": "abstractvision",
    "abstractmusic": "abstractmusic",
    "abstractassistant": "abstractassistant",
}

NPM_RELEASE_VERSIONS: dict[str, str] = {
    "@abstractframework/flow": "0.3.20",
    "@abstractframework/code": "0.4.2",
    "@abstractframework/observer": "0.1.12",
    "@abstractframework/continuum": "0.3.1",
    "@abstractframework/entity": "0.2.1",
}

# Terminal tools published on crates.io alongside this release (installed with `cargo install`,
# or by the bootstrap scripts' --with-console / --with-code-cli flags).
CRATE_RELEASE_VERSIONS: dict[str, str] = {
    "abstractgateway-console": "0.8.0",
    "abstractcore-console": "0.2.0",
    "abstractcode": "0.5.1",
    "abstracttui": "0.6.0",
}

CORE_DEFAULT_EXTRAS = [
    "remote",
    "tools",
    "media",
    "vision",
    "voice",
    "audio",
    "music",
]

# Convenience re-exports (AbstractCore is a base dependency of this meta-package).
# Keep this import lightweight: do not import optional tool/media deps here.
try:
    from abstractcore import GenerateResponse, create_llm  # type: ignore

    __all__ = [
        "CORE_DEFAULT_EXTRAS",
        "RELEASE_VERSIONS",
        "GenerateResponse",
        "create_llm",
        "get_installed_packages",
        "get_release_profile",
        "print_status",
    ]
except Exception:  # pragma: no cover
    __all__ = [
        "CORE_DEFAULT_EXTRAS",
        "RELEASE_VERSIONS",
        "get_installed_packages",
        "get_release_profile",
        "print_status",
    ]


def get_release_profile() -> dict[str, object]:
    """Return the pinned global release profile shipped by this package."""

    return {
        "abstractframework": __version__,
        "packages": RELEASE_VERSIONS.copy(),
        "distributions": PACKAGE_DISTRIBUTIONS.copy(),
        "npm_packages": NPM_RELEASE_VERSIONS.copy(),
        "crates": CRATE_RELEASE_VERSIONS.copy(),
        "core_extras": list(CORE_DEFAULT_EXTRAS),
        "install_profiles": {
            "light": "pip install abstractframework",
            "apple": 'pip install "abstractframework[apple]"',
            "gpu": 'pip install "abstractframework[gpu]"',
        },
    }


def get_installed_packages() -> dict[str, str]:
    """Return a dict of installed AbstractFramework Python packages and versions."""

    packages: dict[str, str] = {}

    def _maybe_add(import_name: str) -> None:
        try:
            mod = __import__(import_name)
            packages[import_name] = getattr(mod, "__version__", "installed")
        except Exception:
            return

    for name in PACKAGE_DISTRIBUTIONS:
        _maybe_add(name)

    return packages


def print_status() -> None:
    """Print installation status of the main AbstractFramework Python packages."""

    installed = get_installed_packages()
    all_packages = list(PACKAGE_DISTRIBUTIONS)

    print("AbstractFramework installation status")
    print("=" * 40)

    for pkg in all_packages:
        if pkg in installed:
            print(f"  ✓ {pkg}: {installed[pkg]}")
        else:
            print(f"  ✗ {pkg}: not installed")

    print("")
    print(f"Installed: {len(installed)}/{len(all_packages)} packages")

    if len(installed) < len(all_packages):
        print("")
        print("To install the framework profile:")
        print("  pip install abstractframework")
        print("")
        print("Hardware-local profiles:")
        print('  pip install "abstractframework[apple]"')
        print('  pip install "abstractframework[gpu]"')

--- abstractframework/install_manifest.py ---
"""Generated install manifest helpers for AbstractFramework."""

from __future__ import annotations

import json
from pathlib import Path
from typing import Any

from . import NPM_RELEASE_VERSIONS, PACKAGE_DISTRIBUTIONS, RELEASE_VERSIONS, __version__

MANIFEST_SCHEMA_VERSION = 2
MINIMUM_INSTALLER_VERSION = "0.2.0"

REPOSITORY = "https://github.com/lpalbou/AbstractFramework"
RAW_SCRIPTS = "https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts"

# Python version the bootstrap provisions through uv. 3.12 satisfies MLX, F5-TTS (3.11+)
# and vLLM (3.10-3.13); it is passed explicitly because the packages have no upper bound.
BOOTSTRAP_PYTHON = "3.12"
DEFAULT_GATEWAY_PORT = 8080

# Flags shared by scripts/install.sh (POSIX) and scripts/install.ps1 (PowerShell).
BOOTSTRAP_FLAGS: list[dict[str, str]] = [
    {"sh": "--profile auto|light|apple|gpu", "ps": "-Profile", "env": "AF_PROFILE",
     "summary": "Install profile; auto picks apple on Apple Silicon macOS 14+, gpu when "
                "nvidia-smi/rocminfo works, light otherwise."},
    {"sh": "--port N", "ps": "-Port", "env": "AF_PORT",
     "summary": "Gateway port (default 8080; the next free port when 8080 is busy)."},
    {"sh": "--pin VERSION|latest", "ps": "-Pin", "env": "AF_PIN",
     "summary": "abstractgateway version (default: bootstrap.gateway_version)."},
    {"sh": "--from PATH|REQUIREMENT", "ps": "-From", "env": "AF_FROM",
     "summary": "Install the gateway from a checkout, wheel or requirement (testing)."},
    {"sh": "--data-dir DIR", "ps": "-DataDir", "env": "AF_DATA_DIR",
     "summary": "Gateway data dir (default: the per-OS user data dir)."},
    {"sh": "--with-apps", "ps": "-WithApps", "env": "",
     "summary": "Ensure Node.js >= 18 for the npx browser apps (uv tool install nodejs-wheel)."},
    {"sh": "--with-console", "ps": "-WithConsole", "env": "",
     "summary": "cargo install abstractgateway-console when cargo exists."},
    {"sh": "--with-code-cli", "ps": "-WithCodeCli", "env": "",
     "summary": "cargo install abstractcode (terminal client) when cargo exists."},
    {"sh": "--with-core-cli", "ps": "-WithCoreCli", "env": "",
     "summary": "Also expose the abstractcore command (--with-executables-from abstractcore)."},
    {"sh": "--with-ollama", "ps": "-WithOllama", "env": "",
     "summary": "Run Ollama's official installer (Linux/macOS may ask for sudo)."},
    {"sh": "--with-lmstudio", "ps": "-WithLmStudio", "env": "",
     "summary": "Install LM Studio (headless llmster on macOS/Linux, winget on Windows)."},
    {"sh": "--no-tray", "ps": "-NoTray", "env": "", "summary": "Skip the tray extra."},
    {"sh": "--no-service", "ps": "-NoService", "env": "",
     "summary": "Do not register a login service; start the gateway in the background."},
    {"sh": "--no-start", "ps": "-NoStart", "env": "", "summary": "Install only."},
    {"sh": "--no-open", "ps": "-NoOpen", "env": "", "summary": "Do not open the browser."},
    {"sh": "--print", "ps": "-Print", "env": "",
     "summary": "Dry run: preflight, then print every command; change nothing."},
    {"sh": "--uninstall [--purge]", "ps": "-Uninstall [-Purge]", "env": "",
     "summary": "Remove the service and uv tools (purge also deletes the data dir)."},
]


def _python_packages() -> list[dict[str, str]]:
    return [
        {
            "id": package_id,
            "distribution": PACKAGE_DISTRIBUTIONS[package_id],
            "version": version,
            "registry": "pypi",
        }
        for package_id, version in RELEASE_VERSIONS.items()
    ]


def _npm_apps() -> list[dict[str, str]]:
    return [
        {
            "id": package_name.rsplit("/", 1)[-1],
            "package": package_name,
            "version": version,
            "registry": "npm",
            "command": f"npx {package_name}",
        }
        for package_name, version in NPM_RELEASE_VERSIONS.items()
    ]


def build_install_manifest() -> dict[str, Any]:
    """Build the installer-consumable manifest from root release pins."""

    return {
        "schema_version": MANIFEST_SCHEMA_VERSION,
        "minimum_installer_version": MINIMUM_INSTALLER_VERSION,
        "framework": {
            "id": "abstractframework",
            "name": "AbstractFramework",
            "distribution": "abstractframework",
            "version": __version__,
            "registry": "pypi",
            "python_requires": ">=3.10",
        },
        "source": {
            "repository": "https://github.com/lpalbou/AbstractFramework",
            "release_profile": "abstractframework.RELEASE_VERSIONS",
        },
        "profiles": [
            {
                "id": "light",
                "name": "Light",
                "summary": (
                    "Remote-first install. Full framework functionality is available through "
                    "remote or OpenAI-compatible endpoints; no local MLX, CUDA, Diffusers, or "
                    "model-runtime stacks are installed by this profile."
                ),
                "pip_requirements": [f"abstractframework=={__version__}"],
                "local_inference": False,
                "platforms": ["macos", "linux", "windows"],
                "prerequisites": ["python>=3.10", "network"],
                "best_for": [
                    "cloud APIs",
                    "LM Studio, Ollama, vLLM, llama.cpp, or other endpoint servers",
                    "lowest-friction install",
                ],
                "excludes": ["local MLX engines", "local CUDA/ROCm engines"],
            },
            {
                "id": "apple",
                "name": "Apple",
                "summary": (
                    "Native Apple Silicon profile. Adds local MLX/Metal-capable stacks on top "
                    "of the same framework interfaces and endpoint providers."
                ),
                "pip_requirements": [f"abstractframework[apple]=={__version__}"],
                "local_inference": True,
                "platforms": ["macos"],
                "prerequisites": ["python>=3.10", "apple-silicon", "macos>=14", "network"],
                "best_for": ["Mac users who want local Apple Silicon inferencers"],
                "excludes": ["CUDA/ROCm engines"],
            },
            {
                "id": "gpu",
                "name": "GPU",
                "summary": (
                    "Native GPU profile. Adds CUDA/ROCm-oriented local stacks on top of the "
                    "same framework interfaces and endpoint providers."
                ),
                "pip_requirements": [f"abstractframework[gpu]=={__version__}"],
                "local_inference": True,
                "platforms": ["linux", "windows"],
                "prerequisites": ["python>=3.10", "gpu-driver", "network"],
                "best_for": ["workstations or servers with supported discrete GPUs"],
                "excludes": ["Apple MLX-only engines"],
            },
        ],
        "python_packages": _python_packages(),
        "npm_apps": _npm_apps(),
        "bootstrap": _bootstrap(),
        "post_install": _post_install(),
        "security": {
            "secrets_in_manifest": False,
            "native_artifacts_signed": False,
            "notes": (
                "The bootstrap is a script (curl | sh, irm | iex) that installs signed-by-vendor "
                "or PyPI/npm artifacts; nothing it runs needs our code signature. Only native "
                "double-click apps (AbstractAssistant .app, future launchers) need signing."
            ),
        },
    }


def _bootstrap() -> dict[str, Any]:
    gateway_version = RELEASE_VERSIONS["abstractgateway"]
    return {
        "gateway_version": gateway_version,
        "python": BOOTSTRAP_PYTHON,
        "tool_requirement": "abstractgateway[{extras}]==" + gateway_version,
        "profile_extras": {"light": [], "apple": ["apple"], "gpu": ["gpu"]},
        "optional_extras": ["tray"],
        "default_port": DEFAULT_GATEWAY_PORT,
        "scripts": {
            "unix": {
                "url": f"{RAW_SCRIPTS}/install.sh",
                "platforms": ["macos", "linux"],
                "one_liner": f"curl -LsSf {RAW_SCRIPTS}/install.sh | sh",
                "with_flags": f"curl -LsSf {RAW_SCRIPTS}/install.sh | sh -s -- --with-apps",
            },
            "windows": {
                "url": f"{RAW_SCRIPTS}/install.ps1",
                "platforms": ["windows"],
                "one_liner": (
                    'powershell -ExecutionPolicy ByPass -c "irm '
                    f'{RAW_SCRIPTS}/install.ps1 | iex"'
                ),
                "with_flags": (
                    "& ([scriptblock]::Create((irm "
                    f"{RAW_SCRIPTS}/install.ps1))) -WithApps"
                ),
            },
        },
        "flags": BOOTSTRAP_FLAGS,
        "steps": [
            "preflight (OS, arch, macOS >= 14 for apple, GPU driver for gpu, disk, port)",
            "install uv when missing (astral.sh official script, no admin)",
            f"uv python install {BOOTSTRAP_PYTHON}",
            f'uv tool install --python {BOOTSTRAP_PYTHON} "abstractgateway[<extras>]==<pin>"',
            "optional: nodejs-wheel, cargo crates, Ollama / LM Studio vendor installers",
            "abstractgateway service install (when supported) or a background start",
            "wait for GET /api/health",
            "abstractgateway-config claim-url (when supported) and open /console",
        ],
        "uninstall": [
            ["abstractgateway", "service", "uninstall"],
            ["uv", "tool", "uninstall", "abstractgateway"],
        ],
    }


def _post_install() -> dict[str, Any]:
    base_url = f"http://127.0.0.1:{DEFAULT_GATEWAY_PORT}"
    return {
        "entrypoint": "console",
        "gateway": [
            "abstractgateway",
            "serve",
            "--host",
            "127.0.0.1",
            "--port",
            str(DEFAULT_GATEWAY_PORT),
        ],
        "health_url": f"{base_url}/api/health",
        "console_url": f"{base_url}/console",
        "claim": ["abstractgateway-config", "claim-url"],
        "claim_fallback": {
            "token_file": "<data_dir>/auth/bootstrap-admin-token",
            "command": ["abstractgateway-config", "bootstrap-admin", "--print-token"],
        },
        "service": ["abstractgateway", "service", "install"],
        "doctor": ["abstractframework", "doctor"],
        "apps": [["npx", "-y", package] for package in NPM_RELEASE_VERSIONS],
        "notes": (
            "Providers, API keys, engines, models and users are configured in the gateway "
            "console (/console first-run wizard); every console action prints its CLI twin."
        ),
    }


def manifest_json(indent: int = 2) -> str:
    """Return the install manifest as stable JSON."""

    return json.dumps(build_install_manifest(), indent=indent, sort_keys=True) + "\n"


def write_install_manifest(path: str | Path) -> None:
    """Write the generated install manifest to a path."""

    Path(path).write_text(manifest_json(), encoding="utf-8")


def check_install_manifest(path: str | Path) -> tuple[bool, str]:
    """Compare a checked-in manifest file with the generated manifest."""

    manifest_path = Path(path)
    expected = manifest_json()
    actual = manifest_path.read_text(encoding="utf-8")
    if actual == expected:
        return True, f"{manifest_path} is up to date"
    return False, f"{manifest_path} differs from generated AbstractFramework install manifest"

--- abstractframework/cli.py ---
"""Command line helpers for the AbstractFramework meta-package."""

from __future__ import annotations

import argparse
import importlib.metadata
import json
import os
import platform
import re
import shutil
import subprocess
import sys
import urllib.error
import urllib.request
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Sequence

from . import PACKAGE_DISTRIBUTIONS, RELEASE_VERSIONS, __version__
from .install_manifest import check_install_manifest, manifest_json, write_install_manifest


STATUS_RANK = {"error": 2, "warn": 1, "ok": 0, "info": 0}

DEFAULT_GATEWAY_URL = "http://127.0.0.1:8080"
DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
DEFAULT_LMSTUDIO_URL = "http://127.0.0.1:1234/v1"
SUPPORTED_PYTHON = ((3, 10), (3, 13))
MIN_NODE_MAJOR = 18
MIN_MACOS_MAJOR = 14
DISK_WARN_BYTES = 5 * 1024**3
DISK_ERROR_BYTES = 1 * 1024**3


@dataclass(frozen=True)
class Check:
    """One doctor finding. ``status`` is ok | warn | error | info (info never fails)."""

    id: str
    status: str
    message: str
    detail: str | None = None
    data: dict[str, Any] = field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        out: dict[str, Any] = {"id": self.id, "status": self.status, "message": self.message}
        if self.detail:
            out["detail"] = self.detail
        if self.data:
            out["data"] = self.data
        return out


def _distribution_version(distribution: str) -> str | None:
    try:
        return importlib.metadata.version(distribution)
    except importlib.metadata.PackageNotFoundError:
        return None


def _command_version(command: str) -> str | None:
    executable = shutil.which(command)
    if not executable:
        return None
    try:
        result = subprocess.run(
            [executable, "--version"],
            check=False,
            capture_output=True,
            text=True,
            timeout=5,
        )
    except Exception:
        return "available"
    text = (result.stdout or result.stderr).strip().splitlines()
    return text[0] if text else "available"


def _http_get_json(url: str, timeout: float) -> tuple[int | None, Any, str | None]:
    """Read-only GET. Returns (status, parsed JSON or None, error)."""

    request = urllib.request.Request(url, method="GET", headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:  # noqa: S310
            body = response.read(1_000_000)
            try:
                return response.status, json.loads(body.decode("utf-8")), None
            except (ValueError, UnicodeDecodeError):
                return response.status, None, None
    except urllib.error.HTTPError as exc:
        return exc.code, None, f"HTTP {exc.code}"
    except Exception as exc:  # connection refused, timeout, DNS ...
        reason = getattr(exc, "reason", exc)
        return None, None, str(reason)


def _uv_tool_bin_dir() -> Path:
    for env in ("UV_TOOL_BIN_DIR", "XDG_BIN_HOME"):
        value = os.environ.get(env)
        if value:
            return Path(value)
    return Path.home() / ".local" / "bin"


def _find_executable(name: str) -> str | None:
    found = shutil.which(name)
    if found:
        return found
    suffix = ".exe" if platform.system() == "Windows" else ""
    candidate = _uv_tool_bin_dir() / f"{name}{suffix}"
    return str(candidate) if candidate.exists() else None


def _parse_major(version_text: str | None) -> int | None:
    if not version_text:
        return None
    match = re.search(r"(\d+)(?:\.\d+)*", version_text)
    return int(match.group(1)) if match else None


def _macos_version() -> tuple[int, ...] | None:
    if platform.system() != "Darwin":
        return None
    raw = platform.mac_ver()[0]
    try:
        return tuple(int(part) for part in raw.split(".") if part)
    except ValueError:
        return None


def _gateway_url() -> str:
    return (os.environ.get("ABSTRACTGATEWAY_URL") or DEFAULT_GATEWAY_URL).rstrip("/")


def _ollama_url() -> str:
    raw = os.environ.get("OLLAMA_BASE_URL") or os.environ.get("OLLAMA_HOST") or DEFAULT_OLLAMA_URL
    if "://" not in raw:
        raw = f"http://{raw}"
    return raw.rstrip("/")


def _lmstudio_url() -> str:
    raw = (os.environ.get("LMSTUDIO_BASE_URL") or DEFAULT_LMSTUDIO_URL).rstrip("/")
    return raw if raw.endswith("/v1") else f"{raw}/v1"


def _python_check() -> Check:
    version = ".".join(str(part) for part in sys.version_info[:3])
    low, high = SUPPORTED_PYTHON
    data = {"version": version, "executable": sys.executable, "supported": "3.10-3.13"}
    if sys.version_info[:2] < low:
        return Check("python", "error", f"Python {version} is below the supported 3.10", data=data)
    if sys.version_info[:2] > high:
        return Check(
            "python",
            "warn",
            f"Python {version} is newer than the tested 3.10-3.13",
            "The bootstrap installs the framework on Python 3.12 through uv.",
            data=data,
        )
    return Check("python", "ok", f"Python {version} (supported: 3.10-3.13)", data=data)


def _platform_checks() -> list[Check]:
    system = platform.system()
    machine = platform.machine().lower()
    checks: list[Check] = []
    macos = _macos_version()
    data: dict[str, Any] = {"os": system, "arch": machine}
    if macos:
        data["macos"] = ".".join(str(part) for part in macos)
    if system == "Darwin" and machine in {"arm64", "aarch64"}:
        if macos and macos[0] >= MIN_MACOS_MAJOR:
            checks.append(
                Check(
                    "profile:apple",
                    "ok",
                    f"Apple Silicon on macOS {data.get('macos')}: the apple profile is supported",
                    data=data,
                )
            )
        else:
            checks.append(
                Check(
                    "profile:apple",
                    "warn",
                    f"The apple profile needs macOS {MIN_MACOS_MAJOR}+ (this is {data.get('macos')})",
                    "Use the light profile, or upgrade macOS for local MLX engines.",
                    data=data,
                )
            )
    elif system == "Darwin":
        checks.append(
            Check("profile:apple", "info", "Intel Mac: use the light profile", data=data)
        )
    else:
        checks.append(
            Check("profile:apple", "info", "The apple profile is for Apple Silicon Macs", data=data)
        )

    gpu_tool = None
    for tool in ("nvidia-smi", "rocminfo"):
        if shutil.which(tool):
            gpu_tool = tool
            break
    if gpu_tool:
        checks.append(
            Check("profile:gpu", "ok", f"{gpu_tool} is available", data={"tool": gpu_tool})
        )
    else:
        checks.append(
            Check(
                "profile:gpu",
                "info",
                "No nvidia-smi or rocminfo; the gpu profile would run local engines on CPU",
            )
        )
    return checks


def _tool_checks() -> list[Check]:
    checks: list[Check] = []
    uv = _find_executable("uv")
    if uv:
        version = _command_version(uv)
        checks.append(Check("uv", "ok", f"uv is available: {version}", data={"path": uv}))
    else:
        checks.append(
            Check(
                "uv",
                "warn",
                "uv is not installed",
                "The bootstrap (scripts/install.sh, install.ps1) installs it; see docs/install.md.",
            )
        )

    node = _find_executable("node")
    node_version = _command_version(node) if node else None
    major = _parse_major(node_version)
    npm = _find_executable("npm")
    node_data: dict[str, Any] = {"path": node, "version": node_version, "npm": npm}
    if node and major is not None and major >= MIN_NODE_MAJOR:
        source = "nodejs-wheel (uv tool)" if str(_uv_tool_bin_dir()) in node else "system"
        node_data["source"] = source
        checks.append(Check("node", "ok", f"Node {node_version} ({source})", data=node_data))
    elif node:
        checks.append(
            Check(
                "node",
                "warn",
                f"Node {node_version} is older than {MIN_NODE_MAJOR}; the browser apps need 18+",
                "Install a newer Node, or run: uv tool install nodejs-wheel",
                data=node_data,
            )
        )
    else:
        checks.append(
            Check(
                "node",
                "warn",
                "Node is not available; the browser apps (npx) need Node 18+",
                "No admin needed: uv tool install nodejs-wheel (or install.sh --with-apps)",
            )
        )
    return checks


def _disk_check() -> Check:
    target = Path.home()
    try:
        usage = shutil.disk_usage(target)
    except OSError as exc:
        return Check("disk", "warn", f"Could not read free disk space for {target}: {exc}")
    free_gb = usage.free / 1024**3
    data = {"path": str(target), "free_bytes": usage.free}
    if usage.free < DISK_ERROR_BYTES:
        return Check("disk", "error", f"Only {free_gb:.1f} GB free under {target}", data=data)
    if usage.free < DISK_WARN_BYTES:
        return Check(
            "disk",
            "warn",
            f"{free_gb:.1f} GB free under {target}; local models need several GB each",
            data=data,
        )
    return Check("disk", "ok", f"{free_gb:.1f} GB free under {target}", data=data)


def _gateway_checks(timeout: float) -> list[Check]:
    """Read-only probes of the gateway (GET /api/health) and its local config."""

    checks: list[Check] = []
    url = _gateway_url()
    status, body, error = _http_get_json(f"{url}/api/health", timeout)
    data: dict[str, Any] = {"url": url}
    if status == 200 and isinstance(body, dict) and body.get("service") == "abstractgateway":
        data["health"] = body.get("status")
        checks.append(Check("gateway", "ok", f"Gateway reachable at {url} ({body.get('status')})",
                            f"Console: {url}/console", data=data))
    elif status is not None:
        data["http_status"] = status
        checks.append(Check("gateway", "warn", f"{url}/api/health answered {status}, not a gateway",
                            data=data))
    else:
        data["error"] = error
        checks.append(
            Check(
                "gateway",
                "warn",
                f"No gateway at {url}",
                "Start it with `abstractgateway serve --host 127.0.0.1 --port 8080` or "
                "re-run the bootstrap; set ABSTRACTGATEWAY_URL for another address.",
                data=data,
            )
        )

    config_cli = _find_executable("abstractgateway-config")
    if not config_cli:
        checks.append(Check("gateway:config", "info", "abstractgateway-config is not on PATH"))
        return checks
    try:
        result = subprocess.run(
            [config_cli, "status", "--json"],
            check=False,
            capture_output=True,
            text=True,
            timeout=max(timeout, 30.0),
        )
        payload = json.loads(result.stdout) if result.stdout.strip() else {}
    except Exception as exc:
        checks.append(Check("gateway:config", "warn", f"abstractgateway-config status failed: {exc}"))
        return checks
    gateway = payload.get("gateway") if isinstance(payload, dict) else None
    gateway = gateway if isinstance(gateway, dict) else {}
    summary = {
        "data_dir": gateway.get("data_dir"),
        "auth_configured": gateway.get("auth_configured"),
        "auth_mode": gateway.get("auth_mode"),
        "store_backend": gateway.get("store_backend"),
        "service": payload.get("service") if isinstance(payload, dict) else None,
    }
    if result.returncode != 0 or not gateway:
        checks.append(
            Check("gateway:config", "warn", "abstractgateway-config status --json gave no gateway section",
                  data={k: v for k, v in summary.items() if v is not None})
        )
    else:
        checks.append(
            Check(
                "gateway:config",
                "ok",
                f"Gateway data dir: {summary['data_dir']}",
                data={k: v for k, v in summary.items() if v is not None},
            )
        )
    return checks


def _engine_checks(timeout: float) -> list[Check]:
    checks: list[Check] = []
    ollama = _ollama_url()
    status, body, error = _http_get_json(f"{ollama}/api/version", timeout)
    if status == 200:
        version = body.get("version") if isinstance(body, dict) else None
        label = f"Ollama {version}" if version else "Ollama"
        checks.append(Check("engine:ollama", "ok", f"{label} reachable at {ollama}",
                            data={"url": ollama, "version": version}))
    else:
        checks.append(Check("engine:ollama", "info", f"Ollama not reachable at {ollama} (optional)",
                            data={"url": ollama, "error": error or f"HTTP {status}"}))
    lmstudio = _lmstudio_url()
    status, body, error = _http_get_json(f"{lmstudio}/models", timeout)
    if status == 200:
        models = body.get("data") if isinstance(body, dict) else None
        count = len(models) if isinstance(models, list) else None
        checks.append(Check("engine:lmstudio", "ok", f"LM Studio reachable at {lmstudio}",
                            data={"url": lmstudio, "models": count}))
    else:
        checks.append(Check("engine:lmstudio", "info", f"LM Studio not reachable at {lmstudio} (optional)",
                            data={"url": lmstudio, "error": error or f"HTTP {status}"}))
    return checks


def build_doctor_report(
    include_environment: bool = True,
    include_network: bool | None = None,
    timeout: float = 2.0,
) -> dict[str, object]:
    """Return a doctor report without importing heavy local inference stacks.

    Network probes are read-only GETs (gateway ``/api/health``, Ollama ``/api/version``,
    LM Studio ``/v1/models``); they run with the environment checks unless disabled.
    """

    if include_network is None:
        include_network = include_environment
    checks: list[Check] = [_python_check()]

    installed_framework = _distribution_version("abstractframework")
    if installed_framework in {None, __version__}:
        status = "ok" if installed_framework == __version__ else "warn"
        message = (
            f"abstractframework {installed_framework} matches release profile"
            if installed_framework
            else "abstractframework distribution metadata is not installed"
        )
        checks.append(Check("abstractframework", status, message))
    else:
        checks.append(
            Check(
                "abstractframework",
                "error",
                f"abstractframework {installed_framework} does not match {__version__}",
            )
        )

    for package_id, expected in RELEASE_VERSIONS.items():
        distribution = PACKAGE_DISTRIBUTIONS[package_id]
        actual = _distribution_version(distribution)
        if actual is None:
            checks.append(
                Check(
                    f"package:{package_id}",
                    "error",
                    f"{distribution} is not installed",
                    f"Expected {distribution}=={expected}",
                )
            )
        elif actual == expected:
            checks.append(Check(f"package:{package_id}", "ok", f"{distribution}=={actual}"))
        else:
            checks.append(
                Check(
                    f"package:{package_id}",
                    "error",
                    f"{distribution}=={actual} does not match pinned {expected}",
                )
            )

    if include_environment:
        checks.extend(_platform_checks())
        checks.extend(_tool_checks())
        checks.append(_disk_check())
    if include_network:
        checks.extend(_gateway_checks(timeout))
        checks.extend(_engine_checks(timeout))

    worst = max((STATUS_RANK[check.status] for check in checks), default=0)
    status = "error" if worst == 2 else "warn" if worst == 1 else "ok"
    return {
        "schema": "abstractframework_doctor_v2",
        "abstractframework": __version__,
        "status": status,
        "platform": {
            "os": platform.system(),
            "arch": platform.machine(),
            "python": ".".join(str(part) for part in sys.version_info[:3]),
        },
        "checks": [check.as_dict() for check in checks],
    }


def _print_doctor(report: dict[str, object]) -> None:
    print(f"AbstractFramework doctor ({report['status']})")
    print("=" * 40)
    for raw in report["checks"]:  # type: ignore[index]
        check = raw  # type: ignore[assignment]
        marker = {"ok": "OK", "warn": "WARN", "error": "ERROR", "info": "INFO"}[check["status"]]
        print(f"[{marker}] {check['message']}")
        if check.get("detail"):
            print(f"       {check['detail']}")


def _doctor(args: argparse.Namespace) -> int:
    report = build_doctor_report(
        include_environment=not args.no_environment,
        include_network=not (args.no_environment or args.no_network),
        timeout=args.timeout,
    )
    if args.json:
        print(json.dumps(report, indent=2, sort_keys=True))
    else:
        _print_doctor(report)
    return 1 if report["status"] == "error" else 0


def _manifest(args: argparse.Namespace) -> int:
    if args.write:
        write_install_manifest(args.write)
        print(f"Wrote {args.write}")
        return 0
    if args.check:
        ok, message = check_install_manifest(args.check)
        print(message)
        return 0 if ok else 1
    print(manifest_json(), end="")
    return 0


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="abstractframework")
    subparsers = parser.add_subparsers(dest="command")

    doctor = subparsers.add_parser("doctor", help="Check install health and profile consistency")
    doctor.add_argument("--json", action="store_true", help="Emit machine-readable JSON")
    doctor.add_argument(
        "--no-environment",
        action="store_true",
        help="Skip host, tool and network probes; only check the Python package profile",
    )
    doctor.add_argument(
        "--no-network",
        action="store_true",
        help="Skip the read-only HTTP probes (gateway /api/health, Ollama, LM Studio)",
    )
    doctor.add_argument(
        "--timeout",
        type=float,
        default=2.0,
        help="Seconds per HTTP probe (default: 2)",
    )
    doctor.set_defaults(func=_doctor)

    manifest = subparsers.add_parser("manifest", help="Print or validate the install manifest")
    manifest.add_argument("--write", type=Path, help="Write the generated manifest to a path")
    manifest.add_argument("--check", type=Path, help="Check a manifest file against the generator")
    manifest.set_defaults(func=_manifest)

    args = parser.parse_args(argv)
    if not hasattr(args, "func"):
        parser.print_help()
        return 0
    return args.func(args)


if __name__ == "__main__":  # pragma: no cover
    raise SystemExit(main())

--- docs/README.md ---
# AbstractFramework documentation

**Write once. Generate everything.**

A modular, open-source ecosystem for building **durable, observable, multimodal** AI systems. Text, voice, image, video, music — one unified interface, any provider, any model, local or cloud.

This doc set focuses on two things:

1. **How to pick the right entry point** (AbstractCore SDK vs AbstractGateway control plane)
2. **How the pieces compose** (clients → Gateway → Agent / Runtime → Core → providers)

Most implementation lives in component repositories. This repo ships the `abstractframework` meta-package (a pinned install profile) and the cross-package docs you're reading now.

---

## Start here

### Choose your entry point

Start lightweight with just the LLM library, or go all-in with a production gateway. Both paths lead to the same ecosystem.

### AbstractCore (SDK + optional `/v1`)

Start with **AbstractCore**:

- 9+ providers with identical API (local + cloud)
- Universal tool calling, structured output, streaming
- Media handling (images, PDFs, audio, video)
- OpenAI-compatible HTTP server mode (`/v1`)
- Multimodal via capability plugins (Voice, Vision, Music)

Read **[Getting Started](getting-started.md)** → "Core-first" section.

### AbstractGateway (durable control plane)

Start with **AbstractGateway** + **AbstractFlow**:

- Durable execution that survives crashes and restarts
- Append-only ledger (replay-first) for auditability
- Scheduled workflows (cron-style, recurring)
- Multi-client: terminal, browser, tray, Telegram, email
- Start on one device, continue on another

Read **[Getting Started](getting-started.md)** → "Gateway-first" section.

---

## How the pieces fit (one picture)

```mermaid
flowchart LR
    CL["Clients<br/>Observer · Flow Editor · Code · Entity<br/>Continuum · Assistant · consoles"]
    GW["AbstractGateway<br/>runs · schedules · catalog · ledger"]
    AG["AbstractAgent"]
    RT["AbstractRuntime<br/>durable kernel"]
    CO["AbstractCore<br/>LLM · tools · media"]
    PL["voice · vision · music<br/>plugins"]
    PR[("providers and<br/>local engines")]
    CL -->|HTTP/SSE| GW
    GW --> AG --> RT
    GW --> RT --> CO
    AG --> CO
    CO -.-> PL
    CO --> PR
```

The full component diagram, with memory, semantics and the consoles, is in
[Architecture](architecture.md#component-view).

---

## Doc map

| Page | What it covers |
|---|---|
| **[Install](install.md)** | Mac installer, one-line install (`install.sh` / `install.ps1`), options, Network setting and uninstall; Light / Apple / GPU chooser, `abstractframework doctor`, installer manifest contract |
| **[Getting Started](getting-started.md)** | The two entry points + first end-to-end run |
| **[Architecture](architecture.md)** | Component diagram, distribution by registry, durable execution primitives, comparisons |
| **[Configuration](configuration.md)** | Minimal config, where defaults live, Core vs Gateway |
| **[Workspace scripts](workspace-scripts.md)** | Working from source: package inventory and tiers, `build.sh`, `status.sh`, `pull.sh`, `commit.sh`, `push.sh`, launchers |
| **[Glossary](glossary.md)** | Shared terminology (run, ledger, effect, wait, bundle, …) |
| **[ADR index](adr/README.md)** | Cross-package architectural decisions and accepted platform contracts |
| **[FAQ](faq.md)** | Common questions, comparisons, limits |
| **[Troubleshooting](troubleshooting.md)** | Symptoms, checks and fixes: installer blocked by macOS, sign-in links, ports, network access, providers |
| **[API](api.md)** | The `abstractframework` meta-package API (pins, helpers, re-exports, `doctor`, `manifest`) |
| **[Runtime artifacts and retrieval](guide/runtime-artifacts.md)** | Runtime, Gateway, Observer, ledger, and KG responsibility map for artifact/retrieval work |
| **[Shipped workflows](https://github.com/lpalbou/AbstractGateway/blob/main/docs/shipped-workflows.md)** | The workflows a packaged Gateway serves out of the box — coder, deep research, co-scientist — and how to run them |

---

## Package map by layer

The root [README](../README.md) is the fuller package catalog. This shorter map
keeps the docs hub cross-linked to the package owners' entrypoints.

### Foundation

| Package | What it is |
|---|---|
| [abstractcore](https://github.com/lpalbou/AbstractCore) | Unified LLM interface: providers, tools, structured output, media, embeddings, `/v1` server, capability plugins |
| [abstractsemantics](https://github.com/lpalbou/AbstractSemantics) | Shared semantics registry for predicates and entity types |
| [abstractmemory](https://github.com/lpalbou/AbstractMemory) | Durable, append-only agent memory: usage-weighted graph + journal — recall, formation, consolidation (the entity mind engine) |

### Durable execution

| Package | What it is |
|---|---|
| [abstractruntime](https://github.com/lpalbou/AbstractRuntime) | Durable execution kernel: runs, effects, waits, append-only ledger, artifacts, and the entity identity lane |
| [abstractagent](https://github.com/lpalbou/AbstractAgent) | ReAct, CodeAct, and MemAct patterns on top of Runtime + Core |
| [abstractflow](https://github.com/lpalbou/AbstractFlow) | Visual workflow editor and portable `.flow` bundles |

### Control plane

| Package | What it is |
|---|---|
| [abstractgateway](https://github.com/lpalbou/AbstractGateway) | Deployable control plane: durable runs over HTTP/SSE, scheduling, workflow catalog, auth, artifact/ledger serving, and the summoned-entity door |

### Multimodal capabilities

| Package | What it is |
|---|---|
| [abstractvoice](https://github.com/lpalbou/AbstractVoice) | Voice I/O (TTS / STT), local and remote backends |
| [abstractvision](https://github.com/lpalbou/AbstractVision) | Model-agnostic image generation |
| [abstractmusic](https://github.com/lpalbou/AbstractMusic) | Text-to-music / text-to-audio capability plugin |
| [abstract3d](https://github.com/lpalbou/abstract3d) | Local-first 3D generation |
| [abstractcamera](https://github.com/lpalbou/AbstractCamera) | Camera control and capture tools |

### Apps and clients

| Package | What it is |
|---|---|
| [abstractcode](https://github.com/lpalbou/AbstractCode) | Coding client with durable sessions and tool approvals: Rust terminal client (`cargo install abstractcode`) and browser client (`npx @abstractframework/code`) |
| [abstractassistant](https://github.com/lpalbou/AbstractAssistant) | macOS tray client for gateway-native chat and voice |
| [abstractobserver](https://github.com/lpalbou/AbstractObserver) | Browser UI for monitoring, control, and scheduling |
| [abstractentity](https://github.com/lpalbou/AbstractEntity) | Summoned-entity manager and chat/replay UI |
| [abstractcontinuum](https://github.com/lpalbou/AbstractContinuum) | Continuous iterative development and deployment console |
| Consoles | Web consoles built into `abstractgateway serve` and `abstractcore serve` (`/console`, with Models and Engines tabs); terminal consoles `cargo install abstractgateway-console` and `cargo install abstractcore-console` |

### Shared libraries

| Package | What it is |
|---|---|
| [abstracttui](https://github.com/lpalbou/AbstractTUI) | Reactive Rust terminal UI engine |
| [abstractuic](https://github.com/lpalbou/AbstractUIC) | Shared React/Web Components UI kit |
| [abstractskill](https://github.com/lpalbou/AbstractSkill) | Shared Agent Skills (`SKILL.md`) loader and activation library |

---

## Example apps

| App | What it does |
|---|---|
| **AbstractCode** | Terminal agentic dev client (local, durable sessions) |
| **AbstractAssistant** | macOS tray client (gateway-first, workflow picker, voice) |
| **AbstractObserver** | Browser UI to monitor, control, and schedule gateway runs |
| **Code Web UI** | Browser coding assistant (gateway-backed) |

---

## More docs

| Folder | What's inside |
|---|---|
| [docs/guide/](guide/) | Focused "how it works" notes |
| [docs/scenarios/](scenarios/) | End-to-end walkthroughs by use case |
| [docs/installers/](installers/README.md) | Install design: script bootstrap + gateway console, per-OS journeys, OS security, manifest, operations |
| [docs/comparisons/](comparisons/) | Trade-offs vs other frameworks |
| [docs/adr/](adr/README.md) | Architecture decision records |
| [CHANGELOG.md](../CHANGELOG.md) | Release history of the meta-package and its pins |
| [CONTRIBUTING.md](../CONTRIBUTING.md) · [SECURITY.md](../SECURITY.md) | Contributing to this repository; reporting a vulnerability |

--- docs/getting-started.md ---
# Getting started

This guide helps you build a correct mental model quickly, then run something end-to-end.

> **Write once. Generate everything.** Durable, observable, multimodal AI systems — one unified interface, any provider, any model, local or cloud.

AbstractFramework is a **stack**:

| Layer | Package | Role |
|---|---|---|
| SDK | **AbstractCore** | Provider/model abstraction, tools, structured output, media, embeddings |
| Agent patterns | **AbstractAgent** | Ready-made loops: ReAct (tool-first), CodeAct (code execution), MemAct (memory-enhanced) |
| Workflow authoring | **AbstractFlow** | Visual editor, portable `.flow` bundles, subflows |
| Durable kernel | **AbstractRuntime** | Runs, effects, waits, ledger, artifacts |
| Control plane | **AbstractGateway** | Persistence, scheduling, bundle discovery, SSE streaming |
| Operations | **AbstractObserver** | Browser UI to monitor, control, and schedule runs |

**Rule of thumb**: start with **Core** when you want a lightweight LLM library (SDK or `/v1`) for scripts/notebooks/apps; add **Gateway** when you need persistent runs, scheduling, and multi-client continuity.

> **Prerequisites**: Python 3.10–3.13 for the manual installs below. Node.js 18+ (only for browser UIs). An LLM backend — local (Ollama, LM Studio, vLLM, llama.cpp) or cloud (OpenAI, Anthropic, etc.).

## Fastest path: the installer

If you want AbstractFramework running on your computer without setting up Python yourself, use
the installer. On a Mac, download and double-click
[AbstractFramework-Installer.pkg](https://github.com/lpalbou/AbstractFramework/releases/latest/download/AbstractFramework-Installer.pkg)
(the first time, allow it with **Open Anyway** in **System Settings > Privacy & Security**: the
package is not signed with an Apple Developer ID; see [Install](install.md#install-on-a-mac)).
On macOS or Linux you can instead paste one line in Terminal:

```bash
curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh -s -- --interactive
```

On Windows: `powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"`.

It installs uv, Python 3.12 and the pinned gateway in your user account (no admin password), asks
whether to start it at login, starts it on `127.0.0.1:8080`, and opens its console in your browser
already signed in. The console's first-run guide sets up a local engine or a cloud key and a
default model (the **Models** tab lists the models that fit your machine and downloads them). Then
continue with [Monitor runs](#4-monitor-runs-with-abstractobserver) or
[AbstractFlow](#author-orchestration-with-abstractflow). Step by step, what to do when something
fails, and how to remove it: [Install](install.md); fixes for common problems:
[Troubleshooting](troubleshooting.md). The sections below cover manual installs for library and
developer use.

---

## Choose your entry point

| If you are… | Start with | Why |
|---|---|---|
| Calling LLMs/tools/media (SDK or OpenAI-compatible `/v1`) | **AbstractCore** | Lightweight, smallest surface area, fastest feedback loop |
| Building persistent agents/workflows (durable runs) | **AbstractGateway** + **AbstractFlow** | Durability, scheduling, bundle discovery, ledger replay/streaming |

You can also install the entire pinned ecosystem in one command:

```bash
pip install abstractframework
```

For the full Light / Apple / GPU profile chooser, see [Install AbstractFramework](install.md).

---

## Core-first: integrate via AbstractCore (SDK or `/v1`)

### 1. Install

```bash
pip install abstractcore
```

### 2. Configure a provider

**Local (Ollama)** — free, no API key:

```bash
ollama serve
ollama pull qwen3:4b-instruct
export OLLAMA_HOST="http://localhost:11434"
```

**OpenAI-compatible** (LM Studio, vLLM, LocalAI, llama.cpp):

```bash
export OPENAI_BASE_URL="http://127.0.0.1:1234/v1"
export OPENAI_API_KEY="local"
```

**Cloud APIs**:

```bash
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```

**Or use a console.** `abstractcore serve` starts the server on `127.0.0.1:8000` and prints a
one-time link to its web console, where the **Engines** tab detects and installs local engines
(Ollama, LM Studio, MLX, llama.cpp) and the **Models** tab downloads a model that fits this machine.
The same actions exist on the command line and in the terminal console:

```bash
abstractcore serve                      # open the printed http://127.0.0.1:8000/console#claim=… link
abstractcore engines status             # which local engines are installed and running
abstractcore models catalog             # models, with a fit verdict for this machine
abstractcore models download ollama qwen3:4b-instruct
cargo install abstractcore-console      # terminal console (Rust 1.87+)
```

The interactive terminal wizard persists config to `~/.abstractcore/config/`:

```bash
abstractcore --config
abstractcore --status
```

### 3. Call the model

```python
from abstractcore import create_llm

llm = create_llm("ollama", model="qwen3:4b-instruct")
resp = llm.generate("Explain durable execution in 3 bullets.")
print(resp.content)
```

### What else you can do with Core

```python
# Tool calling
resp = llm.generate("What's the weather?", tools=[get_weather])

# Structured output (Pydantic)
report = llm.generate("Analyze this.", response_model=Report)

# Media input (images, audio, video, documents)
resp = llm.generate("Describe this image.", media=["photo.jpg"])

# Embeddings
vectors = llm.embed(["first document", "second document"])

# Streaming
for chunk in llm.generate("Write a poem.", stream=True):
    print(chunk.content or "", end="", flush=True)
```

---

## Gateway-first: durable runs + monitoring + scheduling

### 1. Install

```bash
pip install abstractgateway
```

### 2. Configure (optional)

With no configuration, `abstractgateway serve` binds `127.0.0.1:8080`, enables user auth, and keeps
its data in the per-user data folder (macOS `~/Library/Application Support/AbstractGateway`, Linux
`~/.local/share/abstractgateway`, Windows `%LOCALAPPDATA%\AbstractGateway`). Set the environment
when you want another data folder, browser origins or bundle registry:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
export ABSTRACTGATEWAY_WORKFLOW_SOURCE=bundle
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"

# Optional: set only for a custom bundle registry. When this is unset,
# Gateway serves its shipped workflows (basic-agent, coding-agent,
# deep-research, co-scientist, and more).
# export ABSTRACTGATEWAY_FLOWS_DIR="$PWD/bundles"
```

### 3. Start the gateway

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

On first local start, Gateway creates `default/admin`, keeps its token in
`<data dir>/auth/bootstrap-admin-token` (readable by you only), and prints a one-time link:
`First run: open http://127.0.0.1:8080/console#claim=…`. Open it to sign in to the web console and
its first-run guide (engines, default model, apps); `abstractgateway claim --open` mints a new
link. Use the `admin` token from the file to sign in to AbstractFlow, AbstractCode Web or
AbstractObserver. `ABSTRACTGATEWAY_AUTH_TOKEN` is only the legacy server/operator bearer-token
path; it does not sign in browsers.

To start the gateway at login, use `abstractgateway service install --port 8080`. The login item
listens where the gateway's Network setting says (this computer only until you change it; see
[Network setting](install.md#network-setting-who-can-reach-the-gateway)).

Verify:

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

### 4. Monitor runs with AbstractObserver

In another terminal:

```bash
npx @abstractframework/observer
```

Open http://localhost:3001 and connect. In hosted user-auth mode, enter Gateway
URL, Gateway user, and that user's token; Observer exchanges the token for a
browser session and does not persist the token in browser settings.

AbstractObserver is replay-first: it renders runs by replaying the ledger, then streams new steps live via SSE.

### 5. Schedule recurring work

Schedules are owned by the gateway (they survive restarts):

```bash
curl -X POST "http://127.0.0.1:8080/api/gateway/runs/schedule" \
  -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  -H "Content-Type: application/json" \
  -d '{"bundle_id":"my-bundle","flow_id":"my-entrypoint","start_at":"now","interval":"24h"}'
```

---

## Author orchestration with AbstractFlow

The ecosystem's distribution unit is a **workflow bundle** (`.flow` file): a VisualFlow graph + metadata. Gateways discover bundles and expose them to all clients.

You do not have to start from an empty registry. A packaged Gateway already
serves a set of ready workflows — a verify-gated coding agent, `deep-research`,
and `co-scientist` among them — listed in
[AbstractGateway's shipped workflows](https://github.com/lpalbou/AbstractGateway/blob/main/docs/shipped-workflows.md).
Author your own when you need something they do not cover.

### 1. Open the Flow Editor

With the gateway running:

```bash
npx @abstractframework/flow
```

Open http://localhost:3003 and connect to your gateway. In hosted user-auth
mode, use Gateway URL, Gateway user, and that user's token; Flow keeps an
opaque browser session instead of storing the token.

### 2. Build a workflow

- **On Flow Start** → takes input (prompt, provider, model, …)
- LLM steps, tool steps, branching, loops, subflows
- **On Flow End** → returns output (response, success, metadata)

To make it reusable across clients, implement an **interface contract** (for example `abstractcode.agent.v1` — a standard chat-like agent I/O contract).

### 3. Export and deploy

```bash
mkdir -p "$PWD/bundles"
cp my-agent.flow "$PWD/bundles/"
```

Start or restart Gateway with `ABSTRACTGATEWAY_FLOWS_DIR="$PWD/bundles"` when
you want that directory to be the active custom bundle registry. You can also
publish bundles through the Gateway API from AbstractFlow.

### 4. Run from any client

Once deployed, the bundle appears in:

- **AbstractObserver** — workflow picker / run launcher
- **AbstractAssistant** — workflow picker (per session)
- **Code Web UI** — workflow picker
- **Your own client** — via the gateway bundle discovery API

---

## Example apps

### AbstractCode (terminal and browser)

A coding client for durable agentic sessions on the gateway you started above. Install the Rust
terminal client from crates.io (or download a prebuilt binary from the
[AbstractCode GitHub release](https://github.com/lpalbou/AbstractCode/releases)), or run the
browser client with `npx`:

```bash
cargo install abstractcode
abstractcode doctor              # check the gateway connection
abstractcode

npx @abstractframework/code      # browser client on http://127.0.0.1:3002
```

Sessions are durable: close and reopen, your full context is preserved. Type `/help` for commands.

### AbstractAssistant (macOS tray)

Gateway-first by default. Select a workflow per session from the tray UI:

```bash
pip install abstractassistant
assistant tray
```

---

## Next steps

- **[Architecture](architecture.md)** — the layered model (Core / Runtime / Agent / Gateway / Flow / Observer)
- **[Configuration](configuration.md)** — where defaults live and how to configure them
- **[Glossary](glossary.md)** — shared terms (run, ledger, effect, wait, bundle, interface contract)
- **[API](api.md)** — the `abstractframework` helpers, `doctor` and `manifest` commands
- **[FAQ](faq.md)** — comparisons, offline operation, limits
- **[Troubleshooting](troubleshooting.md)** — symptoms and fixes
- **[Workspace scripts](workspace-scripts.md)** — work from source: clone, build and sync every package in dependency order

--- docs/architecture.md ---
# Architecture

AbstractFramework is open-source AI infrastructure built around one idea: **durable, observable execution** for AI workflows.

Most LLM frameworks optimize for prototyping speed. AbstractFramework optimizes for **operational reality**: workflows that pause/resume safely, runs that survive restarts, UIs that reconstruct state from history, and clear boundaries around tool execution and approvals.

---

## Choose your entry point

Start lightweight with just the LLM library, or go all-in with a production gateway. Both paths lead to the same ecosystem.

### AbstractCore (SDK + optional `/v1`)

Start here if you need a lightweight LLM library for scripts, notebooks, or existing applications. No infrastructure required — just install and call. Add multimodal capabilities with plugins as you grow.

- 9+ providers with identical API (local + cloud)
- Universal tool calling, structured output, streaming
- Media handling (images, PDFs, audio, video)
- OpenAI-compatible HTTP server mode (`/v1`), with a web console at `/console`
- Local model and engine management: browse models that fit this machine, download, delete, install engines
- Multimodal via capability plugins (Voice, Vision, Music)

The right first step when you mainly care about calling models/tools/media (in-process via Python or via `/v1`) and want the smallest surface area.

### AbstractGateway (durable control plane)

Start here if you're building persistent AI applications — agents that run for hours, workflows that survive crashes, scheduled tasks. The gateway is your AI control plane: durable runs with ledger replay/streaming and thin clients that can attach/detach across devices.

- Durable execution that survives crashes and restarts
- Append-only ledger (replay-first) for auditability
- Scheduled workflows (cron-style, recurring)
- Multi-client: terminal, browser, tray, Telegram, email
- Start on one device, continue on another

The composition root when you need a control plane (local or remote).

---

## Component view

Every client talks to the gateway over HTTP/SSE. The gateway composes the Python packages below it
in one process: agent patterns, the durable runtime, memory and AbstractCore, which reaches the
model providers and local engines. Arrows point from a component to what it calls or depends on.

```mermaid
flowchart TB
    subgraph CLIENTS["Apps and clients"]
        OBS["AbstractObserver<br/>monitor · control · schedule"]
        FLOWED["Flow Editor<br/>author .flow bundles"]
        CODE["AbstractCode<br/>terminal client + Code Web UI"]
        ENT["AbstractEntity<br/>summoned entities"]
        CONT["AbstractContinuum<br/>development console"]
        ASSIST["AbstractAssistant<br/>desktop menu-bar app"]
        GCON["abstractgateway-console<br/>terminal operator console"]
        APP["Your app"]
    end

    subgraph GATEWAY["AbstractGateway (control plane)"]
        API["HTTP/SSE API<br/>runs · schedules · workflow catalog<br/>ledger + artifacts · users · network"]
        WEB["web /console<br/>first-run guide · Models · Engines"]
        TRAY["menu-bar icon<br/>status · Network"]
    end

    AGENT["AbstractAgent<br/>ReAct · CodeAct · MemAct"]
    RT["AbstractRuntime<br/>runs · effects · waits · ledger · artifacts<br/>VisualFlow compiler"]
    MEM["AbstractMemory<br/>durable agent memory"]
    SEM["AbstractSemantics<br/>predicates + entity types"]
    CORE["AbstractCore<br/>providers · tools · media · embeddings<br/>models + engines"]
    PLUG["Capability plugins<br/>abstractvoice · abstractvision · abstractmusic"]
    CSRV["abstractcore serve<br/>/v1 · /acore · web /console"]
    CCON["abstractcore-console<br/>terminal console + shared screens"]
    PROV[("LLM providers and local engines<br/>Ollama · LM Studio · MLX · llama.cpp · vLLM · cloud APIs")]

    CLIENTS -->|HTTP/SSE| API
    WEB --> API
    TRAY --> API
    GCON -->|embeds Models/Engines screens| CCON
    API --> AGENT
    API --> RT
    API --> MEM
    AGENT --> RT
    AGENT --> CORE
    RT --> CORE
    RT --> MEM
    RT --> SEM
    CORE -.->|entry-point plugins| PLUG
    CORE --> PROV
    CSRV --> CORE
    CCON -->|abstractcore CLI| CORE
```

The layers, from the top:

- **Apps and clients** are thin: they hold no durable state and rebuild their view by replaying
  the ledger, then follow new events over SSE. The Flow Editor publishes `.flow` bundles to the
  gateway; the others start, observe and steer runs.
- **AbstractGateway** owns the run lifecycle (start, resume, cancel), durable schedules, private
  bundle discovery and the shared workflow catalog, users and auth, the Network setting, and
  ledger/artifact serving. Its web console and menu-bar icon are part of the same package.
- **AbstractAgent** provides ready-made agent loops; **AbstractRuntime** is the durable kernel that
  executes them and compiles VisualFlow graphs from `.flow` bundles into workflows.
- **AbstractCore** is the LLM layer: provider and model abstraction, capability routing defaults,
  tools, structured output, media, embeddings, MCP, and the local model and engine management
  (catalog with a fit verdict, downloads, engine installs). Voice, image and music arrive as
  capability plugins.
- **AbstractCore on its own** (`abstractcore serve`) is the second entry point: an
  OpenAI-compatible `/v1` server with its own console, without the durable layers.

## How the framework is distributed

Each layer ships through the registry that fits it. The `abstractframework` meta-package pins the
Python side with exact versions; the apps and terminal tools are installed next to it.

```mermaid
flowchart LR
    subgraph PyPI["PyPI (pinned by abstractframework)"]
        GW["abstractgateway<br/>server + /console"]
        AS["abstractassistant"]
        STACK["abstractcore · AbstractRuntime · abstractagent<br/>AbstractMemory · abstractsemantics<br/>abstractvoice · abstractvision · abstractmusic"]
    end
    subgraph npm["npm (npx @abstractframework/...)"]
        APPS["flow · code · observer<br/>continuum · entity"]
    end
    subgraph crates["crates.io (cargo install)"]
        CLI["abstractcode"]
        CON["abstractgateway-console"]
        CCON["abstractcore-console<br/>(app + Models/Engines screens library)"]
    end
    subgraph GHCR["GHCR images"]
        IMG["abstractgateway · abstractcore-server"]
    end
    BOOT["install.sh / install.ps1<br/>(uv tool install abstractgateway)"]
    BOOT -->|installs, starts, opens /console| GW
    AS -->|HTTP/SSE| GW
    APPS -->|HTTP/SSE| GW
    CLI -->|HTTP/SSE| GW
    CON -->|HTTP/SSE| GW
    CON -->|embeds screens| CCON
    CCON -->|abstractcore CLI| STACK
    GW --> STACK
    IMG -.->|same server, containerized| GW
```

The Models and Engines features are implemented once, in AbstractCore, and inherited by the
gateway:

```mermaid
flowchart TB
    CORE["AbstractCore<br/>host profile · catalog + fit · engines · jobs"]
    CORE --> CCLI["abstractcore models / engines (CLI)"]
    CORE --> CAPI["abstractcore serve<br/>/acore/* + web /console"]
    CORE --> CTUI["abstractcore-console<br/>screens 9 Models, 0 Engines"]
    CORE -->|via AbstractRuntime| GAPI["abstractgateway<br/>/api/gateway/models, engines, jobs, host/profile"]
    GAPI --> GWEB["gateway web /console<br/>embeds Core's Models and Engines screens"]
    GAPI --> GTUI["abstractgateway-console<br/>mounts the abstractcore-console screens over HTTP"]
    GAPI --> GCLI["abstractgateway models / engines (CLI)"]
```

Every console action shows its command-line equivalent. Download, delete and engine installs are
admin-only jobs; engine installs from a console are enabled by default only for a server bound to
loopback (`allow_engine_install` on the gateway, `ABSTRACTCORE_ALLOW_ENGINE_INSTALL` on the core
server).

See [Install AbstractFramework](install.md) for the versions released together and the commands
for each registry.

**AbstractFlow** is the authoring/distribution layer: you design a VisualFlow graph, export a `.flow` bundle, and run it anywhere a compatible host exists.

**AbstractAgent** is the composition layer: ready-made agent loops (ReAct, CodeAct, MemAct) built on top of Runtime. These can be used standalone or inside a Flow as agent nodes.

Portable workflow execution does not require every client to be a generic workflow picker. Some
products expose workflow selection, while others intentionally bind to one published workflow or
interface family for a specialized task. The invariant is that execution still happens as a durable
Gateway/Runtime workflow.

---

## Durable execution primitives

These are the "why" behind the design — the properties that make the framework operationally useful.

### Run

A durable workflow instance with persisted state. Identified by a `run_id`.

### Ledger

The append-only history of a run: every step, effect, result, wait, and error is recorded.

This is what makes replay-first UIs possible: a client reconstructs state by replaying history, then follows along by streaming new events over SSE.

For Core-backed multimodal generation, Runtime can also persist bounded `resolved_actions`
summaries in replay exports. Those records capture the normalized request/output summary and the
effective resolved route without turning route internals into the ordinary app-facing vocabulary.

### Effects and waits

Work happens at explicit boundaries:

- An **effect** is a request for a side-effect (LLM call, tool call, ask user, wait-until, …).
- A **wait** is a checkpointed pause until external input arrives (tool results, user answer, a timer).

The key property: if a process dies while waiting, **the run is still correct**. Another process (or a restart) can resume it from the recorded wait.

### Artifacts

Large payloads (files, media, big tool results) are stored as **artifacts** and referenced by handle from JSON state and the ledger. This keeps state JSON-safe without losing evidence.

### Tool execution boundary

Tool **schemas** are durable (stored in the ledger). Tool **callables** are not (they live in the host process). This is intentional:

- Tool calls become explicit approval points (configurable per tool)
- Execution is auditable (arguments + results in the ledger)
- Runs remain restart-safe (the tool call is a wait, not an in-process function call)

---

## Workflow lifecycle: author → deploy → run → observe

### 1) Author with AbstractFlow

Build a workflow graph in the Flow Editor and export it as a `.flow` bundle.

### 2) Deploy the bundle

Copy bundles to `ABSTRACTGATEWAY_FLOWS_DIR`. The gateway discovers them automatically.

For hosted or multi-user gateways, admins can also promote immutable bundle
versions into the Gateway workflow catalog. The catalog owns shared/default
workflow pointers and ACLs; each catalog run executes in the requesting user's
runtime.

### 3) Run from any client

Any gateway-backed client can: list bundles/entrypoints, start a run, attach to ledger replay/streaming, and resume waits (approvals, user input, tool results).

### 4) Observe and schedule with AbstractObserver

- Inspect any run (ledger replay)
- Watch a run live (SSE)
- Control runs (cancel, resume)
- Schedule recurring runs

## File-like sources in hosted clients

Hosted clients such as AbstractFlow need a clear split between file origin and
runtime value:

- `Artifact`: a saved Runtime-owned file payload. Reusable across runs.
- `Local File`: a file chosen from the client device. In hosted/browser mode it
  is uploaded and normalized into an Artifact before durable execution.
- `Server File`: a user-facing label for a file inside Gateway-approved
  workspace scope on the server. Path-based operations still depend on
  workspace policy and current grants; importing it creates an Artifact
  snapshot.

Internally, the durable payload handle is the artifact ref. Server-side path
access stays a Gateway-owned workspace capability rather than a generic server
filesystem abstraction. Some UI surfaces label server files `Workspace`; it
means the same Gateway-approved scope.

---

## Multimodality

AbstractCore stays lightweight by treating modalities as **capability plugins** (discovered internally via Python entry points, then exposed via the AbstractCore SDK and its optional `/v1` endpoints; gateway-first deployments can also surface them through Gateway):

| Plugin | Capability | API surface |
|---|---|---|
| `abstractvoice` | TTS + STT | `llm.voice.tts(...)`, `llm.audio.transcribe(...)` |
| `abstractvision` | Image generation | `llm.vision.t2i(...)` |
| `abstractmusic` | Music generation | `llm.music.t2m(...)` |

Install a plugin; the API appears on your `llm` instance. Don't install it; Core stays small.

---

## How it compares (honest positioning)

### vs direct provider SDKs (OpenAI, Anthropic)

Direct SDKs are the right choice when you only use one provider and don't need durable orchestration.

AbstractCore adds value when you need: provider portability, consistent tool/structured-output behavior across backends, media policies, modality plugins, or a stable configuration layer that doesn't leak into app code.

### vs LangChain / LlamaIndex / PydanticAI

Those are primarily **in-process orchestration libraries**. AbstractFramework occupies a different niche: an **agentic OS-style** stack for durable, observable execution (runtime + append-only ledger + control plane), where the same workflows can run across providers, devices, and deployment modes.

**Where AbstractFramework is stronger**: durability and pause/resume as primitives, replay-first observability, portable `.flow` bundles that run across clients.

**Where others are stronger**: large connector/RAG ecosystems, minimal boilerplate for simple use cases, broader community examples.

### vs Temporal / Step Functions / job schedulers

AbstractGateway is architecturally closer to these systems, but specialized for LLM/tool loops: tool approval waits, AI-oriented artifacts, and replay-first thin-client UIs over HTTP/SSE.

---

## Where to go deeper

- **[Getting Started](getting-started.md)** — run Core-first or Gateway-first
- **[Configuration](configuration.md)** — minimal config, where defaults live
- **[Glossary](glossary.md)** — shared definitions (run, ledger, effect, wait, bundle, interface contract)
- **[API](api.md)** — the meta-package helpers and the functional API owner of each concern
- **[Install](install.md)** — how each part reaches a machine, and the Network setting
- **[Installer design](installers/README.md)** — the bootstrap + console install model
- **[ADR index](adr/README.md)** — accepted cross-package decisions, including
  [package dependency boundaries](adr/0032-package-dependency-boundaries-and-gateway-first-apps.md),
  [install profiles](adr/0033-install-profiles-config-entrypoints-and-server-boundaries.md) and
  [the script bootstrap install](adr/0038-script-bootstrap-and-gateway-console-install.md)
- **[Runtime artifacts and retrieval](guide/runtime-artifacts.md)** — who owns artifacts, ledger and retrieval
- Per-project architecture docs live in the component repositories

--- docs/install.md ---
# Install AbstractFramework

Most people want AbstractFramework running on their own computer with nothing to set up by hand.
That is what the installer does: one download or one line, no admin password, and at the end
AbstractFramework opens in your web browser, already signed in. Developers who want the Python
libraries in their own environment go to [Install the Python framework](#install-the-python-framework-developers).

## Install on a Mac

You need a Mac with macOS 13 or later (Apple Silicon gets the fast local engines; they need
macOS 14), an internet connection, and about 5 GB of free disk space before models.

1. **Download** [AbstractFramework-Installer.pkg](https://github.com/lpalbou/AbstractFramework/releases/latest/download/AbstractFramework-Installer.pkg)
   (attached to every [GitHub release](https://github.com/lpalbou/AbstractFramework/releases)).
2. **Allow it once.** The package is not signed with an Apple Developer ID, so the first
   double-click shows a warning that macOS cannot verify the installer (*"… Not Opened"*, *"Apple
   could not verify …"* or *"… from an unidentified developer"*, depending on your macOS version).
   Close the warning (**Done** or **OK**), open **System Settings > Privacy & Security**, scroll
   down to **Security**, click **Open Anyway** next to the installer's name, and confirm (macOS may
   ask for your login password or Touch ID). This is the standard macOS step for software from
   outside the App Store; you do it once per download.
3. **Install.** The macOS Installer opens. Click **Continue**, then **Install**. It installs for you
   only, so the Installer does not ask for your password.
4. **A Terminal window opens** and shows each step as it happens. It asks one question:

   ```
   ? Start AbstractFramework automatically when you log in? (a per-user login item, no admin; the uninstaller removes it) [Y/n]
   ```

   Press **Return** for yes (recommended: it is then always there when you need it), or type `n`.
5. **Wait** 2 to 15 minutes, depending on your connection. The last lines say
   `AbstractFramework is ready.` and your browser opens AbstractFramework.
6. **In the browser**, the first-run guide helps you pick an engine (it detects what this Mac can
   run and installs it with one click) and a model that fits your Mac. You can close the Terminal
   window.

Afterwards, AbstractFramework is at `http://127.0.0.1:8080/console` (bookmark it), and its icon in
the menu bar opens it and shows its status.

What it puts on your Mac, all inside your home folder: uv and a private Python 3.12 in
`~/.local`, the AbstractFramework gateway (about 2.4 GB on Apple Silicon, with the MLX engines),
uv's download cache (about 2.4 GB, reused by upgrades), your data in
`~/Library/Application Support/AbstractGateway`, and, if you said yes, a login item
(`~/Library/LaunchAgents/ai.abstractframework.gateway.plist`). Models you download later come on
top.

The package also leaves two double-clickable files in
`~/Library/Application Support/AbstractFramework/Installer`: **Install AbstractFramework.command**
(run it again to repair or upgrade) and **Uninstall AbstractFramework.command**.

### Or: paste one line in Terminal

On macOS and Linux, open Terminal, paste this line and press Return:

```bash
curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh -s -- --interactive
```

`--interactive` makes it ask the start-at-login question; without it the answer is yes. Everything
else is the same as the Mac package above. On Linux the login item is a `systemd --user` service and
the data lives in `~/.local/share/abstractgateway`.

### Windows

Windows 10 22H2+ / 11: open PowerShell, paste this line and press Enter:

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"
```

It installs under your user account (no administrator rights), starts AbstractFramework at sign-in
and opens it in your browser. The Windows script registers the start-at-login entry without asking;
add `-NoService` to skip it.

## If something goes wrong

The installer stops at the first problem, says what happened in plain words, and says what to do.
It never leaves a half-finished install behind that a second run cannot fix: after fixing the
cause, run it again (double-click the installer again, or paste the line again) and it continues
where it stopped.

| What you see | What to do |
|---|---|
| `no internet connection: the installer could not reach pypi.org` | Connect to the internet, then run the installer again. Nothing was changed. |
| `cannot reach pypi.org through the proxy set in your environment (…)` | Your computer is set up to use a proxy that does not answer. Fix the proxy (or remove the `HTTPS_PROXY` setting), then run it again. |
| `… failed because the internet connection dropped` | Reconnect and run it again; it continues where it stopped. |
| `this Terminal runs in Intel (Rosetta) mode on an Apple Silicon Mac` | Quit Terminal. In Finder open **Applications > Utilities**, select **Terminal**, choose **File > Get Info**, untick **Open using Rosetta**, then run the installer again. (Double-clicking the installer restarts itself in the right mode on its own.) |
| `the folder … belongs to 'root', so the installer … cannot write there` | An earlier command was run with `sudo`. Run the `sudo chown -R …` line the message shows (it asks for your password once), then run the installer again. |
| `macOS … is older than the versions AbstractFramework is tested on` | A warning, not a stop. If the install then fails, update macOS in **System Settings > General > Software Update**. |
| `the Apple Silicon engines (MLX) need macOS 14 or later` | You get the light version (remote and endpoint engines). Update macOS and run the installer again to add the local engines. |
| `the installed gateway does not start … reinstalling it` | Nothing to do: an earlier install was interrupted and the installer repairs it. |
| `the gateway did not answer … within 180 s` | Restart the computer (the login item starts it) or run the installer again, then open `http://127.0.0.1:8080/console`. |
| The browser page asks for a token | The one-time sign-in link lasts 10 minutes. Run the installer again: it opens a fresh link. |
| Anything else | Run the installer again. If it stops at the same step, report it with the log file named at the end of the message (`~/Library/Application Support/AbstractGateway/logs/install-….log`). |

## Remove AbstractFramework

Double-click **Uninstall AbstractFramework.command** (in
`~/Library/Application Support/AbstractFramework/Installer` after a package install), or paste:

```bash
curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/uninstall.sh | sh
```

It asks before removing anything, then asks two more questions, both defaulting to no:

- **Also delete your AbstractFramework data** (settings, users, chats, run history)? This cannot be
  undone.
- **Also remove uv, its Python and its download cache** (about 2.5 GB)? Asked only when the
  installer is what added uv; answer no if you use uv for anything else.

It always removes the login item, stops AbstractFramework and removes the gateway. It keeps Ollama
and LM Studio (they have their own uninstallers) and the one PATH line uv added to your shell
profile. Without questions: `sh uninstall.sh --yes` (keeps data), add `--purge` to delete the data
and `--remove-uv` to remove uv. Windows: `install.ps1 -Uninstall [-Purge]`.

## Advanced: what the installer does

The Mac package, the `.command` files and the one line all run the same script,
[`scripts/install.sh`](../scripts/install.sh) (Windows: `scripts/install.ps1`):

1. Checks the machine (OS, CPU, macOS 14+ for Apple Silicon, Rosetta, NVIDIA/ROCm, the folders it
   writes, internet access to PyPI, free disk, a free port) and picks a profile: `apple` on Apple
   Silicon (macOS 14+), `gpu` when `nvidia-smi` or `rocminfo` works, `light` otherwise.
2. Asks whether to start at login (only with `--interactive`; the default is yes, and a previous
   "no" is remembered).
3. Installs [uv](https://docs.astral.sh/uv/) when it is missing, then Python 3.12 through uv.
4. Installs the gateway as an isolated uv tool, pinned to this release:
   `uv tool install --python 3.12 "abstractgateway[<profile>,tray]==0.4.3"`, from prebuilt wheels
   only (see [No compiler needed](#no-compiler-needed)), and checks that the command starts
   (reinstalling it in place when it does not).
5. Optionally installs Node.js for the browser apps, terminal tools, Ollama or LM Studio (flags
   below).
6. Registers the gateway to start at login with `abstractgateway service install --port N` (a
   LaunchAgent on macOS, a `systemd --user` unit on Linux, a Startup shortcut on Windows) and starts
   it. The login item runs plain `abstractgateway serve`, so the gateway's
   [Network setting](#network-setting-who-can-reach-the-gateway) decides where it listens. With
   `--no-service` (or "no" to the question), or on a Linux host without a user systemd session, it
   starts the gateway in the background instead and removes a login item an earlier run
   registered.
7. Waits up to 180 seconds for `/api/health`, then opens `http://127.0.0.1:8080/console` through a
   one-time sign-in link (`abstractgateway-config claim-url`, valid 10 minutes, this machine only).
   If no link can be created, it shows where the admin token is.

Every command is printed as it runs, and the summary lists them all. Re-running the script
upgrades or repairs the install in place. The macOS package is payload-free: it copies the two
`.command` files into `~/Library/Application Support/AbstractFramework/Installer` and opens
`install.sh --interactive` in Terminal. It is built by
[`scripts/lib/build_macos_installer.sh`](../scripts/lib/build_macos_installer.sh).

### No compiler needed

By default the script never compiles anything, so you do not need Xcode Command Line Tools, gcc
or the MSVC Build Tools. It passes uv a small overrides file (`uv-overrides.txt` in the gateway
data directory; `--print` shows it) that swaps `webrtcvad` for `webrtcvad-wheels`, keeps `vllm`
to Linux, and leaves out the two compiled extras below, and it refuses to build those packages
from source, so a gap fails with a clear error instead of starting a compiler. If macOS asks you
to install the command line developer tools (an `xcode-select` prompt) during an install, cancel
the prompt, fetch the script again with the one-liner above and re-run it.

### llama.cpp GGUF models

Every profile, light included, gets in-process llama.cpp GGUF support (`llama-cpp-python`). PyPI
has only its source, so the script takes upstream's prebuilt wheel from
[abetlen's wheel index](https://abetlen.github.io/llama-cpp-python/whl/) (`--find-links` on the
package page, pinned in `uv-constraints.txt` next to the overrides file):

| Machine | Wheel |
|---|---|
| Apple Silicon Mac | `llama-cpp-python==0.3.28`, Metal (GPU offload) |
| Linux x86_64 / aarch64 (glibc) | `llama-cpp-python==0.3.35`, CPU |
| Windows x64 | `llama-cpp-python==0.3.35`, CPU |
| Intel Mac, musl Linux, Windows on ARM | no prebuilt wheel: skipped |

Where no wheel exists, or when the wheel install fails, the script installs everything else and
says `GGUF (llama.cpp) skipped: no prebuilt wheel for this machine`; `--full` then builds it from
source. The summary's `GGUF:` line says which wheel was installed. On Apple Silicon the Metal wheel
is pinned to 0.3.28, the newest Metal wheel on that index that passes uv's archive integrity
check.

### Compiled extras

Two optional engines publish no wheel on PyPI and are skipped by default: stable-diffusion.cpp
image generation (`stable-diffusion-cpp-python`) and voice echo cancellation
(`aec-audio-processing`). `--full` (Windows: `-Full`) keeps them and builds them, and llama.cpp,
from source, which takes several minutes and needs a C/C++ compiler (macOS:
`xcode-select --install`; Debian/Ubuntu: `sudo apt-get install -y build-essential`; Windows:
Visual Studio Build Tools with "Desktop development with C++"). Without a compiler, `--full` stops
before installing anything. You do not need them for MLX on Apple Silicon, for llama.cpp GGUF
models (above), for Ollama, LM Studio or other endpoint engines, or for cloud providers.

### Options

| macOS/Linux | Windows | What it does |
|---|---|---|
| `--profile auto\|light\|apple\|gpu` | `-Profile` | Override the profile choice |
| `--port N` | `-Port N` | Gateway port (default 8080; the next free port when 8080 is busy) |
| `--with-apps` | `-WithApps` | Make sure Node.js 18+ exists for the browser apps (`uv tool install nodejs-wheel`, no admin) |
| `--with-ollama` | `-WithOllama` | Run Ollama's official installer (Linux uses sudo; the script tells you first) |
| `--with-lmstudio` | `-WithLmStudio` | Install LM Studio (headless daemon on macOS/Linux, winget on Windows) |
| `--with-console` | `-WithConsole` | Install the `abstractgateway-console` crate with cargo (terminal console; needs Rust) |
| `--with-code-cli` | `-WithCodeCli` | Install the `abstractcode` crate with cargo (terminal client; needs Rust) |
| `--with-core-cli` | `-WithCoreCli` | Also put the `abstractcore` command on PATH |
| `--full` | `-Full` | Also build the [compiled extras](#compiled-extras) and llama.cpp from source (needs a C compiler) |
| `--no-tray` | `-NoTray` | Leave out the menu-bar icon (`tray` extra) |
| `--no-service` | `-NoService` | Do not register a login service |
| `--no-start` | `-NoStart` | Install only; do not start the gateway |
| `--no-open` | `-NoOpen` | Do not open the browser |
| `--no-modify-path` | `-NoModifyPath` | Do not add `~/.local/bin` to your shell profile (`uv tool update-shell`) |
| `--pin X` / `--from PATH` | `-Pin` / `-From` | Install another gateway version or a local checkout |
| `--manifest PATH` | `-Manifest` | Read the gateway pin from this `install-manifest.json` |
| `--data-dir DIR` | `-DataDir` | Gateway data directory |
| `--interactive` | — | Ask whether to start at login (and, with `--uninstall`, whether to delete data and uv); the double-click installers pass it |
| `--print` | `-Print` (or `-WhatIf`) | Show the plan and every command; change nothing |
| `--print-versions` | `-PrintVersions` | Print the pinned gateway, npm app and crate versions, then exit |
| `-v`, `--verbose` | — | Show the full output of every command |
| `--uninstall [--purge] [--remove-uv]` | `-Uninstall [-Purge]` | Remove the service and uv tools (purge also deletes the data; `--remove-uv` also removes uv, its Python and cache when the installer added uv) |

Pass options through the one-liner like this:

```bash
curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh -s -- --with-apps --with-ollama
```

```powershell
& ([scriptblock]::Create((irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1))) -WithApps -WithOllama
```

### The same install by hand

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh             # Windows: irm https://astral.sh/uv/install.ps1 | iex
uv python install 3.12
uv tool install --python 3.12 "abstractgateway[tray]==0.4.3"    # [apple,tray] or [gpu,tray] for local engines
                       # (add --with and --overrides as shown by `install.sh --print` to avoid compiling)
uv tool update-shell                                          # puts ~/.local/bin on PATH; open a new terminal
abstractgateway service install --port 8080                   # or: abstractgateway serve
abstractgateway-config claim-url --base-url http://127.0.0.1:8080  # prints a one-time console link
```

### Run at login

`abstractgateway service install` registers the login service (LaunchAgent on macOS,
`systemd --user` on Linux, a Startup shortcut on Windows, experimental) and starts the gateway:

```bash
abstractgateway service install --port 8080
abstractgateway service status
abstractgateway service uninstall
```

The login item runs plain `abstractgateway serve`, so the host and port come from the Network
setting below. Passing `--host` to `service install` stores the matching mode in that setting.

The bootstrap scripts use it by default. Gateways older than 0.3.0 (installed with `--pin`) have no
`service` command: `install.sh` then starts the gateway in the background and `install.ps1` adds a
Startup-folder shortcut. To write a login entry yourself on Linux, a user unit is enough:

```ini
# ~/.config/systemd/user/abstractgateway.service
[Service]
Environment=ABSTRACTGATEWAY_USER_AUTH=1
Environment=ABSTRACTGATEWAY_DATA_DIR=%h/.local/share/abstractgateway
ExecStart=%h/.local/bin/abstractgateway serve
Restart=on-failure

[Install]
WantedBy=default.target
```

Then `systemctl --user daemon-reload && systemctl --user enable --now abstractgateway`. On macOS
use a LaunchAgent in `~/Library/LaunchAgents/` with the same absolute command and environment
(launchd does not read your shell profile, so use absolute paths).

### Network setting: who can reach the gateway

The gateway listens on this computer only (`localhost`, `127.0.0.1:8080`) until you choose
otherwise. Change it in the console's network panel, from the menu-bar icon's **Network** menu, or
in a terminal:

```bash
abstractgateway network status                      # configured vs running mode, addresses, warnings
abstractgateway network set lan                     # other devices on your local network
abstractgateway network set localhost --port 8080   # back to this computer only
abstractgateway network set internet --acknowledge-internet   # public; TLS and port forwarding are yours
abstractgateway network restart --token <admin token>   # apply it to the running gateway now
abstractgateway network addresses                   # every URL a client can use
```

A new mode or port applies at the next start: `network restart`, the console, the menu-bar icon's
**Restart AbstractGateway…**, or the next login. Keep
`localhost` unless you need another device to connect; see
[Gateway security](guide/gateway-security.md) before choosing `internet`.

### Upgrade and uninstall

- Upgrade: re-run the one-liner (or `uv tool upgrade abstractgateway`).
- Uninstall: [Remove AbstractFramework](#remove-abstractframework) above,
  `curl -LsSf .../install.sh | sh -s -- --uninstall` (Windows: the script block with
  `-Uninstall`), or by hand: `abstractgateway service uninstall` (when registered), then
  `uv tool uninstall abstractgateway`. Your data stays in the data directory until you delete it
  (`--purge`). See [Operations and support](installers/operations-and-support.md) for locations.

### Check the install

```bash
uvx abstractframework doctor
```

It checks Python, uv, Node, disk, the gateway (`ABSTRACTGATEWAY_URL`, default
`http://127.0.0.1:8080`), and whether Ollama and LM Studio are reachable. It only reads.

## Install the Python framework (developers)

Use the profiles below when you want every framework library in one Python environment, for
example to build on AbstractCore or AbstractRuntime directly. Choose the profile by deciding where
inference should run. The framework APIs stay the same across profiles; the profiles mainly change
whether local inference engines are installed.

### Quick chooser

| Profile | Command | Use when | Local inference stacks |
|---|---|---|---|
| Light | `pip install abstractframework` | You use cloud APIs or endpoint servers such as LM Studio, Ollama, vLLM, llama.cpp, OpenRouter, or OpenAI-compatible services. | No |
| Apple | `pip install "abstractframework[apple]"` | You are on Apple Silicon and want local MLX/Metal-capable engines as well as endpoint providers. | Yes, Apple-focused |
| GPU | `pip install "abstractframework[gpu]"` | You have a supported discrete GPU and want local GPU-capable engines as well as endpoint providers. | Yes, GPU-focused |

#### Requirements per profile

| Profile | Platforms | Python | Notes |
|---|---|---|---|
| Light | macOS, Linux, Windows | 3.10–3.13 | No local inference engines. |
| Apple | macOS 14 or later on Apple Silicon | 3.10–3.13 | MLX wheels need macOS 14+. F5-TTS voice cloning needs Python 3.11+; the rest of the profile works on 3.10. |
| GPU | Linux (and Windows where the engines publish wheels) with NVIDIA CUDA or AMD ROCm drivers | 3.10–3.13 | F5-TTS voice cloning needs Python 3.11+; the rest of the profile works on 3.10. |

A plain `pip install` of the `apple` or `gpu` profile builds the
[compiled extras](#compiled-extras) (`llama-cpp-python`, `stable-diffusion-cpp-python`,
`aec-audio-processing`) from source, so it needs a C/C++ compiler. The one-line install above
does not.

`abstractframework` 0.3.2 pins `abstractgateway==0.4.3`, `abstractassistant==0.5.0`,
`abstractcore==2.15.2`, `AbstractRuntime==0.4.35`, `abstractagent==0.3.13`,
`AbstractMemory==0.3.0`, `abstractsemantics==0.0.5`, `abstractvoice==0.11.4`,
`abstractvision==0.3.29` and `abstractmusic==0.1.15`. The `apple` and `gpu` extras select
`abstractgateway[apple|gpu]` and `abstractassistant[apple|gpu]` at the same versions
(`abstractassistant[apple]` is installed on macOS only). `abstractframework doctor` reports any
installed package whose version differs from these pins.

Light is not a reduced-functionality framework. It is the remote-first profile: multimodal input,
multimodal output, embeddings, tools, durable runs, workflows, and Gateway/Flow still work when
they are backed by remote or local endpoint providers.

## Recommended technical install

Use a clean virtual environment:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -U pip
python -m pip install abstractframework
abstractframework doctor
```

Use `uv` if you prefer faster environment management:

```bash
uv venv
source .venv/bin/activate
uv pip install abstractframework
abstractframework doctor
```

`pipx` is useful for isolated command-line apps, but a normal venv is usually clearer for the full
framework because Gateway, Flow, Core, and local plugins share one environment.

## Light profile

```bash
pip install abstractframework
```

Choose Light when:

- you use OpenAI, Anthropic, OpenRouter, Portkey, or other hosted providers;
- you use local model servers through HTTP, such as LM Studio, Ollama, vLLM, llama.cpp, or LocalAI;
- you want the smallest and least surprising install;
- you do not want pip to install MLX, CUDA, Diffusers, or local model-runtime stacks.

After install, run `abstractframework doctor`, start the gateway and configure providers in its
console (see [Start the gateway](#start-the-gateway-and-apps)).

## Apple profile

```bash
pip install "abstractframework[apple]"
```

Choose Apple when:

- you are on Apple Silicon;
- you want local Apple/MLX-capable inferencers in addition to endpoint providers;
- you accept larger downloads and platform-specific native dependencies.

Then run `abstractframework doctor`.

## GPU profile

```bash
pip install "abstractframework[gpu]"
```

Choose GPU when:

- you have a supported GPU stack and drivers;
- you want local GPU-capable inferencers in addition to endpoint providers;
- you accept larger downloads and platform-specific native dependencies.

Then run `abstractframework doctor`.

## Apps and tools outside pip

The browser apps and the Rust terminal tools are not Python packages, so no profile installs them.
Run or install them next to the Python stack:

| Tool | Command | Version released with 0.3.2 |
|---|---|---|
| Gateway web console | built into `abstractgateway`: open the link `abstractgateway serve` prints (`http://127.0.0.1:8080/console#claim=…`) | 0.4.3 |
| Core web console | built into `abstractcore`: open the link `abstractcore serve` prints (`http://127.0.0.1:8000/console#claim=…`) | 2.15.2 |
| Core terminal console | `cargo install abstractcore-console` (Rust 1.87+), then `abstractcore-console` (uses the `abstractcore` command) | 0.2.0 |
| Gateway terminal console | `cargo install abstractgateway-console` (Rust 1.87+), then `abstractgateway-console --url http://127.0.0.1:8080` | 0.8.0 |
| Flow Editor | `npx @abstractframework/flow` | 0.3.20 |
| Code Web UI | `npx @abstractframework/code` | 0.4.2 |
| Observer | `npx @abstractframework/observer` | 0.1.12 |
| Continuum console | `npx @abstractframework/continuum` | 0.3.1 |
| Entity manager | `npx @abstractframework/entity` | 0.2.1 |
| AbstractCode terminal client | `cargo install abstractcode`, or a prebuilt binary from the [AbstractCode GitHub release](https://github.com/lpalbou/AbstractCode/releases) | 0.5.1 |

The browser apps need Node.js 18 or later and a running gateway. Optional Python add-ons outside the
profiles install on their own: `pip install abstract3d`, `pip install abstractcamera`,
`pip install abstractskill`.

## Start the gateway and apps

Start the gateway, open its console, then any browser app against it:

```bash
abstractgateway serve            # binds 127.0.0.1:8080 and prints a one-time console link
npx @abstractframework/flow
```

Configure providers, API keys, engines and default models in the console. For library-only use
of AbstractCore, `abstractcore serve` opens the same Models and Engines screens in its own console,
and `abstractcore --config` remains available in the terminal. When you work from source, the workspace helper
scripts build and start the same services (see [Workspace scripts](workspace-scripts.md)).

Gateway-hosted browser apps use Gateway user tokens and browser sessions. Do not use the bootstrap
server token as a browser login token.

## Container deployment

For a server/VPS deployment, prefer the Gateway container rather than installing every app package
on the host:

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

This is the Light container: full framework capabilities through remote/endpoint inference, without
local MLX/CUDA stacks. On first start it creates `default/admin` and writes the login token to
`runtime/auth/bootstrap-admin-token`. Use `ghcr.io/lpalbou/abstractgateway:gpu-latest` only on an
NVIDIA host when you explicitly want the local GPU profile (pinned tags are `<version>-gpu`, published on a best-effort basis; this image is
experimental). The AbstractCore OpenAI-compatible server is also published as
`ghcr.io/lpalbou/abstractcore-server:2.15.2`.

## How installs are designed

The scripts and the gateway console are the install experience on every OS. On macOS the
double-click artefacts (a payload-free `.pkg` and two `.command` files) only launch the same
script in Terminal; they add no install logic of their own. Design, security model and per-OS
behavior are in [docs/installers](installers/README.md).

## Generated install manifest

The installer-facing contract is generated from the root release profile:

```bash
abstractframework manifest
abstractframework manifest --check docs/installers/install-manifest.json
abstractframework manifest --write install-manifest.json
```

The bootstrap scripts read `bootstrap.gateway_version` from it; other installers should consume
this manifest instead of maintaining independent package pins. Field reference:
[release-and-manifest.md](installers/release-and-manifest.md).

--- docs/configuration.md ---
# Configuration

This page answers two practical questions:

1. **What do I need to configure first to be productive?**
2. **Where do defaults live in a Core-first vs Gateway-first setup?**

Key principle: **Core owns model/provider defaults; Gateway owns durable execution and operations.**

---

## Core-first quick start

### Interactive wizard (recommended)

```bash
abstractcore --config      # guided setup — persists to ~/.abstractcore/config/
abstractcore --status      # show current config
abstractcore --install     # check readiness + download missing assets
```

The wizard walks through: default provider/model, base URLs, API keys, vision fallback, audio/video strategies, embeddings, and logging. Config is stored in `~/.abstractcore/config/abstractcore.json`.

### Manual environment variables

**Ollama** (local, free):

```bash
export OLLAMA_HOST="http://localhost:11434"
```

**OpenAI-compatible server** (LM Studio, vLLM, LocalAI, llama.cpp):

```bash
export OPENAI_BASE_URL="http://127.0.0.1:1234/v1"
export OPENAI_API_KEY="local"
```

**Cloud APIs**:

```bash
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```

---

## Gateway-first quick start

### Defaults and explicit settings

A bare `abstractgateway serve` needs no configuration: it listens on `127.0.0.1:8080`, turns user
auth on and keeps its data in the per-user data folder. Set the environment when you want those
choices to be explicit, for example in a service definition:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
```

When user auth is enabled, `abstractgateway serve` ensures `default/admin`
exists and writes the first browser-login token to
`$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token`. Users sign in with a
Gateway user id and that user's token, then browser apps keep only an opaque
Gateway session. `ABSTRACTGATEWAY_AUTH_TOKEN` remains available for legacy
server/operator bearer-token deployments, but it maps to `local-admin` and is
not a browser sign-in token.

Gateway serves `/console` for account/runtime summary, admin user management,
optional user email metadata, token rotation, retained runtime transfer/purge,
provider connections, and multimodal capability defaults selected from
available providers. Deleted users leave retained runtime reservations;
admins can transfer retained runtime data to an existing same-tenant user or
purge the retained runtime directory before releasing the runtime id for reuse.

### Recommended (bundle-based workflows)

```bash
export ABSTRACTGATEWAY_WORKFLOW_SOURCE=bundle
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="$PWD/bundles"
```

### Start

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

`--host` and `--port` override the gateway's stored Network setting for this start. Without them,
`serve` uses the setting (`localhost` on port 8080 until you change it).

### Network exposure

The Network setting decides who can reach the gateway: `localhost` (this computer only), `lan`
(devices on your local network) or `internet` (public; you provide TLS and port forwarding). It is
stored in the data directory and applied at each start, including starts from the login item:

```bash
abstractgateway network status
abstractgateway network set lan --port 8080
abstractgateway network set --allowed-origins https://gateway.example.com
```

The console's network panel and the menu-bar icon change the same setting. See
[Network setting](install.md#network-setting-who-can-reach-the-gateway) and
[Gateway security](guide/gateway-security.md).

---

## Model/provider defaults (capability routes)

AbstractCore organizes defaults as **capability routes** — stable slots scoped by capability, not by application:

| Route | Meaning |
|---|---|
| `input.text` | Canonical LLM text model for understanding and generation |
| `output.text` | Read-only derived view of `input.text` |
| `input.image` | Image-understanding fallback when `input.text` is not vision-capable |
| `input.video` | Video-understanding fallback when native video/frame support is not available or should be overridden |
| `input.voice` | Speech-to-text fallback for audio attachments |
| `input.sound` | Non-speech audio/SFX understanding route, not a speech transcription route |
| `input.music` | Music-audio understanding route, not a speech transcription route |
| `embedding.text` | Default embeddings model |

### Set defaults in Core (single-host)

```bash
abstractcore config set-default input.text \
  --provider ollama \
  --model qwen3:4b-instruct

abstractcore config defaults
```

The older `abstractcore --set-global-default ...` and
`abstractcore --set-capability-default ...` flags remain supported for
compatibility. The `abstractcore config ...` form is the preferred explicit
route-default syntax.

Model discovery for LLM and embedding defaults can filter by Core route keys,
for example `capability_route=input.image,output.text`,
`capability_route=input.sound,output.text`, or
`capability_route=embedding.text`. Generated image/video/voice/sound/music
defaults use capability plugin catalogs instead, so plugin readiness and
download/setup state are not stored in `model_capabilities.json`.

AbstractFlow uses the same Gateway/Core discovery contract for text model
authoring. Text provider/model selectors ask Gateway for `output.text` models,
and the Models Catalog node can store a capability route such as
`input.image,output.text` so workflows can discover the provider's models for a
specific input/output shape at run time. Flow stores only selection intent; Core
and Gateway remain the source of truth for model capability metadata.

### Reasoning and thinking controls

AbstractCore exposes a provider-neutral `thinking` generation option for models
that support explicit reasoning controls. Gateway accepts the same field on
`POST /api/gateway/runs/start` and stores it as `_runtime.thinking` for the
run:

```json
{
  "bundle_id": "basic-agent",
  "input_data": {"prompt": "Plan the migration"},
  "thinking": "high"
}
```

Flow LLM Call and Agent nodes also expose a Reasoning selector and a `thinking`
input pin. A node setting or pin value overrides the run default for that node;
leaving it on Auto inherits the Gateway/runtime default. Core performs the
provider-specific translation or unsupported-parameter handling.

### Core provider endpoint profiles

For reusable OpenAI-compatible endpoints, configure a named Core provider
profile once and point route defaults at its virtual provider id:

```bash
export OVH_AI_API_KEY="..."

abstractcore config set-provider ovh-provider \
  --family openai-compatible \
  --base-url https://oai.endpoints.kepler.ai.cloud.ovh.net/v1 \
  --api-key $OVH_AI_API_KEY \
  --name "OVH Provider" \
  --description "OVH hosted OpenAI-compatible endpoint"

abstractcore config models ovh-provider

abstractcore config set-default input.text \
  --provider endpoint:ovh-provider \
  --model Qwen3.5-9B
```

`endpoint:ovh-provider` is the reusable public id. The URL and key stay in the
Core config file, not in exported workflows. Use
`abstractcore config providers --json` to list profiles; raw keys are not
printed.

If you want the config to store an environment reference instead of the expanded
secret value, pass the variable literally, for example `--api-key
'$OVH_AI_API_KEY'`.

### Set defaults through the Gateway (control plane)

In gateway-first deployments, set defaults via gateway tooling so the execution host stays consistent:

```bash
abstractgateway-config set-default input.text \
  --provider ollama \
  --model qwen3:4b-instruct

abstractgateway-config defaults
```

The gateway can update route defaults, but the default schema and file format
are Core-owned. When Gateway user auth is enabled, the Gateway baseline is
stored as a Core config file:

```text
$ABSTRACTGATEWAY_DATA_DIR/config/abstractcore.json
```

Per-user writes through
`/api/gateway/config/capability-defaults/{kind}/{modality}` or
`/api/gateway/config/capability-defaults/{kind}/{modality}/{task}` are stored
as a runtime-scoped Core config file:

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

User runtime defaults override the Gateway baseline only for that runtime, so
one user's provider/model defaults do not mutate another user's defaults.
Gateway does not read `config/capability_defaults.json` overlay files; if you
have one from an older deployment, recreate those defaults with
`abstractgateway-config set-default ...`.

### Gateway provider connections

Gateway Console also lets signed-in users create reusable provider connections
through a guided setup flow for OpenAI, Anthropic, OpenRouter, Portkey, LM
Studio, Ollama, and custom OpenAI-compatible endpoints. A connection stores a
display name, description, provider family, optional base URL, optional API key,
and optional advanced model allowlist. The raw API key is write-only through the
API/UI and is not returned in discovery responses. AbstractCore owns model
capability metadata, so normal setup does not ask users to classify models
manually.

The **Providers** tab is where endpoint base URLs and API keys are configured.
Its **Test** action calls the selected provider or endpoint and previews model
discovery before saving. Leaving all advanced model restrictions unselected
keeps live discovery active; selecting models stores a fixed allowlist for that
profile. The **Multimodal Capabilities** tab intentionally does not ask for a
base URL or API key: it maps each capability route to one available provider
and one discovered/allowed model. Available providers include saved Gateway
provider connections plus direct providers such as `openai` or `anthropic`
when their required key is already present in scoped Core config or process
environment. LM Studio and Ollama also appear automatically when Gateway can
reach their configured or default local endpoints and discover models from
them.

The **Sandbox** tab uses the same provider/default contract. Choose a configured
capability route, then send a short prompt. Text chat uses the selected text
default, and generated media routes such as `output.image.text_to_image`,
`output.video.text_to_video`, `output.voice`, `output.sound`, or
`output.music` run through that route's configured provider/model and return
the generated artifact link. Image edit, image upscale, and image-to-video use
separate defaults: `output.image.image_to_image`,
`output.image.image_upscale`, and `output.video.image_to_video`. Gateway
Console presents these concrete generated-media routes instead of the broad
`output.image` and `output.video` compatibility defaults.

`input.voice`, `input.video`, `input.sound`, and `input.music` are fallback gates, not hidden
package probes. If a route is unconfigured and the primary text model cannot
handle that input natively, the request fails with a configuration error rather
than silently using an installed speech, vision, audio, or music package.
`input.video`, `input.sound`, and `input.music` can be reported as covered by
`input.text` when the text model supports those inputs; operators can still
override them with dedicated routes.

After creation, the profile appears in provider discovery as a virtual provider
id such as `endpoint:office-vllm`. Use that provider id in AbstractFlow nodes or
Gateway capability defaults, then select a discovered/allowed model normally.
At run time Gateway resolves the virtual provider into the real provider family,
base URL, and key for the current runtime call; workflow JSON and exported
bundles should not contain raw secrets.

---

## Storage and persistence

### Gateway data directory

`ABSTRACTGATEWAY_DATA_DIR` is the durability root:

- Run state
- Ledger history
- Artifacts (files, media, big payloads)
- Schedules

**Back up this directory** if you care about long-lived runs and audit trails.

### Optional SQLite backend

For production deployments, you can use SQLite for run metadata (artifacts stay file-backed):

```bash
export ABSTRACTGATEWAY_STORE_BACKEND=sqlite
export ABSTRACTGATEWAY_DB_PATH="$ABSTRACTGATEWAY_DATA_DIR/gateway.sqlite3"
```

### Core config directory

`~/.abstractcore/config/` stores persisted provider keys, base URLs, and
defaults for direct Core usage (written by `abstractcore --config` or
`abstractcore config ...`). You can target another config explicitly with
`ABSTRACTCORE_CONFIG_FILE`, `ABSTRACTCORE_CONFIG_DIR`, or
`abstractcore config --config-file /path/to/abstractcore.json ...`.

---

## Client configuration (Observer / Flow Editor / Code Web UI)

All gateway-backed browser UIs need two things:

- **Gateway base URL** (example: `http://127.0.0.1:8080`)
- **Gateway user + user token** in hosted user-auth mode

Local single-user tools can still use `ABSTRACTGATEWAY_AUTH_TOKEN` as an
operator token. Hosted browser apps should exchange user tokens for Gateway
browser sessions and should not store bearer tokens in browser storage.
AbstractFlow, AbstractCode Web, and AbstractObserver use this hosted
browser-session path when you provide a Gateway user id.

When these browser UIs are served from a non-loopback hostname, the Gateway URL
comes from the UI server configuration. Browser-supplied Gateway URL changes
are rejected unless the app-specific remote override is enabled behind your own
access control.

### AbstractObserver

```bash
npx @abstractframework/observer
```

Set Gateway URL, Gateway user, and that user's token in the UI
(http://localhost:3001). Observer exchanges the token for an app-scoped browser
session and does not persist the token in browser settings.

### Flow Editor

```bash
npx @abstractframework/flow
```

Optional convenience variable:

```bash
export ABSTRACTFLOW_GATEWAY_URL="http://127.0.0.1:8080"
```

### Code Web UI

```bash
npx @abstractframework/code
```

Open http://localhost:3002 and set the gateway URL in the UI settings.

---

## Multimodal capability plugins

Modalities are optional and become available when installed:

| Plugin | Capability | API surface |
|---|---|---|
| `abstractvoice` | Voice | `llm.voice` / `llm.audio` |
| `abstractvision` | Images | `llm.vision` |
| `abstractmusic` | Music | `llm.music` |

**Plugins are configured on the host that actually executes** (local app or gateway host), because that's where model downloads and hardware constraints apply.

---

## Per-project configuration references

This repo covers the overview. Each component repo owns its detailed configuration surface:

- **AbstractCore**: [centralized config](https://github.com/lpalbou/abstractcore/blob/main/docs/centralized-config.md), [prerequisites](https://github.com/lpalbou/abstractcore/blob/main/docs/prerequisites.md)
- **AbstractGateway**: [configuration](https://github.com/lpalbou/AbstractGateway/blob/main/docs/configuration.md), [security](https://github.com/lpalbou/AbstractGateway/blob/main/docs/security.md)
- **AbstractVoice**: [installation](https://github.com/lpalbou/abstractvoice/blob/main/docs/installation.md), [model management](https://github.com/lpalbou/abstractvoice/blob/main/docs/model-management.md)
- **AbstractVision**: [configuration](https://github.com/lpalbou/abstractvision/blob/main/docs/reference/configuration.md), [backends](https://github.com/lpalbou/abstractvision/blob/main/docs/reference/backends.md)

---

## Related docs

- **[Getting Started](getting-started.md)** — first run Core-first or Gateway-first
- **[Architecture](architecture.md)** — what lives where and why
- **[Glossary](glossary.md)** — capability routes, durable execution terms
- **[Troubleshooting](troubleshooting.md)** — provider, connection and sign-in problems

--- docs/workspace-scripts.md ---
# Workspace scripts (working from source)

AbstractFramework is developed as one workspace: this repository at the root and every package
repository cloned next to its files (`abstractcore/`, `abstractgateway/`, `abstractuic/`, …). Each
directory is its own git repository. The scripts in `scripts/` let you clone, build, inspect,
commit, pull and push the whole set at once, always in dependency order.

If you only want to *use* the framework, install the published packages instead: see
[Install](install.md). This page is for contributors who build from the checkouts.

## The package inventory

`scripts/lib/packages.txt` is the single list every script reads. It has one row per published
package (30 packages in 21 repositories):

| Column | Meaning |
|---|---|
| `id` | Short package id used by the scripts (`abstractcore`, `panel-chat`, `code-cli`, …) |
| `repo` | Checkout directory under the workspace root (`.` is this repository) |
| `github` | GitHub `owner/Name`, used by `clone.sh` |
| `kind` | `python`, `npm`, `rust` or `meta` (the root meta-package) |
| `path` | Package directory inside the repository (`abstractuic/panel-chat`, `abstractcode/web`, `abstractgateway/console-tui`, …) |
| `registry`, `name` | Where it is published and under which name (PyPI distribution, npm package, crate) |
| `tier` | Dependency level: `0` has no internal dependency; otherwise one more than its highest dependency |
| `deps` | Internal dependency edges, `id:kind` |

Edge kinds explain *why* one package comes before another:

| Edge | Meaning |
|---|---|
| `dep` | Runtime dependency (pyproject `dependencies`, npm `dependencies`, Cargo `[dependencies]`) |
| `extra` | Only through an optional extra (for example `abstractcore[vision]` → `abstractvision`) |
| `peer` | npm peer dependency |
| `dev` | npm dev dependency bundled at build time from the registry |
| `alias` | Vite source alias to `../abstractuic/<package>/src`: the build needs the AbstractUIC checkout |
| `pin` | Exact `==` pin in the root `abstractframework` meta-package |
| `app` | Browser app version listed by the root install manifest |

`./scripts/deps.sh check` compares the inventory with the real `pyproject.toml`, `package.json`,
`Cargo.toml` and `vite.config.*` files and with each checkout's git origin, and fails on any
difference. Run it after you add a dependency between two packages, then update the row.

## Tiers: what comes first

`./scripts/deps.sh` prints every tier with its edges. The current order:

| Tier | Python (PyPI) | npm | Rust (crates.io) |
|---|---|---|---|
| 0 | abstractskill, abstractsemantics, AbstractMemory, abstractvision, abstractvoice, abstractmusic, abstractcamera | ui-kit, app-server, monitor-flow, monitor-gpu, monitor-memory, monitor-active-memory | abstracttui |
| 1 | abstract3d | panel-chat, flow | abstractcore-console, abstractcode |
| 2 | abstractcore | observer, continuum, entity, code (web) | abstractgateway-console |
| 3 | AbstractRuntime | | |
| 4 | abstractagent, abstractassistant | | |
| 5 | abstractgateway | | |
| 6 | abstractframework (meta-package) | | |

Install, build and release tier by tier: a package is only built or published after everything it
depends on. To see what has to follow a change to one package, ask for its reverse dependencies:

```bash
./scripts/deps.sh rdeps abstractcore
```

The same inventory drives the release sequence described in
[ADR 0034](adr/0034-framework-release-sequence-and-gates.md).

## Commands

| Script | What it does |
|---|---|
| `./scripts/clone.sh [DIR]` | Clone every repository (tier order), or fast-forward the ones already cloned. `--list` prints the repositories. |
| `./scripts/deps.sh` | Tiers and dependency edges. Also `order`, `rdeps ID`, `check`, `versions`, `json`, `--kind python\|npm\|rust`. |
| `source ./scripts/build.sh` | Build every package from the checkouts, tier by tier, and stay in the virtualenv. See below. |
| `./scripts/status.sh` | Git overview of every repository, grouped by tier: branch, changes, unpushed and unpulled commits, and the packages each repository holds. `--short` shows only repositories with pending work. |
| `./scripts/status.sh --registry` | Adds local version vs latest published version on PyPI, npm and crates.io for every package (network). `--versions` shows local versions only; `--tiers` adds the dependency view. |
| `./scripts/pull.sh` | Fetch and fast-forward `main` everywhere. Never merges or rebases; a diverged repository is reported and left alone. `--dry-run` fetches and reports only. |
| `./scripts/commit.sh "message"` | Commit every dirty repository with the same message (`git add -A` per repository). Does not push. |
| `./scripts/push.sh` | Dry run: lists, per repository, the commits `main` would push. `--yes` pushes them. Never force-pushes, never pushes tags, skips a diverged `main`. `--fetch` refreshes the upstream first. |
| `./scripts/install.sh` | Install the *published* release (not the checkouts); see [Install](install.md). `--print` shows the plan. |

`pull.sh` and `push.sh` accept `--only repo1,repo2` to work on a subset.

A typical day:

```bash
./scripts/pull.sh --dry-run      # what changed upstream
./scripts/pull.sh                # fast-forward
source ./scripts/build.sh        # rebuild what you work on
./scripts/status.sh --short      # what you changed
./scripts/commit.sh "Describe the change"
./scripts/push.sh                # review, then:
./scripts/push.sh --yes
```

## Building from source

`build.sh` builds the three ecosystems in dependency order:

- **Python**: editable installs (`pip install -e`) of the 13 Python packages and the root
  meta-package into one virtualenv. Third-party dependencies still come from PyPI.
- **npm**: the seven AbstractUIC packages (installed once at the `abstractuic` workspace root, built
  per package), then the apps `flow`, `observer`, `continuum`, `entity` and `code` (web).
- **Rust**: `cargo build` of `abstracttui`, `abstractcore-console` (`abstractcore/console-tui`),
  `abstractcode` (`abstractcode/tui`) and `abstractgateway-console` (`abstractgateway/console-tui`).

| Option | Effect |
|---|---|
| `--python`, `--npm`, `--rust` | Build only the selected ecosystems (they combine). Default: all three. |
| `--light` (default), `--apple`, `--gpu` | Python dependency profile; also `AF_BUILD_PROFILE=light\|apple\|gpu\|auto`. |
| `--clean` | Delete the virtualenv first. |
| `--plan` | Print the tier-ordered build plan and build nothing. |
| `AF_VENV_DIR=path` | Virtualenv location (default `<root>/.venv`). The `-local` launchers use the same variable. |

The npm and cargo output is kept in a log per package; the terminal shows one line per package and
the log tail when a step fails. The script exits non-zero when any selected ecosystem failed.

Prerequisites: Python 3.10+, Node.js 18+ for the npm packages, a Rust toolchain for the crates.

## Running the stack

| Published packages | Local checkouts | Starts |
|---|---|---|
| `start.sh` / `af.sh` | `start-local.sh` / `af-local.sh` | The whole stack under one supervisor: gateway first, then the apps |
| `gateway.sh` | `gateway-local.sh` | AbstractGateway (control plane, port 8080) |
| `flow.sh` | `flow-local.sh` | `@abstractframework/flow` |
| `observer.sh` | `observer-local.sh` | `@abstractframework/observer` |
| `code.sh` | `code-local.sh` | `@abstractframework/code` (web) |
| `console.sh` | `console-local.sh` | `@abstractframework/continuum` |
| `entity.sh` | `entity-local.sh` | `@abstractframework/entity` |
| `assistant.sh` | `assistant-local.sh` | AbstractAssistant (tray app) |

The stack launchers use one port map: gateway 8080, observer 3001, continuum 3002, code 3003,
entity 3004, flow 3005 (each overridable with `ABSTRACT<APP>_PORT`). `start-local.sh --build`
runs `build.sh` first.

## Script tests

```bash
bash scripts/tests/test_inventory.sh      # inventory vs package files, install pins vs manifest
bash scripts/tests/test_repo_scripts.sh   # clone/status/commit/push/pull/build in an offline sandbox
bash scripts/tests/test_af_supervisor.sh  # stack supervisor semantics with stub services
```

All three run against temporary directories and stub services; they do not modify the workspace.

--- docs/glossary.md ---
# Glossary

Shared terminology used across AbstractFramework documentation.

If you're new, read these groups first:

- **Durable execution**: run, ledger, effect, wait, artifact
- **Workflows**: flow, bundle, interface contract
- **Control plane**: gateway, schedule, observer, gateway console, Network setting
- **Distribution**: Mac installer, bootstrap script, install profile, release pins

---

## Core (LLM SDK)

### Provider

An LLM backend integration (Ollama, OpenAI, Anthropic, any OpenAI-compatible server, etc.).

### Model

A provider-specific model identifier (`qwen3:4b-instruct`, `gpt-4o-mini`, `claude-3-5-sonnet-latest`, etc.).

### Capability route

A stable "slot" for a default model/provider choice, scoped by capability rather than by application. Examples: `input.text` (canonical LLM text route), `input.image` (fallback image-understanding route when the text model is not vision-capable), and `embedding.text` (default embeddings). `output.text` is a read-only derived view of `input.text`.

### Capability plugin

An optional package that extends AbstractCore with a modality API without bloating the base install. Install a plugin and the API appears on `llm` instances:

- `abstractvoice` → `llm.voice` (TTS) / `llm.audio` (STT)
- `abstractvision` → `llm.vision` (image generation)
- `abstractmusic` → `llm.music` (text-to-music)

---

## Tools

### Tool spec (schema)

A JSON-serializable description of a tool: name, description, and input schema. Tool specs are durable — they can be stored in the ledger and replayed.

### Tool executor (callable)

The host-side implementation that actually runs a tool. Executors are **not** durable; they live in the process that owns tool execution.

### Approval boundary

By default, tool execution is gated behind an explicit approval/resume step. This makes tool side-effects auditable, controllable, and restart-safe. Approval policy is configurable per tool (auto-approve safe tools, require manual approval for mutations).

---

## Durable execution (Runtime)

### Run

A durable workflow instance, identified by a `run_id`. A run has persisted state and a full append-only history.

### Session ID

A stable identifier used to group multiple runs into a long-lived "session" across time and clients (a chat thread, a device session, etc.).

### Ledger

The append-only history of what happened in a run (steps, effects, results, waits, errors). Replay-first clients render by replaying the ledger and then streaming new events.

### Step

One recorded unit in the ledger (node transitions, effect requests, results, errors).

### Effect

A typed request for a side-effect (LLM call, tool calls, ask user, wait-until, …). Effects are recorded so a run can resume correctly after restarts.

### Wait

A durable pause point: the run is checkpointed and stops progressing until it is resumed with external input (tool results, user input, time, or an event).

### Artifact

A Runtime-owned durable file payload referenced from JSON state/ledger. Use
artifacts for large payloads (files, media, evidence, big tool results) so run
state stays JSON-safe while the bytes remain reusable, searchable, and
observable across runs.

### Workspace File / Workspace Folder

A server-side file or folder inside the current Gateway-approved workspace
scope. This is the engineering term for server paths used by file helpers,
imports, exports, and run workspaces. A workspace file is not generic arbitrary
server filesystem access.

### Local File / Local Folder

A client-side source chosen from the current device. In hosted/browser mode,
this is an intake source, not a durable runtime path. Local file bytes are
typically uploaded and stored as an Artifact before durable execution.

### Server File / Server Folder

A user-facing source label for a workspace-scoped server file or folder. In
product copy, `Server File` means “a file inside the Gateway-approved workspace
scope,” not “any file the server can see.”

---

## Agent patterns

### Agent

A runtime workflow that implements a reasoning loop: observe → think → act → repeat. AbstractAgent ships three patterns:

- **ReAct**: tool-first reasoning (observe environment, choose tool, execute, reflect)
- **CodeAct**: code execution (generate Python, execute, observe output)
- **MemAct**: memory-enhanced (read/write a knowledge graph during the loop)

Agents can run standalone or as nodes inside a Flow.

---

## Workflows (Flow)

### Flow

A workflow graph: nodes + edges + state transitions. Flows encode orchestration logic: LLM steps, tool steps, branching, loops, subflows, and agent nodes.

### VisualFlow

The JSON workflow graph format used by AbstractFlow and executed by AbstractRuntime.

### Workflow bundle (`.flow`)

A portable distribution unit that packages a VisualFlow graph plus metadata (and optionally subflows/assets). Gateways discover `.flow` bundles and expose them to clients.

### Interface contract

A versioned input/output contract a flow can implement so multiple clients can run it consistently (for example `abstractcode.agent.v1` for chat-like agent flows).

---

## Control plane (Gateway) + operations (Observer)

### Gateway

The control plane for durable runs: start/resume/cancel, persistence, scheduling, bundle discovery, and ledger serving/streaming over HTTP/SSE.

### Schedule

A durable recurring trigger owned by the gateway ("run this workflow every 24h"). Schedules survive restarts.

### Observer

A thin-client browser UI for operations: monitor runs, inspect ledger history, watch live execution, control runs, and (when enabled) create schedules.

### Gateway console

The operator console for one gateway. The web console is built into `abstractgateway` and served at `/console`; the terminal console is the separate `abstractgateway-console` crate (`cargo install abstractgateway-console`). Both include AbstractCore's **Models** and **Engines** screens.

### Core console

The console for AbstractCore on its own. The web console is served at `/console` by `abstractcore serve` (Overview, Models, Engines, Providers); the terminal console is the `abstractcore-console` crate, which is also the library that provides the Models and Engines screens to the gateway's terminal console.

### Claim link

A one-time console sign-in link (`/console#claim=<code>`), single use, valid for 10 minutes and redeemable only from the same machine. `abstractgateway serve` and `abstractcore serve` print one on a first local start; `abstractgateway claim` and `abstractgateway-config claim-url` mint a new one.

### First-run guide

The gateway console's setup flow, opened once per data folder by the claim link and later from the **Setup** button: host summary, local engines, a default model that fits the machine, and the apps.

### Network setting

The gateway's stored choice of who can reach it: `localhost` (this computer only, the default),
`lan` or `internet`, plus the port and allowed browser origins. `abstractgateway serve` and the
login item apply it at each start; change it with `abstractgateway network set`, the console or
the menu-bar icon. See [Network setting](install.md#network-setting-who-can-reach-the-gateway).

### Login item

The per-user service that starts the gateway when you log in: a LaunchAgent on macOS
(`~/Library/LaunchAgents/ai.abstractframework.gateway.plist`), a `systemd --user` unit on Linux, a
Startup shortcut on Windows. Managed with `abstractgateway service install|status|uninstall`.

### Menu-bar icon (tray)

The gateway's status icon (the `tray` extra, installed by the installer): it opens the console,
shows whether the gateway is running and changes the Network setting.

### Models and Engines

The local model and engine management shared by AbstractCore and the gateway: a model catalog with a fit verdict for this machine (`fits`, `tight`, `too_large`, `partial_offload`, `unknown`), installed models with sizes, download and delete jobs, and detection and installation of local engines (Ollama, LM Studio, MLX, llama.cpp, vLLM, transformers). Available as `abstractcore models|engines`, `abstractgateway models|engines`, and in the consoles.

---

## Distribution

### Install profile

One of the three ways to install the pinned Python stack: **Light** (`pip install abstractframework`, remote/endpoint inference only), **Apple** (`abstractframework[apple]`, adds MLX/Metal engines on macOS 14+) and **GPU** (`abstractframework[gpu]`, adds CUDA/ROCm engines). See [Install](install.md).

### Mac installer

`AbstractFramework-Installer.pkg`, attached to each GitHub release. A payload-free package that
copies **Install AbstractFramework.command** and **Uninstall AbstractFramework.command** to
`~/Library/Application Support/AbstractFramework/Installer` and runs the bootstrap script in
Terminal. It is not signed with an Apple Developer ID, so macOS asks you to allow it once
(**Open Anyway**). See [Install on a Mac](install.md#install-on-a-mac).

### Bootstrap script

The one-line installer (`scripts/install.sh` for macOS and Linux, `scripts/install.ps1` for Windows). It installs uv, Python 3.12 and the pinned gateway as a uv tool, registers and starts it, and opens the console with a claim link. See [Install](install.md#advanced-what-the-installer-does).

### Release pins

The exact (`==`) versions a given `abstractframework` release installs, exposed as `RELEASE_VERSIONS` and checked by `abstractframework doctor`. The npm apps and crates released with it are listed in `NPM_RELEASE_VERSIONS` and `CRATE_RELEASE_VERSIONS`.

---

## Memory

### Active context

The current message view sent to the model (what the LLM "sees"). A derived view that can be compacted without losing underlying history.

### Stored history

The durable record of what happened (ledger + artifacts). The source of truth.

### Knowledge graph (KG) memory

Long-term memory provided by AbstractMemory: append-only temporal triples, and on top of them a usage-weighted memory graph with a journal (recall, formation, consolidation). Predicates and entity types come from the shared semantics registry (AbstractSemantics). AbstractRuntime and AbstractGateway depend on AbstractMemory.

--- docs/faq.md ---
# FAQ

## What is AbstractFramework?

An open-source ecosystem for building **durable, observable, multimodal AI systems**.

Two things share the name:

- **The ecosystem**: composable packages (Core, Runtime, Agent, Gateway, Flow, Observer, apps, modality plugins).
- **This repo / meta-package**: `abstractframework` is a pinned install profile + cross-package docs.

If you're looking for the main SDK, that's **AbstractCore**. If you need durable orchestration, that's **AbstractGateway**.

---

## Where should I start?

- **AbstractCore** — when you want direct LLM/tool/media integration with a clean, provider-agnostic API (Python SDK **or** OpenAI-compatible `/v1`).
- **AbstractGateway** — when you need durability, orchestration, scheduling, or language-agnostic access via HTTP/SSE routes.

Most teams start with Core (SDK or `/v1`), then introduce Gateway when workflows become long-running, scheduled, or shared across clients. See **[Getting Started](getting-started.md)**.

---

## Do I need to install the whole stack?

No.

| Goal | Install |
|---|---|
| A running gateway + web console, no Python setup | the installer (a Mac package, or one line) in [Install](install.md) |
| Smallest useful (LLM SDK only) | `pip install abstractcore` |
| Gateway-first deployment | `pip install abstractgateway` |
| Everything at compatible versions | `pip install abstractframework` |
| A browser app against an existing gateway | `npx @abstractframework/<flow\|code\|observer\|continuum\|entity>` |
| A terminal client | `cargo install abstractcode`, `cargo install abstractgateway-console` or `cargo install abstractcore-console` |
| A container deployment | `ghcr.io/lpalbou/abstractgateway:0.4.3` |

See [Install AbstractFramework](install.md) for the Light / Apple / GPU chooser. Light is
remote-first, not reduced-functionality: multimodal and embeddings still work through remote or
local endpoint providers.

---

## Can I run everything offline with local models?

Yes. The core execution stack works fully offline with local model servers (Ollama, LM Studio, vLLM, llama.cpp, LocalAI). You need internet only to download models or use cloud APIs.

Multimodal plugins also work offline:

- **AbstractVoice**: Piper TTS (ONNX) + faster-whisper STT — prefetch once, run offline.
- **AbstractVision**: local Diffusers models or GGUF via stable-diffusion.cpp.
- **AbstractMusic**: local ACE-Step inference.

---

## What agent patterns are available?

AbstractAgent ships three patterns, all built on the durable Runtime kernel:

| Pattern | How it works |
|---|---|
| **ReAct** | Tool-first reasoning: observe → think → choose tool → execute → reflect |
| **CodeAct** | Code execution: generate Python, run it, observe output |
| **MemAct** | Memory-enhanced: reads/writes a knowledge graph during the loop |

Agents can run standalone or as nodes inside an AbstractFlow workflow.

---

## How does AbstractFramework compare to other frameworks?

### vs direct provider SDKs (OpenAI, Anthropic)

Direct SDKs are fine when you only use one provider and don't need durable orchestration.

AbstractCore adds value when you need: provider portability (local ↔ cloud), consistent tool/structured-output behavior across backends, media policies, modality plugins, or a configuration layer that doesn't leak into app code.

### vs LangChain / LlamaIndex / PydanticAI

Most agent libraries are **in-process orchestration**. AbstractFramework is a **durable orchestration stack**.

**Where AbstractFramework is stronger**: durability and pause/resume as primitives, replay-first observability, portable `.flow` bundles that run across clients.

**Where others are stronger**: large connector/RAG ecosystems, minimal boilerplate for simple use cases, broader community examples.

### vs Temporal / Step Functions / job schedulers

AbstractGateway is architecturally closer to these, but specialized for LLM/tool loops: tool approval waits, AI-oriented artifacts, and replay-first thin-client UIs over HTTP/SSE.

### Can I use AbstractFramework with LangChain/LlamaIndex?

Yes. Use AbstractFramework for orchestration and durability; integrate other libraries as tools or subflows:

- Use LlamaIndex retrievers as tools within an AbstractAgent
- Wrap LangChain chains as tool executors
- Let AbstractRuntime handle durability while external libraries handle specific capabilities

---

## What is a ".flow bundle" and why does it matter?

A `.flow` file is the portable distribution unit for workflows. It packages:

- A VisualFlow workflow graph
- Metadata (entry points, interfaces)
- Optional subflows and assets

Deploy a bundle to a gateway and any gateway-backed client can discover and run it.

---

## How do I author complex agentic orchestration?

1. Run a gateway (for durability + discovery).
2. Open the Flow Editor (`npx @abstractframework/flow`) and connect to the gateway.
3. Build a workflow: LLM steps, tool steps, agent nodes, branching, loops, subflows.
4. Export to `.flow`.
5. Copy into `ABSTRACTGATEWAY_FLOWS_DIR` to deploy.

To make it reusable across clients, implement an **interface contract** (for example `abstractcode.agent.v1`).

See **[Getting Started](getting-started.md)** → "Author orchestration with AbstractFlow".

---

## How do I monitor and schedule agentic work?

- **Monitoring**: use **AbstractObserver** — replay ledger, stream live execution, inspect errors, control runs.
- **Scheduling**: durable schedules are owned by the **gateway** (survive restarts). Create them from a client UI or via the gateway scheduling API.

See **[Getting Started](getting-started.md)** → "Gateway-first" section.

---

## How does multimodality work?

AbstractCore supports modalities via **capability plugins** (installed separately, discovered via entry points):

| Plugin | Capability |
|---|---|
| `abstractvoice` | `llm.voice` (TTS) / `llm.audio` (STT) |
| `abstractvision` | `llm.vision` (image generation) |
| `abstractmusic` | `llm.music` (text-to-music) |

Plugins are configured on the machine that actually executes (local app host or gateway host). Don't install a plugin; Core stays lightweight.

---

## Where is data stored?

- **Gateway**: `ABSTRACTGATEWAY_DATA_DIR` is the durability root (runs, ledger, artifacts, schedules). The bootstrap scripts set it to `~/Library/Application Support/AbstractGateway` (macOS), `~/.local/share/abstractgateway` (Linux) or `%LOCALAPPDATA%\AbstractGateway` (Windows).
- **Core config**: `~/.abstractcore/config/` (persisted by `abstractcore --config`).
- **Local apps**: typically `~/.abstractcode/`, `~/.abstractassistant/`, etc.

If you care about auditability and long-lived workflows, back up the gateway data directory.

---

## Installing with the one-line script

### Why does macOS ask me to allow the installer?

`AbstractFramework-Installer.pkg` is not signed with an Apple Developer ID, so macOS blocks the
first double-click. Open **System Settings > Privacy & Security** and click **Open Anyway** next to
the installer's name, once per download. The one-line install in Terminal does not show this
step. See [Install on a Mac](install.md#install-on-a-mac).

### Can other devices on my network use the gateway?

Yes, when you choose it. The gateway listens on this computer only until you change its Network
setting (`abstractgateway network set lan`, the console, or the menu-bar icon). See
[Network setting](install.md#network-setting-who-can-reach-the-gateway).

### Why is `abstractgateway` not found after the install?

The commands live in `~/.local/bin` (`%USERPROFILE%\.local\bin` on Windows). The script runs
`uv tool update-shell` once to add that directory to your PATH, which applies to **new**
terminals. Open a new terminal, or call `~/.local/bin/abstractgateway` directly. Pass
`--no-modify-path` if you manage PATH yourself.

### Which port does the gateway use?

`8080` by default. When something else already listens there (for example `llama-server` or
`mlx_lm.server`, which also default to 8080), the script picks the next free port, remembers it
for later runs, and prints the console URL. Choose one with `--port N` (`-Port N`).

### Will it ask for my password or admin rights?

Not for the default install: uv, Python, the gateway and Node (`nodejs-wheel`) install in your
user account. Only optional vendor installers may: Ollama on Linux uses sudo (it installs a system
service), Ollama on macOS may ask to link `/usr/local/bin/ollama`, and LM Studio on Linux may ask
to install `libatomic1`. The script tells you before running them.

### Do I need Xcode or a C compiler?

No. The default install uses prebuilt wheels only. If macOS shows an "install the command line
developer tools" prompt, cancel it and re-run the one-liner. llama.cpp GGUF models come from upstream's prebuilt wheel on Apple
Silicon, Linux and Windows x64 ([llama.cpp GGUF models](install.md#llamacpp-gguf-models)). You
need a compiler only for `--full`, which adds stable-diffusion.cpp and echo cancellation, and
llama.cpp on machines without a prebuilt wheel ([Compiled extras](install.md#compiled-extras)).

### Windows says scripts are disabled on this system

Pasting the one-liner works under the default `Restricted` policy because
`powershell -ExecutionPolicy ByPass -c "irm … | iex"` runs a command, not a script file, and the
bypass applies to that process only. If your organization sets the policy through Group Policy
(`Get-ExecutionPolicy -List` shows `MachinePolicy` or `UserPolicy`), a saved `install.ps1` will not
run; use the one-liner or ask your administrator. The installer reports this during preflight.

### The sign-in link expired or I closed the tab

The one-time link is valid for 10 minutes and works only from the same machine. Mint a new one with
`abstractgateway claim --open` (or `abstractgateway-config claim-url`). The admin token also stays
in `<data dir>/auth/bootstrap-admin-token`; the install summary prints that path.

### How do I stop or restart the gateway?

When the script registered the login service, `abstractgateway service status` shows it,
`abstractgateway service uninstall` stops the gateway and removes the login entry (your data is
kept), and `abstractgateway service install --port 8080` registers and starts it again. With `--no-service`, the install summary prints the stop and start commands; re-running the
installer starts it again.

### How do I download a model or install Ollama later?

Open the console's **Engines** tab (detect, install with the exact command shown first) and
**Models** tab (models that fit this machine, download, delete). From a terminal:
`abstractgateway engines status|install` and `abstractgateway models catalog|download|delete`.
Engine installs from the console run on the gateway host and are enabled by default only when the
gateway listens on loopback.

### How do I see what the script will do before running it?

Add `--print` (Windows: `-Print` or `-WhatIf`). It runs the read-only checks and prints every
command without changing anything.

### How do I remove it?

Re-run the script with `--uninstall` (`-Uninstall`). Add `--purge` (`-Purge`) to delete the gateway
data as well. See [Install](install.md#upgrade-and-uninstall).

---

## Something does not work

Symptoms, checks and fixes (provider calls failing, a browser app that cannot connect, missing
voice models, install errors) are in **[Troubleshooting](troubleshooting.md)**.

--- docs/troubleshooting.md ---
# Troubleshooting

Symptoms you may meet when installing or running AbstractFramework, how to confirm the cause, and
how to fix it. Installation steps are in [Install](install.md); concepts and limits are in the
[FAQ](faq.md); package-specific problems are covered in each component's own documentation.

## First checks

Start with the read-only health report:

```bash
uvx abstractframework doctor        # or `abstractframework doctor` inside a framework venv
curl -sS http://127.0.0.1:8080/api/health
```

`doctor` checks Python, the pinned package versions, uv, Node.js, free disk, the gateway
(`ABSTRACTGATEWAY_URL`, default `http://127.0.0.1:8080`) and whether Ollama and LM Studio answer.
It changes nothing. See [API](api.md#abstractframework-doctor) for its options.

Logs:

| What | macOS | Linux | Windows |
|---|---|---|---|
| Install log | `~/Library/Application Support/AbstractGateway/logs/install-*.log` | `~/.local/share/abstractgateway/logs/install-*.log` | `%LOCALAPPDATA%\AbstractGateway\logs\install-*.log` |
| Gateway started at login | `~/Library/Logs/AbstractGateway/gateway.out.log`, `gateway.err.log` | `journalctl --user -u abstractgateway` and `<data>/logs/` | `<data>\logs\gateway.err.log` |
| Gateway started in the background | `<data>/logs/gateway.log` | `<data>/logs/gateway.log` | `<data>\logs\gateway.err.log` |

`<data>` is the gateway data directory; all locations are listed in
[Operations and support](installers/operations-and-support.md#locations).

## Installing

### macOS will not open `AbstractFramework-Installer.pkg`

- **Cause**: the package is not signed with an Apple Developer ID, so Gatekeeper blocks the first
  double-click of a downloaded copy.
- **Fix**: close the warning, open **System Settings > Privacy & Security**, click **Open Anyway**
  next to the installer's name, and confirm. The **Open Anyway** button appears after you have
  tried to open the file once.
- **Alternative**: paste the one-line install in Terminal; it is not subject to this step.
- **Verify**: the macOS Installer opens and then a Terminal window shows the install steps.

### The installer stopped with a message

Every stop names its cause and the next step. The full list of messages and what to do is the
table in [If something goes wrong](install.md#if-something-goes-wrong). After fixing the cause,
run the installer again: it continues where it stopped.

### macOS asks to install the command line developer tools

- **Cause**: something tried to compile a package. The installer itself uses prebuilt wheels only.
- **Fix**: cancel the prompt, fetch the script again with the one-liner in
  [Install](install.md#or-paste-one-line-in-terminal), and re-run it.
- You need a compiler only for `--full` ([Compiled extras](install.md#compiled-extras)) or for a
  plain `pip install` of the `apple` / `gpu` profiles.

### `abstractgateway: command not found`

- **Cause**: the commands live in `~/.local/bin` (`%USERPROFILE%\.local\bin` on Windows), which is
  added to your PATH for **new** terminals.
- **Fix**: open a new terminal, or call `~/.local/bin/abstractgateway` directly.
- **Verify**: `abstractgateway --version`.

### Windows says scripts are disabled on this system

- **Cause**: an execution policy set by Group Policy (`Get-ExecutionPolicy -List` shows
  `MachinePolicy` or `UserPolicy`) blocks saved `.ps1` files.
- **Fix**: paste the one-liner from [Install](install.md#windows), which runs a command rather than
  a script file, or ask your administrator.

## Starting and signing in

### The browser page asks for a token, or the sign-in link expired

- **Cause**: the one-time link (`/console#claim=…`) is single use, valid for 10 minutes and works
  only on the gateway's computer.
- **Fix**: `abstractgateway claim --open` opens a fresh link (or `abstractgateway-config claim-url`
  prints one). Re-running the installer also opens one.
- **Alternative**: sign in with the `admin` token stored in `<data>/auth/bootstrap-admin-token`.

### The gateway does not answer

- **Check**: `curl -sS http://127.0.0.1:8080/api/health` and, for a login item,
  `abstractgateway service status`.
- **Cause**: the first start loads the local engines and can take a few minutes; a gateway that
  exits during start writes the reason to its log (see [First checks](#first-checks)).
- **Fix**: wait for the first start to finish, restart the computer (the login item starts the
  gateway), or run the installer again, which repairs an interrupted install.
- **Verify**: `/api/health` answers and `http://127.0.0.1:8080/console` opens.

### The gateway is on another port

- **Cause**: port 8080 was taken (for example by `llama-server` or `mlx_lm.server`), so the
  installer used the next free port and remembered it.
- **Fix**: use the URL the install summary prints, or choose a port with `--port N` (Windows:
  `-Port N`) and re-run. `abstractgateway network status` shows the running address.

### No gateway after a restart

- **Cause**: the gateway starts at login only when a login item was registered (answering "no" to
  the start-at-login question, or `--no-service`, skips it).
- **Fix**: `abstractgateway service install --port 8080`, or re-run the installer and answer yes.

## Connecting clients

### Another device cannot reach the gateway

- **Cause**: the gateway listens on this computer only until you change its Network setting.
- **Fix**: `abstractgateway network set lan`, then apply it (`abstractgateway network restart`,
  the console, or the menu-bar icon). `abstractgateway network addresses` lists the URLs to use.
  See [Network setting](install.md#network-setting-who-can-reach-the-gateway).

### A browser app (Observer, Flow, Code Web) cannot connect

- **Check**: the gateway URL in the app, then `curl http://127.0.0.1:8080/api/health`.
- **Fix**: sign in with a Gateway user and that user's token (the `admin` token file on a fresh
  install), not the legacy `ABSTRACTGATEWAY_AUTH_TOKEN`. For apps served from another origin,
  include that origin in `ABSTRACTGATEWAY_ALLOWED_ORIGINS` (local development:
  `http://localhost:*,http://127.0.0.1:*`) or `abstractgateway network set --allowed-origins …`.
- See [Configuration](configuration.md#client-configuration-observer--flow-editor--code-web-ui).

## Models and providers

### Provider calls fail

- **Check**: `abstractcore --status` for the persisted configuration, and whether a local server
  (Ollama, LM Studio) is running. The console's **Engines** tab and `abstractframework doctor` show
  which engines answer.
- **Fix**: set the provider's environment variables or configure it in a console
  ([Configuration](configuration.md)); start the local server.

### Voice models are not found

- **Cause**: voice models download on demand and are not part of the install.
- **Fix**: prefetch them once, then they work offline:

  ```bash
  abstractvoice-prefetch --stt small --piper en
  ```

## Reporting a problem

When a step keeps failing, open an issue at
[github.com/lpalbou/AbstractFramework/issues](https://github.com/lpalbou/AbstractFramework/issues)
with the output of `abstractframework doctor --json` and the install log named in the error
message. Report security problems privately as described in [SECURITY.md](../SECURITY.md).

--- docs/api.md ---
# API (meta-package)

This page documents the API exported by `abstractframework`, the meta-package shipped by this repository.

`abstractframework` is a **pinned distribution profile** plus a few lightweight helpers. AbstractFramework has two entrypoints: **AbstractCore** (LLM SDK + optional OpenAI-compatible `/v1` server) and **AbstractGateway** (durable run control plane over HTTP/SSE). Most functional APIs live in component packages — especially **AbstractCore** for the LLM SDK.

---

## Install

Full pinned ecosystem:

```bash
pip install abstractframework
```

Only the LLM SDK:

```bash
pip install abstractcore
```

Hardware-specific profiles (native installs, not Docker):

```bash
pip install "abstractframework[apple]"       # Apple Silicon native stack (MLX/Metal)
pip install "abstractframework[gpu]"         # GPU native stack (CUDA/ROCm)
```

See [Install AbstractFramework](install.md) for the profile chooser and first health checks.

---

## Convenience re-exports

`abstractframework` re-exports two common AbstractCore entry points so simple scripts can `from abstractframework import ...` without a separate `abstractcore` import.

### `create_llm`

```python
from abstractframework import create_llm

llm = create_llm("ollama", model="qwen3:4b-instruct")
resp = llm.generate("hello")
print(resp.content)
```

### `GenerateResponse`

The response type returned by `llm.generate(...)`.

---

## Release profile helpers

### `__version__`

The meta-package version (`0.3.2` for this release).

### `RELEASE_VERSIONS`

Dictionary mapping each ecosystem package name to the pinned version for this release. In 0.3.2:
`abstractcore` 2.15.2, `abstractruntime` 0.4.35, `abstractagent` 0.3.13, `abstractgateway` 0.4.3,
`abstractmemory` 0.3.0, `abstractsemantics` 0.0.5, `abstractvoice` 0.11.4, `abstractvision` 0.3.29,
`abstractmusic` 0.1.15, `abstractassistant` 0.5.0.

### `PACKAGE_DISTRIBUTIONS`

Maps each package name in `RELEASE_VERSIONS` to its PyPI distribution name (for example
`abstractruntime` → `AbstractRuntime`).

### `NPM_RELEASE_VERSIONS`

The npm apps released with this version, each runnable with `npx <package>`:
`@abstractframework/flow` 0.3.20, `@abstractframework/code` 0.4.2,
`@abstractframework/observer` 0.1.12, `@abstractframework/continuum` 0.3.1 and
`@abstractframework/entity` 0.2.1. They also appear as `npm_apps` in the install manifest.

### `CRATE_RELEASE_VERSIONS`

The Rust terminal tools released with this version, installed with `cargo install <crate>`:
`abstractgateway-console` 0.8.0, `abstractcore-console` 0.2.0, `abstractcode` 0.5.1 and the
`abstracttui` engine 0.6.0. The bootstrap scripts install `abstractgateway-console` and
`abstractcode` at these versions with `--with-console` and `--with-code-cli`.

### `CORE_DEFAULT_EXTRAS`

List of AbstractCore extras implied by the default framework install profile (remote-first): `remote`, `tools`, `media`, `vision`, `voice`, `audio`, `music`.

### `get_release_profile()`

Returns the full pinned profile metadata as a dict.

```python
from abstractframework import get_release_profile

profile = get_release_profile()
print(profile["abstractframework"])        # meta-package version
print(profile["packages"]["abstractcore"]) # pinned Core version
print(profile["crates"])                   # CRATE_RELEASE_VERSIONS
```

### `get_installed_packages()`

Returns a dict of installed AbstractFramework package versions detected in the current environment.

```python
from abstractframework import get_installed_packages
print(get_installed_packages())
```

### `print_status()`

Prints a human-readable status report of detected packages (installed vs missing).

```python
from abstractframework import print_status
print_status()
```

### `abstractframework doctor`

Checks the Python version (3.10–3.13), pinned package versions, the Apple/GPU profile
prerequisites (macOS 14+ on Apple Silicon, `nvidia-smi` / `rocminfo`), uv, Node.js 18+ (system or
`nodejs-wheel`), free disk, and, over read-only GET requests, the gateway health
(`ABSTRACTGATEWAY_URL`, default `http://127.0.0.1:8080`), `abstractgateway-config status --json`,
and whether Ollama and LM Studio are reachable. It does not import heavy local inference stacks.
Checks report `ok`, `warn`, `error` or `info` (`info` never fails the run).

```bash
abstractframework doctor
abstractframework doctor --json          # schema abstractframework_doctor_v2
abstractframework doctor --no-network    # skip the gateway / engine probes
abstractframework doctor --no-environment  # only check the Python package profile
abstractframework doctor --timeout 5     # probe timeout in seconds (default 2)
```

### `abstractframework manifest`

Prints or validates the installer-facing manifest generated from the root release profile.

```bash
abstractframework manifest                                         # print it
abstractframework manifest --check docs/installers/install-manifest.json   # compare a file with it
abstractframework manifest --write install-manifest.json           # write it to a file
```

Field reference: [release-and-manifest.md](installers/release-and-manifest.md).

---

## Where to find the functional APIs

| What you need | Package |
|---|---|
| LLM calls, tools, structured output, media, embeddings, MCP | `abstractcore` |
| Durable execution kernel (runs, ledger, effects, waits) | `abstractruntime` |
| Agent patterns (ReAct, CodeAct, MemAct) | `abstractagent` |
| Control plane (HTTP server, scheduling, bundle discovery, SSE) | `abstractgateway` |
| Workflow authoring UI | `@abstractframework/flow` (npm) |
| Monitoring / operations UI | `@abstractframework/observer` (npm) |
| Coding client | `abstractcode` (crates.io) and `@abstractframework/code` (npm) |
| Gateway operator console | built-in `/console`, and `abstractgateway-console` (crates.io) |
| Local models and engines (catalog, fit, download, delete, engine installs) | `abstractcore` (`abstractcore models`, `abstractcore engines`, `/acore/*`), mirrored by `abstractgateway` (`/api/gateway/models`, `/engines`, `/jobs`) |
| AbstractCore consoles | built-in `/console` of `abstractcore serve`, and `abstractcore-console` (crates.io) |
| Continuous development console | `@abstractframework/continuum` (npm) |
| Summoned-entity manager | `@abstractframework/entity` (npm) |

See **[Getting Started](getting-started.md)** for the two entry points and a first end-to-end run,
**[Architecture](architecture.md)** for how the packages connect, and
**[Troubleshooting](troubleshooting.md)** when `doctor` reports a problem.

Gateway-hosted workflow APIs distinguish private runtime bundles from the
shared workflow catalog:

- `/api/gateway/bundles` remains the caller runtime's private bundle surface.
- `/api/gateway/workflow-catalog` lists catalog workflows visible to the signed
  in principal.
- `/api/gateway/admin/workflow-catalog/*` is admin-only for immutable catalog
  upload/promote/default/ACL/status operations.
- `/api/gateway/runs/start` and `/api/gateway/runs/schedule` accept
  `registry_scope: "tenant_catalog"` to start a catalog workflow in the
  requesting user's runtime.
- Catalog scope is explicit. Without `registry_scope`, Gateway starts only
  private runtime bundles. Catalog flow/schema inspection uses ACL-aware
  `/api/gateway/workflow-catalog/{bundle_id}/versions/{version}/flows/{flow_id}`
  routes.

Gateway-hosted user administration keeps retained runtime data explicit:

- `/api/gateway/admin/users` is the admin-only user list/create/read/update/delete
  surface.
- `/api/gateway/admin/runtime-reservations` lists retained runtime reservations
  left by deleted or reassigned users.
- `/api/gateway/admin/runtime-reservations/{runtime_id}/transfer` intentionally
  assigns retained runtime data to an existing same-tenant user.
- `/api/gateway/admin/runtime-reservations/{runtime_id}/purge` requires exact
  runtime-id confirmation, deletes the retained runtime directory, then releases
  the runtime id for reuse.

Gateway-hosted provider endpoint profiles make reusable hosted endpoints
discoverable without exposing raw keys:

- `/api/gateway/config/provider-endpoint-profiles` lists and creates profiles
  for the current principal.
- `/api/gateway/config/provider-endpoint-profiles/discover-models` previews the
  model list for a draft or saved profile by calling the configured provider
  family and base URL with the server-side or entered key. The response never
  echoes the raw key.
- `/api/gateway/config/provider-endpoint-profiles/{profile_id}` updates or
  deletes an existing profile. Gateway-scoped profiles require an admin
  principal.
- `/api/gateway/discovery/providers` includes enabled profiles as virtual
  providers such as `endpoint:office-vllm`.
- `/api/gateway/discovery/providers/{provider_name}/models` resolves virtual
  providers through the stored profile and returns the allowed or discovered
  model list without returning the raw API key.

--- docs/guide/runtime-artifacts.md ---
# Runtime artifacts and retrieval

This guide explains how to investigate runtime resources without conflating
artifacts, ledgers, provider traces, and semantic memory.

## Responsibility map

| Layer | Responsibility |
|---|---|
| AbstractRuntime | Stores runs, ledgers, waits, artifacts, artifact descriptors, media facts, and access stats. Runtime is the source of truth for artifact identity and canonical descriptors. |
| AbstractGateway | Exposes Runtime resources over HTTP, applies auth/RBAC, projects `artifact_envelope_v1`, provides exact stats/facets/paging, and adds action links where available. |
| AbstractObserver | Visualizes Gateway data. Observe explains workflow narratives from ledger records; Runtime Activity supervises runs; Artifact Explorer inventories artifacts. |
| AbstractMemory / KG | Stores semantic memory and knowledge graph records. It is for concept/entity/relationship retrieval, not byte-level artifact inventory. |
| AbstractSemantics | Validates semantic predicates and schema-level meaning where KG/memory data is used. |

## Artifact vs local/server file sources

Use these terms consistently when a hosted client works with files:

- `Artifact`: the durable Runtime-owned file payload. This is what Artifact
  Explorer inventories.
- `Local File`: a client-device source. In hosted/browser mode it is uploaded
  and stored as an Artifact before durable execution.
- `Server File`: a user-facing label for a file inside Gateway-approved
  workspace scope. For artifact-style inputs it may be imported into a new
  Artifact; for path-based operations it remains a workspace-scoped server path
  whose availability depends on current Gateway policy and grants.

This guide is about Artifacts. A server workspace file that has not been
imported or produced as an Artifact will not appear in Artifact Explorer.

## Which surface to use

Use **Observe** when the question is about a workflow: what was requested, what
steps ran, what is waiting, what failed, what subworkflows exist, and what the
final outcome was. Observe replays the run ledger and can stream updates.

Use **Runtime Activity** when the question is operational: what is running,
waiting, failed, scheduled, stale, or needs a human action. Activity rows expose
open Observe, open ledger, open artifacts, open logs, copy ids, and cancel/stop
actions where Gateway authorizes them. Queue counts are for the loaded run page
unless Gateway exposes a broader run-stats endpoint.

Use **Artifact Explorer** when the question is about outputs: images, markdown,
HTML, JSON, voice, music, transcripts, documents, workflow snapshots, and other
files. Artifact Explorer uses Gateway artifact search with exact stats/facets
when available and pages the result set.

If the question is “what files are available in the server workspace for this
run?”, use workspace/file-helper surfaces instead. Artifact search only answers
questions about stored artifacts.

Use **Mindmap/KG** when the question is semantic: entities, relationships,
notes, memories, and graph-level knowledge. Do not use artifact metadata search
as a substitute for KG retrieval.

Use **Gateway audit/provider links** for host-level request traces. Audit tails
are global system activity unless an artifact envelope or ledger record links a
trace to a specific run/artifact.
Gateway artifact envelopes only expose safe relative Gateway/UI action links;
external provider URLs should be converted into Gateway-owned trace records or
redacted trace artifacts before users open them.

## Artifact search contract

Gateway artifact search is the canonical thin-client query path:

```bash
curl -sS -H "$AUTH" \
  "$BASE_URL/api/gateway/artifacts/search?scope=all&artifact_kind=music,voice&include_stats=true&limit=500"
```

Useful filters include `scope`, `session_id`, `run_id`, `artifact_kind`,
`semantic_kind`, `render_kind`, `modality`, `content_type`, `workflow_id`,
`node_id`, `created_after`, `created_before`, `query`, and `tags`.

Use `include_stats=true` when a UI needs exact totals, byte totals, or facet
counts. The stats are independent of the current page limit. Use `limit`,
`offset`, and cursors for bounded pages.

`artifact_kind` is UI-oriented. Canonical filters should prefer descriptor
fields:

- `semantic_kind=music` for generated music.
- `semantic_kind=voice` for TTS/voice artifacts.
- `render_kind=markdown` for Markdown rendering.
- `render_kind=html` for HTML source rendering and full-page preview.
- Generic `audio` should be treated as unclassified audio, not as voice or
  music unless the descriptor says so.

## Provenance and descriptors

Runtime-owned artifact descriptors are the reliable source for artifact meaning.
For descriptor-aware generated media, the envelope may include:

- producer package, capability route, provider, model, backend;
- prompt or TTS text, requested format, redacted parameters, output index;
- source artifacts used for edits, reference media, image-to-video, or other
  derivations;
- media facts such as dimensions, sample rate, channels, duration, and frame
  counts;
- links back to run, workflow, node, turn, ledger cursor, and provider trace
  availability.

Missing descriptor fields mean the producer did not record that fact or the
artifact is legacy. Consumers should show that absence plainly.

## Direct Runtime inspection

When investigating from the filesystem, start with the Gateway data directory
or runtime root configured by `ABSTRACTGATEWAY_DATA_DIR`. Artifact bytes,
metadata, and catalog state are Runtime-owned. Prefer Gateway APIs when the
server is running because Gateway applies authorization and records access
actions for previews/downloads.

For package-level details, see:

- `abstractruntime/docs/artifacts.md`
- `abstractruntime/docs/api.md#artifacts-store-by-reference`
- `abstractgateway/docs/api.md#artifacts-and-filesystem-handoff`
- `abstractobserver/docs/architecture.md#runtime-boundary`

## Privacy and safety

Artifact metadata can include prompts, source refs, provider/model ids, and
bounded generation parameters. Do not dump raw provider payloads, secrets, or
large artifact contents in summaries. Prefer ids, counts, descriptors, and
redacted snippets unless the user explicitly asks to inspect a non-image
artifact's content.

--- docs/guide/README.md ---
# Guides

Short, focused guides for common framework questions.

- [Agent vs LLM Call (VisualFlow)](agent-vs-llm.md)
- [Capability plugins (voice/audio/vision)](capability-plugins.md)
- [Capability routing defaults](capability-routing-defaults.md)
- [Deployment topologies](deployment-topologies.md)
- [Web deployment (browser UI + gateway)](deployment-web.md)
- [iPhone notes (Safari / PWA)](deployment-iphone.md)
- [Gateway exposure security](gateway-security.md)
- [Runtime scope (run/session/global/all)](runtime-scope.md)
- [Runtime artifacts and retrieval](runtime-artifacts.md)
- [Flow + KG memory (memory object)](flow-and-kg-memory.md)
- [Scheduled workflows (durable jobs)](scheduled-workflows.md)
- [Prompt caching (prompt/KV)](prompt-caching.md)
- [Agent Skills (SKILL.md) — proposal](agent-skills.md)
- [WorkflowBundles (`.flow`) lifecycle](workflow-bundles.md)
- [Telegram integration](telegram-integration.md)
- [Email integration](email-integration.md)
- [Process manager env vars (write-only)](process-manager-env-vars.md)

--- docs/guide/agent-vs-llm.md ---
# Agent vs LLM Call (VisualFlow)

VisualFlow has two LLM-oriented nodes that intentionally look similar, but differ in **autonomy**.

## When to use which

### Use **LLM Call**

- You want a **single** LLM request/response step (no internal loop).
- You want to explicitly wire tool execution in the graph:
  - `LLM Call.tool_calls` -> `Tool Calls.tool_calls` -> (your next node).

### Use **Agent**

- You want the node to run an **internal multi-step loop** (ReAct-style) until it finishes or hits a cap.
- You want a runtime-owned **scratchpad** (trace/transcript) for observability.

## Inputs (shared contract)

Agent and LLM Call share the same parameter set and ordering (Agent has one extra cap: `max_iterations`):

1. `use_context` (boolean): include the run's active context messages (`context.messages`) in the request.
2. `context` (object): explicit context override. When provided, `context.messages` overrides inherited run context
   messages.
3. `provider` (provider), `model` (model): route the call.
4. `system` (string): optional system instructions for this node.
5. `prompt` (string): the user prompt/content for this node.
6. `tools` (tools): allowlist of tools the model may request (execution is still explicit in the graph).
7. Agent-only: `max_iterations` (number): maximum internal loop iterations (safety cap).
8. `max_in_tokens` (number): optional per-call/per-agent input token budget (VisualFlow shorthand for
   `max_input_tokens`).
9. `temperature` (number), `seed` (number): sampling controls.
10. `resp_schema` (object): optional JSON Schema for schema-constrained responses.

Notes:
- The canonical prompt key/pin is always `prompt` (there is no `request` alias).

## Outputs (what differs)

### LLM Call outputs

- `response` (string), `success` (boolean), `meta` (object), `tool_calls` (array)

### Agent outputs

- `response` (string), `success` (boolean), `meta` (object), `scratchpad` (object)

There is no separate `result` output pin in the durable contract.

### Agent `scratchpad` (what it contains)

The Agent scratchpad is runtime-owned observability (it can be large). Common fields:

- `messages`: agent-internal transcript for the sub-run (ReAct loop)
- `task`: the agent prompt/task for this node
- `context_extra`: any extra fields passed in `context` besides `task`/`messages` (host-defined)
- `node_traces` / `steps`: structured per-node trace + flattened UI-friendly steps
- `tool_calls` / `tool_results`: best-effort extraction from the trace

## `resp_schema` (structured responses)

When `resp_schema` is provided:

- `response` is a JSON string matching the schema (so it stays a simple `string` pin).
- Use a JSON parser node if you want to treat it as an object downstream.

## RunnableFlow interface (for chat-like clients)

The RunnableFlow (v1) interface (id: `abstractcode.agent.v1`) is the host contract used by AbstractCode, AbstractObserver,
and similar clients:

- `On Flow Start` exposes the same parameter set (in the same order) so hosts can configure a workflow run.
- Required `On Flow Start` pins: `provider`, `model`, `prompt` (everything else optional).
- Required `On Flow End` pins: `response`, `success`, `meta` (others optional).


--- docs/guide/capability-plugins.md ---
# Capability plugins (Voice/Audio/Vision/Music)

This guide explains how to add optional audio/voice/vision capabilities to AbstractFramework without turning
`abstractcore` into a kitchen sink.

## Mental model (two concepts; don't mix them)

1. **LLM input modalities (AbstractCore)**
   - Attaching image/audio/video to an LLM call (`generate(..., media=[...])`) depends on the selected provider/model's
     input capabilities.

2. **Deterministic capabilities (plugins)**
   - STT/TTS and generative vision are deterministic APIs that can be used with or without an LLM call:
     - `core.voice` / `core.audio` (speech-to-text, text-to-speech) via `abstractvoice`
     - `core.vision` (text-to-image, image-to-image, ...) via `abstractvision`
     - `core.music` (text-to-music) via `abstractmusic`

This split keeps `abstractcore` lightweight by default.

## Library mode (Python)

### Install

```bash
pip install abstractcore
pip install abstractvoice      # enables core.voice + core.audio
pip install abstractvision     # enables core.vision
pip install abstractmusic      # enables core.music
```

### Discover what's available

```python
from abstractcore import create_llm

llm = create_llm("ollama", model="qwen3:4b-instruct")  # example; pick a provider/model you have access to
print(llm.capabilities.status())
```

Notes:
- Capabilities load lazily the first time you access `llm.capabilities` / `llm.voice` / `llm.audio` / `llm.vision` / `llm.music`.
- Missing plugins raise an actionable error (includes an install hint).

### Use voice/audio (STT/TTS)

```python
wav_bytes = llm.voice.tts("Hello from AbstractVoice", format="wav")
open("hello.wav", "wb").write(wav_bytes)

text = llm.audio.transcribe("speech.wav")
print(text)
```

### Use generative vision (T2I/I2I/...)

`core.vision` can use an OpenAI-compatible images backend (configured via `vision_base_url` / `ABSTRACTVISION_BASE_URL`).

```python
llm = create_llm(
    "openai",
    model="gpt-4o-mini",
    vision_base_url="http://localhost:8000/v1",  # any OpenAI-compatible images endpoint
)

png_bytes = llm.vision.t2i("a red square on white background")
open("out.png", "wb").write(png_bytes)
```

### Use music generation (T2M)

`core.music` is plugin-backed (like voice/vision). `abstractmusic` provides
**local in-process** generation, with **ACE-Step Official**
(`acestep-official`) as the recommended backend and Diffusers audio pipelines
as alternative backends.

```python
llm = create_llm(
    # Any provider/model works here. The LLM does *not* generate music audio.
    # Music generation is performed by the configured AbstractMusic backend.
    "ollama",
    model="qwen3:4b-instruct",
    music_backend="acestep-official",
    music_model_id="ACE-Step/Ace-Step1.5",
)

wav_bytes = llm.music.t2m("uplifting synthwave, 120bpm, catchy chorus", format="wav", duration_s=10.0)
open("out.wav", "wb").write(wav_bytes)
```

## Framework mode (gateway/runtime)

Install modality plugins on the durable host (the machine/process that runs the runtime + tool execution and imports
`abstractcore`), typically the AbstractGateway runner.

Thin clients (web, remote TUI) do not need `abstractvoice`/`abstractvision`/`abstractmusic` installed locally.

## Server mode (OpenAI-compatible `/v1`)

AbstractCore Server can optionally expose OpenAI-compatible endpoints by delegating to plugins:
- `/v1/images/*` (via `abstractvision`)
- `/v1/audio/*` (via the capability plugin layer, typically `abstractvoice`; plus `/v1/audio/music` when `abstractmusic` is installed)

These endpoints are interoperability-first. For durable artifact-backed outputs, prefer gateway/runtime + ArtifactStore.

--- docs/guide/capability-routing-defaults.md ---
# Capability Routing Defaults

Capability routing defaults define which provider/model/backend the framework should use when a
request does not provide an explicit route.

They are configuration, not residency. A route can be configured even when the provider does not
currently have that model loaded. Loaded-model state is reported separately by provider residency
endpoints and the AbstractFlow "Loaded models" view.

## Route Keys

Routes use:

```text
<kind>.<modality>[.<task>]
```

The optional task suffix is used only for generated-media defaults that need
different provider/model pairs inside one modality. Model capability metadata
stays broad unless a separate ADR changes it.

Route kinds:

- `input`: understanding or enrichment of request content.
- `output`: generation targets.
- `embedding`: vectorization for retrieval and indexes.
- `rerank`: ranking routes for a future reranker manager.

Core modalities:

- `text`
- `image`
- `video`
- `voice`
- `sound`
- `music`
- `scene3d`

Examples:

- `input.text`: canonical LLM route for text understanding and text generation.
- `input.image`: VLM or captioning fallback route for images when `input.text`
  is not vision-capable.
- `input.video`: native video or video-frame understanding fallback. When
  `input.text` is known to handle visual frames, this route may be reported as
  covered by `input.text`; unlike `input.image`, it remains overrideable.
- `input.voice`: speech-to-text fallback route for audio attachments.
- `input.sound`: non-speech audio understanding route. It is not used as a
  speech-to-text fallback.
- `output.text`: read-only derived view of `input.text`.
- `output.image.text_to_image`: text-to-image generation backend.
- `output.image.image_to_image`: image edit / image-to-image backend.
- `output.image.image_upscale`: image restoration / upscale backend.
- `output.video.text_to_video`: text-to-video generation backend.
- `output.video.image_to_video`: image-to-video generation backend.
- `output.voice`: TTS route.
- `output.sound`: sound effects / text-to-audio route.
- `output.music`: music generation route.
- `embedding.text`: text embedding model for semantic retrieval.
- `rerank.text`: reserved route for text reranking.

## Route Payload

Each route stores a small JSON-safe target:

```json
{
  "provider": "lmstudio",
  "model": "qwen/qwen3.6-35b-a3b",
  "base_url": "http://127.0.0.1:1234/v1",
  "reasoning": "medium",
  "options": {
    "voice": "M1"
  }
}
```

`provider`, `model`, and `base_url` are shared fields. `reasoning` is an optional non-secret
default for reasoning-capable text routes. `options` is provider/plugin-specific and can carry
values such as a voice, language, quality preset, or backend profile.

Secrets do not belong in route defaults. API keys remain provider credentials managed by
AbstractCore, Gateway deployment secrets, or the capability plugin.

## Configuration Ownership

AbstractCore owns the schema and persistence. Routes live in:

```text
~/.abstractcore/config/abstractcore.json
```

Gateway is the control plane:

- co-located Gateway reads/writes the local AbstractCore config;
- split Gateway proxies to the configured AbstractCore server;
- Gateway does not create a separate provider/model defaults file.

In split deployments, `base_url` is interpreted from the execution host that actually calls the
provider. A URL that works from the Core/Runtime host may not work from the browser or Gateway host.

Task-specific generated-media routes are the operator-facing configuration
surface. Core still accepts broad `output.image` and `output.video`
compatibility defaults for older configurations, but Gateway Console does not
display them because `output.image.text_to_image` and
`output.video.text_to_video` are the concrete defaults for those tasks.

## Configure From Core

Set the framework text default:

```bash
abstractcore config set-default input.text \
  --provider lmstudio \
  --model qwen/qwen3.6-35b-a3b \
  --reasoning medium
```

`output.text` is accepted as a compatibility alias, but Core persists it as
`input.text`. Use `abstractcore --set-global-default ...` only when you want the
older global-default helper, which now writes that same canonical route.

Configure `input.image` only as a fallback for text models that cannot accept
images. When AbstractCore's model-capability registry knows the `input.text`
model supports image input, Gateway and Core report `input.image` as covered by
`input.text` instead of as an independently editable route.

Configure `input.voice` when speech attachments should be transcribed before a
text model receives the request:

```bash
abstractcore config set-default input.voice \
  --provider faster-whisper \
  --model large-v3
```

Core does not silently use installed STT packages as a hidden fallback. If the
current text model cannot accept audio natively, `audio_policy=auto` needs this
`input.voice` route. `audio_policy=speech_to_text` remains available for
explicit per-call routing, but normal framework defaults should use the route.

Configure `input.sound` only for non-speech audio understanding such as sound
events, audio scenes, and SFX. This is not the same as STT: Whisper-style
transcription models belong under `input.voice`, while audio-language models
such as `qwen3-omni-30b-a3b-instruct`,
`qwen3-omni-30b-a3b-captioner`, `qwen2.5-omni-7b`, or
`qwen2-audio-7b-instruct` are better candidates when a provider can serve them.
`qwen/qwen3.6-35b-a3b` remains a text/image/video default candidate, not an
audio-understanding model.

Configure `input.video` when a text route should use a separate video/VLM
fallback instead of native video support or the `input.text` model's frame
support:

```bash
abstractcore config set-provider office-vlm \
  --family openai-compatible \
  --base-url https://vlm.example.com/v1 \
  --api-key $OFFICE_VLM_API_KEY \
  --description "Office vision endpoint"

abstractcore config set-default input.video \
  --provider endpoint:office-vlm \
  --model qwen2.5-vl-72b
```

If no native video route and no `input.video` default are available, Core
reports a configuration error instead of silently choosing an unrelated
vision/video backend.

Set one route directly:

```bash
abstractcore config set-default output.voice \
  --provider supertonic \
  --model supertonic-3 \
  --base-url http://127.0.0.1:5000/v1 \
  --option voice=M1
```

Configure text embeddings:

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

or through the embedding convenience commands:

```bash
abstractcore --set-embeddings-provider lmstudio
abstractcore --set-embeddings-model lmstudio:text-embedding-nomic-embed-text-v1.5
abstractcore --set-embeddings-base-url http://127.0.0.1:1234/v1
```

## Configure Through Gateway

Use Gateway when it is the operator/control-plane entry point:

```bash
abstractgateway-config defaults

abstractgateway-config set-default input.text \
  --provider lmstudio \
  --model qwen/qwen3.6-35b-a3b \
  --base-url http://127.0.0.1:1234/v1

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
```

With Gateway user auth enabled, the default command edits the Gateway baseline
Core config at `$ABSTRACTGATEWAY_DATA_DIR/config/abstractcore.json`. Target one
runtime explicitly with:

```bash
abstractgateway-config set-default input.text \
  --scope user \
  --tenant default \
  --user alice \
  --provider endpoint:alice-openai \
  --model gpt-4.1
```

Gateway still has deployment settings such as host, port, auth, store backend, and the Core server
URL/token it uses to reach the execution host. Those are Gateway internals, not framework model
defaults.

## AbstractFlow UI

AbstractFlow authoring surfaces treat blank provider/model pins as
`Auto (Gateway default)`. This is the preferred portable setting for LLM Call,
Agent, and generative media nodes because the saved workflow does not bake in a
deployment-specific provider/model.

Provider dropdowns include `Auto (Gateway default)` as the first option so a
user can switch back after pinning a provider.

The AbstractFlow Model Residency modal is loaded-state only:

- **Loaded models**: provider-reported runtime residency.

Changing a default route does not load a model. Loading/unloading is an operator action against the
provider/runtime residency surface. Configure capability defaults in Gateway
Console or with the Core/Gateway config CLIs.

## Related

- ADR: `docs/adr/0035-capability-routing-defaults.md`
- Gateway configuration: `abstractgateway/docs/configuration.md`
- Core configuration: `abstractcore/docs/centralized-config.md`

--- docs/guide/deployment-topologies.md ---
# Deployment Topologies (Supported Patterns)

AbstractFramework is easier to reason about if you think in roles, not packages:

- UI / host UX: renders progress, collects approvals (AbstractCode, browser UIs, custom apps)
- Orchestrator: durable state machine + stores (AbstractRuntime + stores)
- Agent logic: produces effects/steps (AbstractAgent patterns, flows)
- LLM gateway: provider abstraction + tool-call parsing (AbstractCore)
- Tool executors: side effects (local tools, MCP workers, sandboxes)

## Topology A: Single machine (local everything)

Best for local development and offline-first workflows.

- UI + runtime + tools run in one process (or one machine).
- You can still use file-backed stores for durability.

## Topology B: Local orchestration + remote inference

Best when you want local tool execution but a remote model (GPU box, cloud API, hosted vLLM).

- Runtime + tools stay local.
- AbstractCore routes LLM calls to a remote provider endpoint.

## Topology C: Remote tool execution (MCP-backed)

Best when tools must run near the target environment (servers, private networks, sandboxes).

- Runtime stays on the durable host.
- Tool calls are delegated to an MCP worker.

## Topology D: Thin client UI + remote durable host (Gateway-first)

Recommended for multi-device use and trusted multi-client use.

- The gateway host owns the durable runtime + stores and progresses runs.
- Thin clients render by replaying/streaming the ledger and act by submitting durable commands.
- Bundles (`.flow`) provide portable, discoverable specialized agents.
- Independent users should use Gateway user auth. Gateway routes each principal
  to a runtime/data plane and reserves retained runtime ids after deletion until
  an admin explicitly transfers or purges that runtime.

Container baseline:

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

Use `ghcr.io/lpalbou/abstractgateway:gpu-latest` only for explicit NVIDIA/GPU
deployments. Apple/MLX inference should run natively on macOS and be exposed to
the light Gateway container as an OpenAI-compatible endpoint.

## Topology E: Multi-host orchestration (planned/advanced)

Only needed when you want to distribute durable orchestration itself across multiple hosts.
For v0, prefer picking a host per run (avoid mid-run migration).

## See also

- [Scenario: Gateway-first local development](../scenarios/gateway-first-local-dev.md)
- [Guide: Gateway exposure security](gateway-security.md)

--- docs/guide/deployment-web.md ---
# Web Deployment (Browser UI + Gateway)

This guide covers deploying a browser UI (Observer / Flow Editor / Code Web UI) against an AbstractGateway.

## What "gateway-first" means

- The browser does not tick the runtime.
- It renders by replaying/streaming the ledger.
- It acts by sending durable commands (start/resume/pause/cancel/emit_event).

## Minimum gateway settings (browser access)

Set these on the gateway host:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
```

Start the gateway:

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

Gateway creates `default/admin` if needed and writes the first browser-login
token to `$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token`. Use that token
for `/console` and browser apps, then create named users or rotate tokens from
the console.

## Run the UIs

```bash
npx @abstractframework/observer
npx @abstractframework/flow
npx @abstractframework/code
```

Default ports:
- Observer: http://localhost:3001
- Code Web UI: http://localhost:3002
- Flow Editor: http://localhost:3003

In each UI, set:
- Gateway URL: `http://127.0.0.1:8080`
- User: the Gateway user id assigned by the Gateway admin
- Gateway token: that user's token, used only to create the browser session

For hosted deployments on a non-local UI hostname, configure the Gateway URL on
the UI server. Browser-supplied Gateway URL changes are rejected by Flow, Code
Web, and Observer unless the app-specific
`*_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG=1` override is enabled behind your own
access control. If the UI is behind a reverse proxy that rewrites `Host`, enable
`*_TRUST_PROXY_HEADERS=1` only after the proxy strips client-supplied forwarded
headers.

## Production notes (high-signal)

- Terminate TLS at a reverse proxy and forward to `127.0.0.1:8080`.
- Restrict `ABSTRACTGATEWAY_ALLOWED_ORIGINS` to exact UI origins (avoid broad wildcards).
- Keep the Gateway admin token secret and rotate it like any control-plane
  credential.
- Hosted browser apps should keep only their app-scoped Gateway session cookie;
  do not persist user bearer tokens in browser storage.
- Do not use one shared user token for independent users. Use Gateway
  per-principal routing so each user token maps to that user's runtime.

See [Guide: Gateway exposure security](gateway-security.md).

--- docs/guide/deployment-iphone.md ---
# iPhone Notes (Safari / PWA)

Code Web UI is designed to run on iPhone as a thin host UI that connects to a remote gateway deployment.

## Prereqs (recommended)

- Gateway reachable over HTTPS (reverse proxy + TLS).
- Web UI hosted over HTTPS.
- Gateway configured with:
  - `ABSTRACTGATEWAY_USER_AUTH=1` so each browser user signs in with a Gateway user token
  - `ABSTRACTGATEWAY_ALLOWED_ORIGINS` including your web UI origin (exact host recommended for prod)

## Steps

1. Open the web UI URL in Safari.
2. In Settings:
   - set Gateway URL (for example `https://gateway.example.com`)
   - sign in with your Gateway user id and user token
3. Optional: Safari -> Share -> Add to Home Screen.

## Constraints

- iOS suspends background tabs aggressively; durability depends on ledger replay, not "staying connected".
- File access is remote (via gateway); the phone does not run local tools in v1.

--- docs/guide/gateway-security.md ---
# Gateway Exposure Security (Checklist)

Treat `abstractgateway serve` as a control-plane service: it can access runs/ledgers/attachments, and (optionally) execute
maintenance actions depending on your deployment.

## Recommended defaults (local dev)

- Bind to loopback: `--host 127.0.0.1`
- Enable Gateway user auth for browser apps and per-user runtime routing:

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
```

- Use the generated `default/admin` browser-login token from
  `$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token` for first setup, then
  rotate it or create named users in `/console`.
- Use a strong `ABSTRACTGATEWAY_AUTH_TOKEN` only for legacy server/operator
  bearer-token deployments; it is not a browser sign-in token.

```bash
export ABSTRACTGATEWAY_AUTH_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
```

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
```

- Allow only localhost origins for browser UIs:

```bash
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
```

## If exposing beyond localhost (LAN, tunnels, internet)

1. Run behind TLS (reverse proxy or a trusted tunnel).
2. Use a strong token and rotate it periodically.
3. Restrict `ABSTRACTGATEWAY_ALLOWED_ORIGINS` to exact UI origins (avoid broad wildcards).
4. Protect reads as well as writes (ledgers contain prompts and tool outputs).

## Hosted file-source terms

When hosted browser clients talk about files, use this vocabulary:

- `Artifact`: a saved Runtime-owned file payload.
- `Local File`: a file chosen from the client device. In hosted/browser mode it
  is uploaded and becomes an Artifact before durable execution.
- `Server File`: a user-facing label for a file inside Gateway-approved
  workspace scope on the server. It does **not** mean arbitrary server
  filesystem access.

Canonical server paths use:

- `rel/path` for the main workspace root
- `mount_alias/rel/path` for approved mounts

If two allowed mounts share the same basename, Gateway now assigns
deterministic digest-suffixed mount aliases so the public path string is stable
across Gateway discovery, import/export, and Runtime execution.

Current UI surfaces may still say `Workspace` in some places. The engineering
term remains `Workspace File` / `Workspace Folder`.

## User isolation

When Gateway user auth is enabled, each user token resolves to a Gateway
principal and runtime mapping. Hosted browser apps such as AbstractFlow,
AbstractCode Web, and AbstractObserver exchange the signed-in user's token for
a Gateway browser session, store only an app-scoped opaque session id in an
HTTP-only browser cookie, and forward that session to Gateway server-side. The
raw user token is not stored in browser settings, and Gateway login response
bodies do not expose the session id or CSRF token. Mutating proxied Gateway
requests also carry a CSRF token. A server/admin token does not sign in
browsers. Gateway browser session cookies use an HTTP-only cookie for the
session id, a separate CSRF cookie, `SameSite=Lax`, path `/`, no `Secure` flag
on plain HTTP, and `Secure` when served over HTTPS. Sessions expire, logout
revokes the session record, and disabling/deleting/rotating the backing Gateway
user invalidates existing sessions.

For independent users, prefer `1 user = 1 runtime` through Gateway's
per-principal router. Gateway rejects duplicate runtime ids within the same
tenant when creating or updating users, so an admin cannot accidentally map two
tenant users to the same runtime. When a user is deleted, Gateway removes that
user's credential but reserves the retained runtime id for that deleted
principal; assigning the retained runtime to a different user is rejected unless
an admin explicitly transfers or purges the retained runtime reservation. Purge
requires exact runtime-id confirmation and deletes the retained runtime directory
before releasing the runtime id. Transfer assigns the retained runtime to an
existing same-tenant user and reserves that user's previous runtime id. Keep
shared/cross-user memory or collaboration explicit, permissioned, and auditable.

Hosted browser apps use the server-configured Gateway URL on non-local hosts.
AbstractFlow, AbstractCode Web, and AbstractObserver reject browser-supplied
Gateway URL changes when the UI is served from a non-loopback hostname. Enable
the app-specific override only behind your own access control:
`ABSTRACTFLOW_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG=1`,
`ABSTRACTCODE_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG=1`, or
`ABSTRACTOBSERVER_ALLOW_REMOTE_BROWSER_GATEWAY_CONFIG=1`. The host check uses
the request `Host` header by default. Trust proxy headers only when your reverse
proxy strips client-supplied forwarded headers, using the app-specific
`*_TRUST_PROXY_HEADERS=1` setting or `ABSTRACTGATEWAY_TRUST_PROXY_HEADERS=1`.

Shared workflows are handled through the Gateway workflow catalog rather than
by sharing a user's private runtime bundle directory. Catalog versions are
immutable by `bundle_id@version`; admins can move a default pointer, set ACLs,
deprecate or block a version, or tombstone it without deleting the stored
bundle bytes. Catalog workflows run in the requesting user's runtime by
default, so the shared workflow definition does not imply shared run state.
Run-start policy checks the signed-in user's tenant, roles, and catalog ACL
before execution. Private `/api/gateway/bundles` routes remain per-runtime
authoring surfaces.

Catalog bundles are loaded under internal host ids, but those internal ids are
not a public authorization surface. Direct private-bundle inspection routes
reject catalog-internal ids; use the ACL-aware `/api/gateway/workflow-catalog`
routes to list or inspect shared workflows. Gateway also strips client-supplied
`_runtime.workflow_policy` values and replaces catalog starts with a signed
Gateway-issued policy snapshot before Runtime sees the run.

Gateway serves a built-in admin/account console at `/console`. The console uses
the same browser-session contract: users sign in with a Gateway user id and
token, then the browser keeps only the opaque session cookie. Because the
console is served by Gateway itself, it does not ask for a Gateway URL; the
current origin is the Gateway. Admin users can
manage Gateway users, rotate tokens, and handle retained runtime reservations
from this console. Signed-in users configure provider connections in the
Providers tab, then set capability defaults from those configured virtual
providers and discovered models. Admin/root defaults act as the Gateway
baseline; normal users inherit that baseline and can override it only for their
own runtime.

The console also supports Gateway-owned provider endpoint profiles. Profiles
can describe OpenAI-compatible or hosted provider endpoints with a display name,
description, provider family, base URL, API key, capabilities, and optional
model allowlist. Discovery exposes only non-secret metadata and a virtual
provider id such as `endpoint:office-vllm`; Gateway injects the raw key only into
the runtime call that resolves that profile. Normal users can create user-scoped
profiles. Gateway-scoped profiles require an admin principal.

The console Sandbox sends browser-local prompt-grounding metadata such as local
datetime, timezone, timezone offset, and locale with test chat requests. Runtime
may use those fields to ground the model response for the browser user, but the
fields are explicitly untrusted and are never used for authorization, runtime
routing, provider credential selection, or audit authority. Server-derived
grounding remains recorded as provenance. When both browser timezone and browser
locale are present, country grounding prefers the timezone mapping over the
locale region because browser language is not a reliable location signal.

Gateway keeps operator surfaces admin-only through a central route-family
policy. User management, audit/process/backlog/triage/report routes, email
bridge routes, host metrics, model residency list/load/unload, server workspace
file helpers, and server workspace artifact import/export require an admin
principal. Browser local files should use upload routes; server filesystem
read/import/export is not exposed to ordinary hosted users.

In hosted user-auth mode today, ordinary users can still upload `Local File`
sources and reuse Artifacts, but server workspace helper routes and server
workspace artifact import/export remain admin/operator controlled until a
stronger per-principal workspace grant model lands.

Discovery metadata is permission-aware for these high-trust surfaces. Ordinary
users see admin-only workspace artifact import/export and provider prompt-cache
control operations marked unavailable with `admin_required` metadata. Their
session prompt-cache names are still usable, but the private hash includes the
current principal scope so two users cannot collide by choosing the same
session id, provider, and model.

## Verify quickly

- `/api/health` returns `200`
- `/api/gateway/*` endpoints return `401` without `Authorization: Bearer ...`
- `/console` loads the Gateway Console, and admin-only actions return `403` for
  non-admin users

## See also

AbstractGateway has a deeper security guide (env vars, limits, lockouts, audit log):
- https://github.com/lpalbou/abstractgateway/blob/main/docs/security.md

--- docs/guide/runtime-scope.md ---
# Runtime Scope (run / session / global / all)

This guide defines the meaning of **scope** across AbstractRuntime's memory effects and host UX.

## Why scope exists

The runtime stores durable "memory" under a runtime-owned namespace in run state. A scope decides **which durable owner**
receives reads/writes, and therefore which runs share memory.

## Scopes

### `run`

- The current `run_id`.
- Best for per-run working memory (notes/tags/compaction spans that should not leak outside this run).

### `session`

- Shared across runs that share the same `session_id`.
- Use when you want continuity across multiple runs launched by the same client "session".

Important: `session` is a host contract. If a host does not provide a stable `session_id`, session scope may degrade to
"per-run" behavior.

### `global`

- A single global owner shared across the whole runtime instance.
- Use for durable cross-session memory (preferences, stable facts).

### `all`

`all` is a query fan-out scope used by some operations to search across:
- `run`
- `session`
- `global`

This is why `global != all`:
- `global` means "only the global owner"
- `all` means "run + session + global"

## Practical examples

- "Remember this for the rest of this run": `scope=run`
- "Remember this for this user session": `scope=session`
- "Remember this forever": `scope=global`
- "Search everything I know": `scope=all`

## What survives a backend restart?

Scope controls *which durable owner* receives reads/writes, but persistence depends on the host stores:

- With file-backed stores (run store + ledger store + artifact store), `run`/`session`/`global` scoped memory persists.
- With in-memory stores, scope still works, but everything is lost on restart.

## Gateway API note (important)

If you start runs via the gateway and you want `scope=session` behavior across multiple runs, you must send a stable
`session_id` when starting those runs.


--- docs/guide/flow-and-kg-memory.md ---
# Flow + KG Memory (Memory object v0) - Guide

This guide explains how to configure KG memory recall + writeback in VisualFlow using the first-class `memory` object.

It is written for novice users of AbstractFlow and focuses on practical, step-by-step setup.

## What is the `memory` object?

`memory` is a JSON-safe object that groups "what memory the model can use" and "how KG memory is queried/written" into a
single value.

It is designed to:
- keep Agent / LLM Call nodes clean (one pin instead of many),
- be portable (host can pass one object),
- stay backward compatible (legacy per-pin keys still work).

Where it's used:
- Agent node: connect `memory` -> `agent.memory`
- LLM Call node: connect `memory` -> `llm_call.memory`
- RunnableFlow start (optional): `On Flow Start.memory` can be supplied by the client/run modal

## Quick start (recommended): use the Memory literal node

1. In AbstractFlow, add a **Memory** node (Literals -> Memory).
2. Edit its JSON to your desired configuration.
3. Connect:
   - `Memory.value` -> `Agent.memory` (or `LLM Call.memory`)
4. Run the flow and inspect the node trace:
   - When `use_kg_memory=true`, the model should receive a bounded "KG ACTIVE MEMORY" system block when recall finds
     relevant items.

## Provide memory from the client (RunnableFlow)

RunnableFlow workflows (interface `abstractcode.agent.v1`) can accept a `memory` input at start:

1. Ensure your `On Flow Start` node exposes an output pin `memory` (type `memory`).
2. Wire `On Flow Start.memory` into your Agent/LLM Call.
3. In your client (run modal / API), set `memory` as JSON.

### Best practice: defaults + overrides

If you want the flow to have good defaults but still allow overrides from the client:

1. Create a Memory (defaults) literal node (typed `memory`).
2. Ensure `On Flow Start` provides `memory = {}` by default.
3. Merge defaults + overrides and use that output as the effective memory object.

## Memory object schema (v0)

All keys are optional. Omitted keys mean "no override" (runtime defaults apply).

### Recall/source controls (Agent + LLM Call)

- `use_session_attachments: boolean`
- `use_span_memory: boolean`
- `use_semantic_search: boolean` (reserved; not implemented in v0)
- `use_kg_memory: boolean`
- `memory_query: string` (defaults to the node prompt/task if omitted)
- `memory_scope: string` (`run | session | global | all`)
- `recall_level: string` (`urgent | standard | deep`)
- `max_span_messages: number`
- `kg_max_input_tokens: number`
- `kg_limit: number`
- `kg_min_score: number` (0..1)

### KG write defaults (useful for ingest subflows)

- `kg_write_scope: string` (`run | session | global`)
- `kg_domain_focus: string`
- `kg_max_out_tokens: number` (0 means "no cap")

## Example memory object

```json
{
  "use_session_attachments": true,
  "use_span_memory": false,
  "use_semantic_search": false,
  "use_kg_memory": true,
  "memory_query": "",
  "memory_scope": "session",
  "recall_level": "standard",
  "max_span_messages": 24,
  "kg_max_input_tokens": 1200,
  "kg_limit": 80,
  "kg_min_score": 0.35,
  "kg_write_scope": "session",
  "kg_domain_focus": "software / agents / memory systems",
  "kg_max_out_tokens": 0
}
```

## Query and insert (KG recall + KG writeback)

### Query (recall)

To make an Agent/LLM Call use KG recall:
1. Set `memory.use_kg_memory = true`.
2. Optionally set `memory.memory_query` (otherwise the prompt is used).
3. Choose `memory.memory_scope`:
   - `session` is common when you want continuity within one client session.
   - `global` is common when you want durability across independent runs.

### Insert (writeback)

To write new knowledge into the KG, run an ingestion step/subflow after you produce an answer.
The typical pattern is to build a turn transcript string and pass it to an extractor that writes assertions using:
- scope from `memory.kg_write_scope` (or a fixed literal)
- domain hint from `memory.kg_domain_focus`


--- docs/guide/scheduled-workflows.md ---
# Scheduled Workflows (Durable Jobs)

This guide explains how scheduled workflows work when using `abstractgateway` + `abstractruntime`.

## What "scheduled workflows" means in AbstractFramework

A scheduled workflow is a durable parent run that triggers a target workflow as child runs over time.

Key properties:
- Durable: schedule state is persisted and survives restarts.
- Replay-first observability: you can attach later and reconstruct what happened from the ledger.
- Single authority: the gateway host owns ticking/resuming.

## Who "runs" the schedule (and why it stops when processes stop)

AbstractRuntime is a library. It can represent "wait until time X", but it does not create OS timers or wake itself up.

Something must keep calling `Runtime.tick(...)` to advance runs. In the gateway topology, the gateway runner loop:
- ticks running runs
- resumes due waits (including "wait until" for scheduled jobs)
- applies durable commands (pause/resume/cancel/emit_event)

If you stop the gateway process, nothing ticks. When it restarts, due waits resume on the next poll cycle.

## Creating a scheduled run (Gateway HTTP API)

Endpoint: `POST /api/gateway/runs/schedule`

Example: start now, repeat every 20 minutes forever:

```bash
curl -sS -X POST "http://127.0.0.1:8080/api/gateway/runs/schedule" \
  -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_id": "my-bundle",
    "flow_id": "root",
    "input_data": { "prompt": "write my report" },
    "start_at": "now",
    "interval": "20m",
    "share_context": true
  }'
```

Example: start at a specific time (UTC ISO), run 10 times:

```bash
curl -sS -X POST "http://127.0.0.1:8080/api/gateway/runs/schedule" \
  -H "Authorization: Bearer $(cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token")" \
  -H "Content-Type: application/json" \
  -d '{
    "bundle_id": "my-bundle",
    "flow_id": "root",
    "input_data": { "prompt": "write my report" },
    "start_at": "2026-01-15T15:06:00+00:00",
    "interval": "20m",
    "repeat_count": 10
  }'
```

Notes:
- Intervals are relative (drift is expected if runs take time or the gateway is down).
- If a child run blocks on a durable wait (ask-user, approvals, wait-event), the schedule blocks too (it waits for the
  child to finish).
- Durable execution is not transactional I/O (at-least-once semantics). Prefer idempotent outputs for scheduled jobs.

## Controlling scheduled runs

Scheduled runs are normal durable runs:
- Pause the parent run to pause the schedule.
- Cancel the parent run to stop the schedule and cancel active children.

This is done via `POST /api/gateway/commands`.

--- docs/guide/prompt-caching.md ---
# Prompt Caching (Prompt / KV)

This guide explains how the framework should think about caching for LLM calls, with an emphasis on prompt/KV caching
(prefill reuse) rather than response memoization.

## Terminology (three different "caches")

1. Response cache (exact/semantic)
   - Memoize final model outputs keyed by the input request.
   - Useful for repetitive questions, but risky for correctness drift.

2. Prompt/KV cache (prefix/prefill reuse)
   - Reuse the model's internal KV state for repeated prompt prefixes.
   - This can dramatically reduce time-to-first-token for long prompts with stable prefixes.

3. Composable KV modules (advanced)
   - Precompute caches for separate chunks (docs, history) and stitch them later.
   - This is typically an engine-level feature and not guaranteed uniformly across providers.

## AbstractCore support (best-effort)

AbstractCore exposes a provider-optional prompt cache surface. Depending on provider/backend, it may be:
- fully supported (in-process local backends),
- pass-through to an upstream server,
- or a no-op.

Typical usage patterns:

```python
from abstractcore import create_llm

llm = create_llm("mlx", model="mlx-community/Qwen3-4B")
llm.prompt_cache_set("tenantA:session123")  # also sets the default key

llm.generate("Hello")
llm.generate("Continue, but shorter.")
```

Or per-call:

```python
resp = llm.generate("Summarize this.", prompt_cache_key="tenantA:session123")
```

## Gateway/runtime note

In gateway-first deployments, prompt caching (when enabled) should be scoped per user/session to avoid accidental
cross-user reuse. Prefer stable `session_id` values so cache keys remain stable across multiple runs.


--- docs/guide/agent-skills.md ---
# Agent Skills (SKILL.md) — Proposal

This guide captures a **planned** integration of the Agent Skills (`SKILL.md`) format into AbstractFramework.
Nothing in this document implies the feature is already shipped; it records a design direction so the knowledge
is not lost.

## What are “Agent Skills”?

In the Agent Skills ecosystem, a **skill** is a shareable folder with a required `SKILL.md` (YAML frontmatter +
instructions) plus optional `scripts/`, `references/`, and `assets/`. Skills are designed for **progressive disclosure**:
systems load only `name`/`description` for discovery and fetch the full content only when a skill is activated.

Spec constraints worth carrying into AbstractFramework:
- `name` is constrained (lowercase alphanumeric + hyphens, 1–64 chars) and must match the skill’s leaf directory name.
- `allowed-tools` is **experimental** and is a space-delimited list of “pre-approved” tools in the originating ecosystem.

## Skills vs flows (what each is “for”)

- **Flows** (`.flow` bundles / VisualFlow) are **executable programs** in AbstractFramework:
  durable execution, explicit waits, tool boundaries, replayable ledger history.
- **Skills** (`SKILL.md`) are **portable procedure packs**:
  prompt/instructions + optional scripts/resources, designed to be shared across agents/ecosystems.

### Are flows “more advanced” than skills?

They’re “more advanced” in different dimensions:

- **Execution**: flows are more advanced (they *run* as durable state machines).
- **Portability**: skills are more advanced (they’re a widely adopted, tool-agnostic packaging standard).

### Can every skill be modeled as a flow?

**Conceptually yes**: a skill is “a procedure + resources”, and a flow can orchestrate any procedure.
In practice, the limit is **tooling/environment assumptions**, not the flow model:
a skill may assume Playwright is installed, or a specific “container” tool surface exists.

Also, not every skill is *worth* turning into a dedicated flow:
“guidelines/checklists/style” skills often work best as **prompt modules** attached to a generic agent flow.

## Proposed interaction model (v0)

The key design choice is: **flows run; skills are activated/loaded** (and skill scripts only run as explicit tools).

### 1) Run-attached skills (primary)

- Clients/hosts attach an `available_skills` metadata set to a run (name/description only).
- Users explicitly activate a skill (e.g. `/skill <name>`), which loads full `SKILL.md` and any resources.
- Activation is logged durably (ledger record). For replay/resume safety, activation should snapshot skill content
  (or at least record a content hash) so a long-running run doesn’t silently pick up a modified skill file.

### 2) Bundle-declared skill dependencies (secondary)

Flows may declare skill dependencies without changing the `.flow` format by using `manifest.metadata`, for example:

- `skills.required`: skills that must be present on the host/gateway
- `skills.defaults`: skills to auto-activate at run start

This lets organizations ship a workflow that says “this workflow expects these skills”, while keeping skills
as separately managed artifacts.

### 3) Bundle-embedded skills (optional; later)

WorkflowBundles support `assets/*`. If we want hermetic distribution (“workflow + skills in one file”),
we can embed skills under bundle assets (e.g. `assets/skills/<id>/...`) and expose them via the gateway.

## Implementation notes (what fits best with existing runtime/flow mechanics)

The lowest-friction implementation in AbstractFramework is to add skills as **runtime-owned tools** in the
AbstractRuntime ↔ AbstractCore integration (the same pattern already used for `open_attachment`):

- `list_skills()` for metadata-only discovery (progressive disclosure).
- `open_skill(...)` to load full `SKILL.md` (and optionally specific resources), snapshotting large payloads as artifacts
  and recording a content hash.
- optional `activate_skill(...)` to update durable run state (`_runtime.skills.active`) and apply `allowed-tools` as a
  restriction (intersection with the run’s tool allowlist).

Because these are tools, **flows can compose over skills immediately** using existing Tool/CallTool nodes, and agent
loops can activate skills without introducing new effect types.

Note: bundle-declared dependencies via `manifest.metadata.skills.*` are supported as a pattern, but VisualFlow JSON
currently has no `metadata` field; making this authorable requires a UI/CLI surface (or a schema extension).

## Safety and tool gating

- Skills may include scripts. **Scripts must never run implicitly** “because a skill exists”.
- Skills may declare `allowed-tools` (ecosystem field; experimental). In AbstractFramework the safe behavior is:
  - treat it as a **restriction** when it can be mapped to AbstractFramework tool names (intersection with the run’s tool allowlist),
  - if it cannot be mapped (unknown grammar/tool ids), emit `#FALLBACK` and **do not relax** the run’s tool policy,
  - deny and log out-of-policy tool calls with actionable `#FALLBACK` warnings,
  - keep run state JSON-safe (store bodies/resources as artifacts when large).

## Where this fits (packages)

- `abstractagent`: prompt injection of metadata; “activate skill” as an explicit step; optional schema-only built-in
  (e.g. `open_skill`) so the runtime/host performs the read.
- `abstractruntime`: runtime-owned “skill read/activate” handler that is durable + ledger-recorded + artifact-backed,
  plus enforcement hooks for `allowed-tools`.
- `abstractgateway`: optional “skills registry” (list/fetch/install/deprecate), parallel to `.flow` bundle distribution.
- `abstractcore`: stays lean; any provider-specific “container skills” integration remains optional and gated.

## References and next steps

- Backlog item: `docs/backlog/planned/074_agent_skills_integration.md`
- Implementation plan (phased): `docs/backlog/planned/074_agent_skills_integration_plan.md`
- Research notes (spec + ecosystem scan): `docs/skills/`

--- docs/guide/workflow-bundles.md ---
# WorkflowBundles (`.flow`) and Lifecycle

WorkflowBundles (`.flow`) are the portable distribution unit for VisualFlow workflows:

- a zip bundle containing `manifest.json` + `flows/*.json` (and optional assets)
- entrypoints can advertise interface contracts (for example `abstractcode.agent.v1`) for discovery across clients

## Where bundles live (gateway-first)

On a gateway host, configure a bundles directory:

```bash
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/workflows"   # contains *.flow
```

The gateway discovers and serves bundles to thin clients via discovery endpoints.

Hosted gateways now distinguish two bundle registries:

- **Private runtime bundles** live in the caller's routed runtime and are served
  through `/api/gateway/bundles`. AbstractFlow's normal save/publish/test loop
  continues to use this private surface.
- **Workflow catalog bundles** live in the Gateway control plane and are served
  through `/api/gateway/workflow-catalog`. Admins upload or promote immutable
  `.flow` versions, move explicit default pointers, set ACLs, and deprecate,
  block, or tombstone versions without deleting bundle bytes.

Catalog workflows run in the requesting user's runtime by default. Clients
start them with `registry_scope: "tenant_catalog"` plus `bundle_id`,
`bundle_version` when an exact version is required, and `flow_id`. If the
version is omitted, Gateway resolves the admin-managed default pointer rather
than guessing from semantic version order. Exact older versions keep working
until that specific version is deprecated, blocked, or tombstoned.

Catalog starts must be explicit. If `registry_scope` is omitted, Gateway treats
the request as a private-runtime bundle start and will not silently fall through
to the shared catalog. Catalog run policy is Gateway-issued and signed before it
is passed into Runtime state; clients cannot authorize catalog subworkflow
starts by sending their own `_runtime.workflow_policy`.

## Recommended lifecycle controls

- Publish/install new versions instead of editing deployed bundles in place.
- Deprecate workflows instead of deleting:
  - hides from discovery
  - blocks new starts
  - keeps old versions available for replay/audit of historical runs
- Specialized applications may intentionally preselect or hardcode a
  `bundle_id`/`flow_id` when that workflow is the product contract. This is a
  valid use of WorkflowBundles; what matters is that the app still starts a
  normal durable Gateway run instead of bypassing workflow execution.
- For shared/default workflows, use the workflow catalog. Do not overwrite
  catalog bundle bytes for an existing `bundle_id@version`; publish a new
  immutable version and move the default pointer.
- `framework_catalog` is reserved for a later cross-tenant catalog. The
  implemented shared catalog scope today is `tenant_catalog`.

## See also

- [Scenario: Publish, install, and deprecate workflows](../scenarios/workflow-bundle-lifecycle.md)

--- docs/guide/telegram-integration.md ---
# Telegram Integration (Gateway Bridge + Workflow)

This guide explains how to run a Telegram "permanent contact" that talks to an agent workflow through the gateway.

Telegram is implemented as a **thin client**:
- Inbound Telegram messages -> gateway starts a new run per message (stable `session_id` for durable memory)
- Outbound replies -> the bridge sends the run output back to Telegram
- Attachments -> stored in the ArtifactStore and passed as `context.attachments` / `context.media`

## Security model choices

Telegram has two integration paths:

1. TDLib + Secret Chats (E2EE in transit; recommended)
2. Bot API (easy, not E2EE)

Even with E2EE, messages are decrypted on the gateway host and persisted to durable stores in plaintext by design. Secure
the gateway host and its storage.

## Access control (critical)

Telegram bots and user accounts are discoverable. Without access control, anyone who finds the handle can message it and
trigger durable runs + LLM calls.

The bridge is **fail-closed by default**:
- DMs: `ABSTRACT_TELEGRAM_DM_POLICY=allowlist` (default) — only allowlisted Telegram `user_id`s are processed.
- Groups: `ABSTRACT_TELEGRAM_GROUP_POLICY=disabled` (default) — all group/supergroup/channel messages are ignored.
- Unauthorized messages are ignored (no run created, no token spend). `/whoami` always works.

Commands:
- `/whoami` — always works; prints your `user_id` and `chat_id` (useful for allowlists).
- `/pair ...` — pairing workflow (DM only; only when `ABSTRACT_TELEGRAM_DM_POLICY=pairing`).

Minimal env vars (Bot API + DM allowlist):

```bash
export ABSTRACT_TELEGRAM_BRIDGE=1

# Bot API (easy, not E2EE). If ABSTRACT_TELEGRAM_BOT_TOKEN is set, transport defaults to bot_api.
export ABSTRACT_TELEGRAM_BOT_TOKEN="..."  # from @BotFather

# Allowlisted DM users (numeric Telegram user_id). Use /whoami to discover yours.
# Accepts comma/newline-separated ints or JSON list.
export ABSTRACT_TELEGRAM_ALLOWED_USERS="123456789"
```

## Quickstart (DM-only allowlist)

This is a practical checklist to verify access control + approvals end-to-end.

1. Configure the bridge (Bot API):

```bash
# Gateway (required for `abstractgateway serve`).
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"  # directory containing *.flow bundles (incl. shipped `basic-agent`)
export ABSTRACTGATEWAY_AUTH_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"

export ABSTRACT_TELEGRAM_BRIDGE=1
export ABSTRACT_TELEGRAM_BOT_TOKEN="..."               # from @BotFather
```

2. Start the gateway.
   - You may see a warning about an empty DM allowlist; this is expected until you set `ABSTRACT_TELEGRAM_ALLOWED_USERS`.
   - Then DM the bot and run `/whoami`.
   - Copy your `user_id` (the bot also prints an `export ABSTRACT_TELEGRAM_ALLOWED_USERS="..."` hint).

3. Set your allowlist and restart the gateway:

   - `export ABSTRACT_TELEGRAM_ALLOWED_USERS="123456789"`

4. Verify:
   - From an allowlisted account, send “hi” → expected: you receive a reply.
   - From a non-allowlisted account, send “hi” → expected: ignored (except `/whoami`).

5. Verify tool approvals:
   - In the chat, ask: `run free -m` (or `uname -a`).
   - Expected: the bot asks you to reply `/approve` before executing `execute_command`.
   - Expected: after `/approve`, you receive the final answer/result in Telegram (read-only tools like `web_search` should not require approval).

## Optional: pairing mode (DMs)

Pairing lets unknown users request access without editing `ABSTRACT_TELEGRAM_ALLOWED_USERS`, but it requires an admin.

```bash
export ABSTRACT_TELEGRAM_DM_POLICY="pairing"
export ABSTRACT_TELEGRAM_ADMIN_USERS="123456789"   # your operator user_id (use /whoami)
export ABSTRACT_TELEGRAM_PAIRING_TTL_S="3600"      # optional
```

## Optional: group chat support

Group chats are disabled by default. To enable allowlisted groups:

```bash
export ABSTRACT_TELEGRAM_GROUP_POLICY="allowlist"
export ABSTRACT_TELEGRAM_ALLOWED_CHATS="-100123456789"  # use /whoami inside the group to discover chat_id
# Optional (default true): require @mention in groups
export ABSTRACT_TELEGRAM_REQUIRE_MENTION_IN_GROUPS=1
```

## Viewing Telegram sessions in AbstractCode

Telegram runs are durable; you can replay them from any thin client.

1. Open AbstractCode (default): http://localhost:3002
2. Go to **History**, click **Refresh**, then open the Telegram session (e.g. `telegram:<chat_id>:r<rev>`).

Notes:
- Each incoming Telegram message creates a new run under the same `session_id` (durable memory across turns).

## Minimal gateway configuration

Install Telegram support:

```bash
pip install abstractgateway
# Optional (only if your workflows call Telegram tools like `send_telegram_message`):
# pip install "abstractcore[tools]"
```

Set env vars on the gateway host:

```bash
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"  # directory containing *.flow bundles
export ABSTRACTGATEWAY_AUTH_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"  # required

export ABSTRACT_TELEGRAM_BRIDGE=1

# Bot API (easy, not E2EE). If ABSTRACT_TELEGRAM_BOT_TOKEN is set, transport defaults to bot_api.
export ABSTRACT_TELEGRAM_BOT_TOKEN="..."
export ABSTRACT_TELEGRAM_ALLOWED_USERS="123456789"  # use /whoami

# Tool execution + approvals:
# - `approval` (default): safe tools run in-process; dangerous/unknown tools still require a Telegram reply: `/approve` or `/deny`.
# - `passthrough`: delegated execution (advanced; not recommended for thin clients).
export ABSTRACTGATEWAY_TOOL_MODE="approval"

# Optional: override which workflow to run per message.
# Default (when unset): shipped `basic-agent` bundle entrypoint.
# export ABSTRACT_TELEGRAM_BUNDLE_ID="basic-agent"
# export ABSTRACT_TELEGRAM_FLOW_ID="81795ea9"
```

Notes:
- Default LLM routing comes from the execution-host `input.text` capability
  route. Set it with `abstractcore --set-global-default ...` or
  `abstractgateway-config set-default input.text ...`.
- Telegram-only routing override (does not affect other gateway traffic): set `ABSTRACT_TELEGRAM_MODEL="..."` (and optionally `ABSTRACT_TELEGRAM_PROVIDER="..."`).
- Durable history limit: `ABSTRACT_TELEGRAM_MAX_HISTORY_MESSAGES` (default: 30; `0` keeps only system messages).
- STT fallback and vision caption fallback are configured via `abstractcore --config` (audio strategy + vision fallback).
- Telegram typing keepalive is best-effort: tune with `ABSTRACT_TELEGRAM_TYPING_INTERVAL_S` (default: 4s) and `ABSTRACT_TELEGRAM_TYPING_MAX_S` (default: 600s; set to `0` to disable).
- `/reset` behavior is best-effort: the bridge clears the durable session, sends a confirmation message, and optionally deletes recent messages in the background. Controls: `ABSTRACT_TELEGRAM_RESET_DELETE_MESSAGES` (default: true), `ABSTRACT_TELEGRAM_RESET_DELETE_MAX` (default: 200), `ABSTRACT_TELEGRAM_RESET_MESSAGE` (confirmation text). Telegram may still reject deletions depending on chat permissions and age.

Start the gateway normally.

## Local dev test (Bot API + LMStudio)

If you're in the AbstractFramework repo, you can use `./execute.sh` as a convenience env setup:

```bash
source ./execute.sh
```

1. Start LMStudio “Local Server” and load `google/gemma-3n-e4b`.
2. Set the text output route for this run:

```bash
abstractgateway-config set-default input.text \
  --provider lmstudio \
  --model google/gemma-3n-e4b \
  --base-url http://127.0.0.1:1234/v1
export LMSTUDIO_BASE_URL="http://127.0.0.1:1234/v1"
```

3. Ensure the gateway can see the shipped bundle (a directory containing `*.flow`):

```bash
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"  # e.g. ./abstractgateway/flows/bundles in this repo
```

4. Send a Telegram message to the bot and verify:
   - you receive a reply
   - a follow-up (“What did I just say?”) works (durable memory)
   - media messages (photo/voice/video/document) are handled

## Workflow wiring (VisualFlow)

### Default workflow (recommended)

By default, the bridge runs the shipped `basic-agent` bundle entrypoint once per incoming message and sends the run output back to Telegram.
Durable memory comes from the stable Telegram `session_id` (no special workflow shape required).

### Custom workflow

Any flow that reads `prompt`/`context` (like `abstractcode.agent.v1`) and writes a string to `run.output.answer` or `run.output.response` will work.
The bridge provides Telegram metadata in `input_data.telegram` if you want to branch on it.

## TDLib notes (E2EE path)

TDLib requires:
- a real Telegram user account for the AI identity
- the TDLib shared library (`tdjson`) installed on the gateway host
- a persistent TDLib session directory (so you authenticate once)

Because TDLib is platform-specific, keep your setup steps close to your deployment scripts. The gateway integration code
and configuration surface live in:
- https://github.com/lpalbou/abstractgateway

## Testing checklist

1. Confirm the gateway loaded your bundles (API: `GET /api/gateway/bundles`) and that the configured Telegram flow exists.
2. Send a message to the bot; verify you get a reply and that Observer can replay the run.
3. Send a follow-up (“What did I just say?”) to confirm durable memory works.
4. Send media:
   - photo (with and without caption)
   - voice note (STT fallback depends on your `abstractcore --config` audio strategy + installed plugins)
   - video and document
5. Send `/reset` to clear the binding/runs, then confirm the next message starts fresh.

## Tool approvals (Telegram)

When the agent requests a tool call that requires explicit permission (for example `write_file` or `execute_command`),
the bridge sends an approval prompt into the chat. Reply with:
- `/approve` (or `approve`) to execute the tool calls and continue
- anything else to cancel the tool calls and let the workflow continue with failure results
 
Note: `/tools` is intentionally disabled in thin-client mode; use `/approve` and `/deny`.

--- docs/guide/email-integration.md ---
# Email Integration (Inbound IMAP -> Events, Outbound SMTP Defaults)

This guide explains how to use framework-native email tooling:

- Outbound: `send_email` with centralized SMTP defaults (no repeated host/user per call)
- Inbound: gateway email bridge polls IMAP and emits durable `email.message` events into stable sessions, with
  artifact-backed attachments

## Minimal outbound configuration (SMTP defaults)

Configure on the process that executes tools (gateway local tools, CLI host, or tool worker):

```bash
export ABSTRACT_EMAIL_SMTP_HOST="smtp.example.com"
export ABSTRACT_EMAIL_SMTP_USERNAME="me@example.com"
export ABSTRACT_EMAIL_SMTP_PASSWORD_ENV_VAR="EMAIL_PASSWORD"
export EMAIL_PASSWORD="..."
```

Then a minimal tool call can be:

```json
{ "name": "send_email", "arguments": { "to": "you@example.com", "subject": "Hello", "body_text": "Hi!" } }
```

## Minimal inbound configuration (gateway email bridge)

Configure IMAP + enable the bridge on the gateway host:

```bash
export ABSTRACT_EMAIL_BRIDGE=1
export ABSTRACT_EMAIL_IMAP_HOST="imap.example.com"
export ABSTRACT_EMAIL_IMAP_USERNAME="me@example.com"
export ABSTRACT_EMAIL_IMAP_PASSWORD_ENV_VAR="EMAIL_PASSWORD"
export ABSTRACT_EMAIL_POLL_SECONDS=60
```

Recommended for v0: auto-start a workflow per thread/session:

```bash
export ABSTRACT_EMAIL_FLOW_ID="<bundle_id>:<flow_id>"
```

## Workflow wiring

Create a flow that:
1. handles `email.message` (On Event; scope `session`)
2. reads `payload.email.*` (subject/from/body and artifact-backed attachments)
3. optionally opens attachments via `open_attachment(artifact_id=...)`
4. replies with `send_email(...)`

## See also

Gateway maintenance docs mention the bridge and email inbox endpoints:
- https://github.com/lpalbou/abstractgateway/blob/main/docs/maintenance.md


--- docs/guide/process-manager-env-vars.md ---
# Process Manager Env Vars (Write-only) - Operator Guide

This guide explains how to configure a small allowlist of environment variables on the gateway host via AbstractObserver,
without exposing the values back to browsers/clients.

## What this is (and why)

- Goal: configure framework integrations (for example email) from the UI.
- Security model:
  - allowlist-only keys (no arbitrary env var editing)
  - write-only values (the gateway API never returns env var values)
  - values are persisted on the gateway host with restrictive file permissions

## Requirements

- Gateway process manager enabled: `ABSTRACTGATEWAY_ENABLE_PROCESS_MANAGER=1`
- AbstractObserver connected to that gateway

## Where values are stored (host-side)

- `<ABSTRACTGATEWAY_DATA_DIR>/process_manager/env_overrides.json`

## How values are applied

- When you set/unset an allowlisted env var in the UI:
  - the gateway persists it to `env_overrides.json`
  - the gateway applies it to its own `os.environ` (so integrations reading env vars can see it)
- When the process manager launches managed processes, it merges:
  1. gateway `os.environ`
  2. allowlisted overrides
  3. per-process env (static)

If a service reads env vars only at startup, restart that service after changing env vars.

## Allowlisted keys

The allowlist is gateway-defined. Email-related keys are commonly allowlisted for inbox and SMTP defaults.

## See also

- AbstractGateway docs: https://github.com/lpalbou/abstractgateway


--- docs/scenarios/README.md ---
# Scenarios

This section is use-case oriented. Each scenario is an end-to-end path with concrete commands and "what to expect".

- [Offline coding assistant (terminal)](offline-coding-assistant.md)
- [Gateway-first local development](gateway-first-local-dev.md)
- [Specialized agent as a portable `.flow`](specialized-agent-flow.md)
- [Workflow bundle lifecycle (publish/install/deprecate)](workflow-bundle-lifecycle.md)
- [Telegram permanent contact](telegram-permanent-contact.md)
- [Email inbox agent](email-inbox-agent.md)
- [Phone thin client (iPhone via Web/PWA)](phone-thin-client.md)

If you prefer component-first docs, start with [Getting Started](../getting-started.md).

--- docs/scenarios/offline-coding-assistant.md ---
# Scenario: Offline Coding Assistant (Terminal)

Goal: run a durable coding assistant on one machine, offline-first, with Ollama (or an
OpenAI-compatible local server) as the model backend.

AbstractCode is a client of AbstractGateway: the gateway runs the coding agent on your machine and
the terminal client connects to it over HTTP/SSE. Nothing leaves the machine when the model server
is local too.

## Prereqs

- Python 3.10+ (for the gateway)
- Rust 1.87+ (for `cargo install abstractcode`), or a prebuilt binary from the
  [AbstractCode GitHub release](https://github.com/lpalbou/AbstractCode/releases)
- An LLM backend:
  - Ollama (recommended)
  - LM Studio / vLLM / LocalAI (OpenAI-compatible)

## Step 1: Install

```bash
pip install abstractframework     # pinned stack, includes abstractgateway
cargo install abstractcode        # terminal client
```

`pip install abstractgateway` is enough if you only want the gateway.

## Step 2: Start a local model

### Ollama

```bash
ollama serve
ollama pull qwen3:4b-instruct
export OLLAMA_HOST="http://localhost:11434"
```

Pick it as the default text model:

```bash
abstractcore --config
```

## Step 3: Start the gateway on loopback

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

The gateway ships the `coding-agent:coder` workflow that AbstractCode uses by default.

## Step 4: Run AbstractCode

```bash
abstractcode doctor               # checks the gateway connection and available workflows
abstractcode                      # connects to http://127.0.0.1:8080
```

Prefer a browser? `npx @abstractframework/code` serves the same client on
`http://127.0.0.1:3002`.

## Step 5: Work with files and tools

- Type a task and press Enter; reasoning cycles and tool cards stream in live.
- Tools are approval-gated by default: approve or reject each call, from either client.
- Type `/help` for commands.

## What "durable" means here

- The run lives in the gateway, not in the client. Close the terminal and reattach later; the
  session keeps its full history.
- A run gated on your approval in the terminal can be approved from the browser client, and the
  other way round.

## When to go further

Use the same gateway when you want:
- multiple thin clients observing the same run
- remote execution
- scheduling and a durable command inbox
- bundle discovery for specialized agents

See [Gateway-first local development](gateway-first-local-dev.md).

--- docs/scenarios/gateway-first-local-dev.md ---
# Scenario: Gateway-first Local Development

Goal: run the full "thin clients + gateway control plane" stack locally:

- AbstractGateway (HTTP/SSE control plane)
- AbstractObserver (run observability)
- AbstractFlow Editor (author `.flow` workflows)
- Code Web UI (browser coding assistant)

This is the recommended topology because execution is unified and clients can attach/detach freely.

## Step 0: Install the pinned stack (recommended)

```bash
pip install abstractframework
```

From a source checkout, `./scripts/gateway-flow-local.sh` starts Gateway and
Flow together and prints the default `admin` browser-login token. For a
published-package smoke run, `./scripts/gateway-flow.sh` creates an isolated
published-package venv, enables Gateway user auth, prepares the same `admin`
user, and prints the Gateway URL, user, and token to enter in Flow.

If you want a minimal install instead, you need at least:
- `abstractgateway`
- `npx @abstractframework/flow` for the editor UI

## Step 1: Prepare directories

Pick the Gateway data folder. Packaged Gateway installs already include the
shipped `basic-agent` bundle. In a source checkout, point the gateway at the
repo's bundled workflows.

- `DATA_DIR`: where gateway stores run state/ledger/artifacts
- `FLOWS_DIR`: optional custom `.flow` bundle directory; in this repo use
  `./abstractgateway/flows/bundles`

Example:

```bash
mkdir -p ./runtime/gateway
```

## Step 2: Configure the gateway

```bash
export ABSTRACTGATEWAY_USER_AUTH=1
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"

# Source checkout only. Packaged installs can omit this and use the shipped bundle path.
export ABSTRACTGATEWAY_FLOWS_DIR="$PWD/abstractgateway/flows/bundles"
```

When the gateway starts, it creates `default/admin` if needed and writes the
browser-login token to `$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token`.

Optional route defaults (if your flows use LLM nodes):

```bash
abstractgateway-config set-default input.text \
  --provider ollama \
  --model qwen3:4b-instruct
```

You can also set the text default through Core:

```bash
abstractcore --set-global-default ollama/qwen3:4b-instruct
```

Capability routes are the durable default surface for local setups.

## Step 3: Start the gateway

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

Smoke check:

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

Read the local admin user token for browser UIs:

```bash
cat "$ABSTRACTGATEWAY_DATA_DIR/auth/bootstrap-admin-token"
```

## Step 4: Start the thin clients

In separate terminals:

```bash
npx @abstractframework/observer
npx @abstractframework/flow
npx @abstractframework/code
```

Default ports:
- Observer: http://localhost:3001
- Code Web: http://localhost:3002
- Flow Editor: http://localhost:3003

## Step 5: Connect UIs to the gateway

In each UI:
- Set Gateway URL: `http://127.0.0.1:8080`
- Set User: `admin`
- Paste the generated Gateway user token
- Sign in

## Step 6: Author and run a specialized agent

1. In Flow Editor, create a workflow implementing `abstractcode.agent.v1`.
2. Export as a `.flow` bundle.
3. Put the `.flow` file into `FLOWS_DIR` (or configure the editor to publish to the gateway).
4. In Observer or Code Web, pick the workflow and start a run.
5. Watch the ledger stream in Observer.

See [Specialized agent as a portable `.flow`](specialized-agent-flow.md).

## Troubleshooting

- CORS errors in browser: widen `ABSTRACTGATEWAY_ALLOWED_ORIGINS` for your UI origin.
- "Unauthorized": ensure the browser UI is signed in with a Gateway user token, not the admin token.
- Bundles not showing up: verify `ABSTRACTGATEWAY_FLOWS_DIR` contains the shipped `basic-agent` bundle and any custom `.flow`
  files, then reload bundles from the UI (or restart the gateway).

--- docs/scenarios/specialized-agent-flow.md ---
# Scenario: Specialized Agent as a Portable `.flow`

Goal: build a specialized agent once, then run it in:

- AbstractCode (terminal)
- Code Web UI (browser)
- AbstractObserver (browser)
- your own apps (via gateway bundle discovery)

The key is an interface contract: `abstractcode.agent.v1`.

## Step 1: Create the flow

In the Flow Editor (`npx @abstractframework/flow`):

1. Add an **On Flow Start** node.
2. Add an **Agent** node (or **LLM Call** for a single-shot step).
3. Add an **On Flow End** node.
4. Wire the pins:
   - Start outputs to Agent inputs: `provider`, `model`, `prompt` (and optional `tools`, `context`, `memory`)
   - Agent outputs to End inputs: `response`, `success`, `meta` (and optional `scratchpad`)
5. In flow properties, set:
   - `interfaces: ["abstractcode.agent.v1"]`

## Agent vs LLM Call

- Use **Agent** when you want an internal multi-step loop (ReAct/CodeAct/MemAct style).
- Use **LLM Call** when you want a single request/response and explicit tool wiring in the graph.

See [Guide: Agent vs LLM Call](../guide/agent-vs-llm.md).

## Step 2: Export as a bundle

Export the workflow as a `.flow` bundle.

A `.flow` bundle packages:
- the root VisualFlow JSON
- any referenced subflows
- optional assets

## Step 3: Run locally (terminal)

With AbstractCode:

```bash
abstractcode --workflow /path/to/my-agent.flow
```

Or install the bundle into the local registry:

```bash
abstractcode workflow install /path/to/my-agent.flow
abstractcode --workflow my-agent
```

## Step 4: Deploy to a gateway

Copy the `.flow` bundle into `ABSTRACTGATEWAY_FLOWS_DIR`.

Then it will appear in:
- Observer workflow picker
- Code Web UI workflow picker
- Gateway discovery endpoints

## Step 5: Pass memory (optional)

If you want KG memory recall/writeback, wire a `memory` object into the Agent/LLM Call node.

See [Guide: Flow + KG memory](../guide/flow-and-kg-memory.md).


--- docs/scenarios/workflow-bundle-lifecycle.md ---
# Scenario: Publish, Install, and Deprecate Workflows (`.flow` bundles)

Goal: author a workflow once, distribute it as a portable `.flow` bundle, and manage its lifecycle on a gateway so it is
discoverable across clients.

## What you're managing

AbstractFramework distributes workflows as WorkflowBundles (`.flow` files):
- a zip bundle containing `manifest.json` + `flows/*.json` (and optional assets)
- entrypoints can advertise interface contracts (for example `abstractcode.agent.v1`) for discovery across clients

## Step 1: Author the workflow (Flow Editor)

Run the editor UI:

```bash
npx @abstractframework/flow
```

Open http://localhost:3003 and create/update your workflow.

## Step 2: Export/publish a `.flow` bundle

In the editor, export a `.flow` file (the bundle includes the root flow plus referenced subflows).

## Step 3: Put the bundle where the gateway loads bundles

On the gateway host, configure:

```bash
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/workflows"   # directory containing *.flow
export ABSTRACTGATEWAY_DATA_DIR="/path/to/gateway-data" # durable stores (run, ledger, artifacts)
```

Copy your bundle into `ABSTRACTGATEWAY_FLOWS_DIR`, for example:

- `my-bundle@0.1.0.flow`

Start/restart the gateway (bundle mode is the default):

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

## Step 4: Discover and run from clients

Once loaded, the bundle entrypoints show up in:
- Observer workflow picker
- Code Web UI workflow picker
- Gateway discovery endpoints (for custom clients)

If you built a chat-like agent flow, declare `interfaces: ["abstractcode.agent.v1"]` so clients can run it as an agent.

## Step 5: Deprecate instead of deleting (recommended)

Deprecation hides workflows from discovery and blocks new starts, while keeping installed bundles for:
- reproducibility of past runs
- auditability and traceability

Look for gateway lifecycle controls in your UI client (Observer / Flow Editor), or use the gateway API if needed.


--- docs/scenarios/telegram-permanent-contact.md ---
# Scenario: Telegram "Permanent Contact" (Gateway Bridge + Agent Workflow)

Goal: run a Telegram contact that forwards inbound Telegram messages to a durable workflow (one run per message) and sends
replies back to Telegram.

This is a gateway-first scenario: the gateway host owns durability and stores plaintext history for replay/observability.

## High-level architecture

1. Telegram bridge receives a message.
2. Gateway maps it to a stable `session_id` (typically `telegram:<chat_id>:r<rev>`).
3. Gateway starts a new run for the configured flow (thin-client semantics).
4. The bridge sends the run output back to Telegram.

## Security model choices

Telegram has two integration paths:

1. TDLib + Secret Chats (E2EE in transit; recommended)
2. Bot API (easy, not E2EE)

Even with E2EE, messages are decrypted on the gateway host and persisted to the durable stores in plaintext by design.
Secure the gateway host and its storage.

## Step 1: Install AbstractGateway (Telegram bridge)

```bash
pip install abstractgateway
# Optional (only if your workflows call Telegram tools like `send_telegram_message`):
# pip install "abstractcore[tools]"
```

## Step 2: Configure the gateway (minimum)

You need a normal gateway configuration plus Telegram bridge settings. At minimum:

```bash
# Bundles (*.flow). Include the shipped `basic-agent` bundle (and any custom bundles).
export ABSTRACTGATEWAY_FLOWS_DIR="/path/to/bundles"
export ABSTRACTGATEWAY_AUTH_TOKEN="..."  # required
export ABSTRACTGATEWAY_ALLOWED_ORIGINS="http://localhost:*,http://127.0.0.1:*"
export ABSTRACTGATEWAY_DATA_DIR="$PWD/runtime/gateway"

# Tool execution + approvals:
# - `approval` (default): safe tools run in-process; dangerous/unknown tools require a Telegram reply: `/approve` or `/deny`.
export ABSTRACTGATEWAY_TOOL_MODE="approval"

export ABSTRACT_TELEGRAM_BRIDGE=1
# Bot API (easy, not E2EE). If ABSTRACT_TELEGRAM_BOT_TOKEN is set, transport defaults to bot_api.
export ABSTRACT_TELEGRAM_BOT_TOKEN="..."      # from @BotFather
# Optional: TDLib (E2EE) instead of Bot API:
# export ABSTRACT_TELEGRAM_TRANSPORT="tdlib"
# Optional: override which workflow to run per message.
# Default (when unset): shipped `basic-agent` bundle entrypoint.
# export ABSTRACT_TELEGRAM_BUNDLE_ID="basic-agent"
# export ABSTRACT_TELEGRAM_FLOW_ID="81795ea9"

# Access control (required by default)
# Use /whoami in Telegram to discover your numeric user_id.
export ABSTRACT_TELEGRAM_ALLOWED_USERS="123456789"

# Optional: pairing mode (lets unknown users request access, but requires an admin):
# export ABSTRACT_TELEGRAM_DM_POLICY="pairing"
# export ABSTRACT_TELEGRAM_ADMIN_USERS="123456789"

# Optional: group chat support (disabled by default):
# export ABSTRACT_TELEGRAM_GROUP_POLICY="allowlist"
# export ABSTRACT_TELEGRAM_ALLOWED_CHATS="-100123456789"  # use /whoami inside the group to discover chat_id
# export ABSTRACT_TELEGRAM_REQUIRE_MENTION_IN_GROUPS=1     # default true
```

Then start the gateway:

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

Notes:
- Default LLM routing comes from the execution-host `input.text` capability
  route. Set it with `abstractcore --set-global-default ...` or
  `abstractgateway-config set-default input.text ...`.
- Telegram-only routing override: set `ABSTRACT_TELEGRAM_MODEL="..."` (and optionally `ABSTRACT_TELEGRAM_PROVIDER="..."`) without changing other gateway traffic.
- Durable history limit: `ABSTRACT_TELEGRAM_MAX_HISTORY_MESSAGES` (default: 30).
- STT fallback and vision caption fallback are configured via `abstractcore --config` (audio strategy + vision fallback).
- `/reset` clears the durable session; optional best-effort message deletion is controlled by `ABSTRACT_TELEGRAM_RESET_DELETE_MESSAGES` and `ABSTRACT_TELEGRAM_RESET_DELETE_MAX`. The confirmation text is configurable via `ABSTRACT_TELEGRAM_RESET_MESSAGE`.
- For tool approvals, the bridge will prompt you in chat; reply with `/approve` to run tools, or `/deny` to cancel.

## Step 3: Workflow wiring

Telegram is a thin client: any workflow that reads `prompt`/`context` (like `abstractcode.agent.v1`) and writes a string
to `run.output.answer` or `run.output.response` will work. Durable memory comes from the Telegram `session_id`.

## Step 4: TDLib (E2EE) bootstrap (recommended path)

TDLib requires a real Telegram user account and the TDLib shared library (`tdjson`) installed on the gateway host. You
then authenticate once to create a persistent TDLib session directory.

Because TDLib setup is platform-specific, follow:
- [Guide: Telegram integration](../guide/telegram-integration.md)

## Step 5: Test

Send a Telegram message to the AI contact. You should see:
- gateway emits the event into the session
- your run progresses and sends a reply via tools
- Observer can replay the ledger for the session/run

## See also

- [Guide: Telegram integration](../guide/telegram-integration.md) — configuration and TDLib details

--- docs/scenarios/email-inbox-agent.md ---
# Scenario: Email Inbox Agent (IMAP Bridge + SMTP Replies)

Goal: ingest inbound emails as durable events and let a workflow reply (or take actions) with framework-native email tools.

## High-level architecture

- Inbound: gateway email bridge polls IMAP, stores raw + attachments as artifacts, emits `email.message` events into a
  stable `session_id` per thread.
- Outbound: workflows call `send_email` with centralized SMTP defaults (no repeating host/user per tool call).

## Step 1: Configure email accounts on the tool-execution host

Email tools are account-scoped: IMAP/SMTP host/user are configured on the process that executes tools (gateway local
tools, CLI host, or a tool worker).

For the full configuration matrix (env vs YAML vs AbstractCore config), use the canonical guide in the main framework
workspace:
- [Guide: Email integration](../guide/email-integration.md)

## Step 2: Enable the inbound email bridge on the gateway host

Minimum env vars (plus your email account config):

```bash
export ABSTRACT_EMAIL_BRIDGE=1
export ABSTRACT_EMAIL_POLL_SECONDS=60

export ABSTRACT_EMAIL_FLOW_ID="<bundle_id>:<flow_id>"   # autostart/attach a workflow per thread/session
```

Start the gateway with a persistent `ABSTRACTGATEWAY_DATA_DIR` so bridge state survives restarts.

## Step 3: Wire the workflow

Create a workflow that:
1. handles the `email.message` event
2. reads `payload.email.*` (subject/from/body and artifact-backed attachments)
3. optionally opens attachments via `open_attachment(artifact_id=...)`
4. replies with `send_email(to=..., subject=..., body_text=...)`

## Step 4: Test

Send an email into the mailbox. Expected:
- the bridge emits `email.message`
- a session-run starts (or is resumed) for that thread
- your workflow replies via `send_email`

--- docs/scenarios/phone-thin-client.md ---
# Scenario: Phone Thin Client (iPhone via Web/PWA) + Gateway

Goal: run a thin client UI on a phone that attaches to runs and controls them, while the gateway host owns durability and
execution.

This is especially useful for:
- remote coding/agent sessions from a phone
- observing runs while away from your workstation

## Mental model (thin client)

- The phone does not tick the runtime.
- It renders by replaying/streaming the ledger.
- It acts by sending durable commands (resume/pause/cancel/emit_event).

## Quickstart (LAN dev, no HTTPS)

1. Run the gateway bound to all interfaces (still use auth):
   - `abstractgateway serve --host 0.0.0.0 --port 8080`
2. Run the web UI host on all interfaces (dev server):
   - `npx @abstractframework/code`
3. Allow the dev origin in gateway CORS/origin allowlist.
4. Open the web UI URL on your iPhone Safari and connect it to the gateway.

## Production note (recommended)

For installable iOS PWA behavior, host the web UI over HTTPS and run the gateway behind HTTPS termination (reverse proxy
or tunnel). Restrict `ABSTRACTGATEWAY_ALLOWED_ORIGINS` to the exact UI origin.

## Canonical iPhone guide (deeper)

For a step-by-step iPhone/PWA guide, see:
- [Guide: Web deployment](../guide/deployment-web.md)
- [Guide: iPhone notes](../guide/deployment-iphone.md)

--- docs/installers/README.md ---
# Installers

This directory documents how AbstractFramework is installed on a user's machine: a one-line
bootstrap script per OS that provisions the gateway with [uv](https://docs.astral.sh/uv/), and
the gateway's web console (`/console`) as the guided setup UI. The user-facing instructions are in
[Install](../install.md); these pages explain the design, the contracts and the security model
behind them. The decision is recorded in
[ADR-0038](../adr/0038-script-bootstrap-and-gateway-console-install.md).

## Document map

- [`strategy.md`](strategy.md): the install model and why it is script + console.
- [`user-journeys.md`](user-journeys.md): what happens step by step on macOS, Linux and Windows,
  for first install, apps, engines, upgrade and uninstall.
- [`components.md`](components.md): each component, how it reaches the machine, and what the
  bootstrap does with it.
- [`security-and-os-blocks.md`](security-and-os-blocks.md): Gatekeeper, SmartScreen, execution
  policy, sudo/UAC prompts, loopback binding, and where code signing still applies.
- [`release-and-manifest.md`](release-and-manifest.md): the generated install manifest
  (`install-manifest.json`, schema v2) and how releases update it.
- [`operations-and-support.md`](operations-and-support.md): data and log locations, health
  checks, troubleshooting.
- [`implementation-plan.md`](implementation-plan.md): what is delivered and what comes next.
- `install-manifest.json` / `install-manifest.schema.json`: the generated manifest and its schema.

## Scripts

| OS | Script | One-liner |
|---|---|---|
| macOS, Linux | [`scripts/install.sh`](../../scripts/install.sh) | `curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh \| sh` |
| Windows 10 22H2+ / 11 | [`scripts/install.ps1`](../../scripts/install.ps1) | `powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 \| iex"` |

--- docs/installers/strategy.md ---
# Installer Strategy

AbstractFramework installs as a **one-line script per OS** plus the **AbstractGateway web
console** as the guided UI. The script provisions everything the gateway needs in user space,
starts it on `127.0.0.1`, and opens `/console` in the browser; from there the console configures
providers, engines, models and users. This page explains the model; [Install](../install.md) has
the commands and [ADR-0038](../adr/0038-script-bootstrap-and-gateway-console-install.md) records
the decision.

## The model in one picture

```mermaid
flowchart LR
    U["User pastes one line"] --> S["install.sh / install.ps1"]
    S --> UV["uv (installed if missing)"]
    UV --> PY["Python 3.12 (uv-managed)"]
    UV --> GW["abstractgateway[profile,tray]==pin<br/>(uv tool, ~/.local/bin)"]
    UV -. "--with-apps" .-> NODE["nodejs-wheel (node, npm, npx)"]
    S -. "--with-ollama / --with-lmstudio" .-> V["Vendor installers"]
    S --> SVC["abstractgateway service install<br/>(LaunchAgent / systemd --user / Startup)"]
    SVC --> H["GET /api/health"]
    H --> C["Browser: /console (claim link)"]
    C --> W["First-run wizard: engines, default model, apps"]
```

## Principles

- **No admin rights on the default path.** uv, Python, the gateway and Node all install under the
  user's home. Only optional vendor installers (Ollama on Linux, for example) may ask for sudo or
  UAC, and the script says so before running them.
- **No system Python.** uv downloads a standalone Python 3.12, which satisfies every profile (MLX,
  F5-TTS, vLLM). The script always passes `--python 3.12`.
- **One source of pins.** The gateway version comes from `bootstrap.gateway_version` in the
  generated [install manifest](release-and-manifest.md), which follows the root release pins.
- **Every step has a CLI twin.** The script prints each command; `--print` (`-Print` on Windows)
  shows the whole plan without changing anything; the final summary lists the commands so the
  install can be reproduced by hand. Power users can skip the script entirely.
- **The gateway owns the host integration.** Autostart is `abstractgateway service install`, and
  browser sign-in is a one-time claim link from `abstractgateway-config claim-url` (both in
  gateway 0.3.0 and later). With an older gateway selected through `--pin`, the script starts the
  gateway in the background and shows the admin token file instead.
- **Loopback first.** The gateway binds `127.0.0.1`. Remote hosts use an SSH tunnel or the
  container deployment.
- **Idempotent.** Re-running the script upgrades or repairs in place and keeps the port, profile and
  data directory from the previous run. `--uninstall` removes the service and the uv tools and keeps
  data unless `--purge` is given.

## Profiles

The script picks a profile from the machine and accepts `--profile` to override it.

| Profile | Picked when | Gateway extras | Local engines |
|---|---|---|---|
| `apple` | Apple Silicon, macOS 14 or later | `apple,tray` | MLX/Metal stacks in the gateway environment |
| `gpu` | `nvidia-smi` or `rocminfo` works (Linux; best-effort on Windows) | `gpu,tray` | CUDA/ROCm stacks |
| `light` | anything else | `tray` (Linux: only with a display) | none: remote APIs and endpoint servers (Ollama, LM Studio, vLLM, llama.cpp) |

The bootstrap installs the gateway distribution, which is the framework's deployment entry point
(ADR-0033). The full meta-package (`pip install "abstractframework[<profile>]"`) stays available
for developers who want every library in one environment.

## Where the guided UI lives

The console already covers providers, API keys, capability defaults, users and model downloads.
The first-run wizard adds: host profile, engine detection and one-click installs (with the exact
command shown), a default model filtered to what fits the machine, and app launch commands. The
terminal console (`abstractgateway-console`) offers the same screens on headless hosts.

## Large model assets

Models are not part of the bootstrap. The console's Models tab shows sizes and a fit verdict for
the machine before a download, and downloads run as background jobs with progress.

## Out of scope

Signed native launchers, enterprise MSI/pkg packages and offline bundles are not part of this
model; see [implementation-plan.md](implementation-plan.md). The Mac installer package is not an
exception: it carries no payload and only opens the same bootstrap script in Terminal.

--- docs/installers/user-journeys.md ---
# User Journeys

These flows describe exactly what the bootstrap scripts do. Commands are in
[Install](../install.md); the model is in [strategy.md](strategy.md).

## First install on macOS

With the Mac installer, the user downloads `AbstractFramework-Installer.pkg`, allows it once in
**System Settings > Privacy & Security** (**Open Anyway**: the package is not signed with an Apple
Developer ID), and clicks through the macOS Installer. Its postinstall copies the two `.command`
files to `~/Library/Application Support/AbstractFramework/Installer` and opens
`install.sh --interactive` in Terminal, which asks one question (start at login, default yes) and
then runs the steps below. The one-line path:

1. Paste in Terminal:
   `curl -LsSf https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.sh | sh`
2. **Preflight** (read-only): macOS version and CPU (`sw_vers`, `uname -m`), profile choice
   (`apple` on Apple Silicon with macOS 14+, else `light`), free disk under your home, port 8080
   (the next free port is used and remembered when 8080 is taken).
3. **uv**: installed into `~/.local/bin` by the official astral.sh script when missing
   (`UV_NO_MODIFY_PATH=1`), then `uv tool update-shell` adds `~/.local/bin` to your shell profile
   once.
4. **Python**: `uv python install 3.12`.
5. **Gateway**: `uv tool install --python 3.12 "abstractgateway[apple,tray]==<pin>"`. The
   commands `abstractgateway` and `abstractgateway-config` land in `~/.local/bin`.
6. **Optional** (flags): Node for the apps (`--with-apps` installs the `nodejs-wheel` uv tool when
   no Node 18+ exists), Ollama (`--with-ollama`: the official installer, which may ask for your
   password to link `/usr/local/bin/ollama`), LM Studio (`--with-lmstudio`: the headless `llmster`
   daemon), terminal tools (`--with-console`, `--with-code-cli` through cargo).
7. **Service**: `abstractgateway service install --port 8080` registers a per-user LaunchAgent
   and starts it, so the gateway also starts at login. The LaunchAgent runs plain
   `abstractgateway serve`, so the gateway's Network setting (default `localhost`) decides where it
   listens. With `--no-service` (or "no" to the question), the script stores the setting and starts
   the gateway in the background instead.
8. **Health**: waits up to 180 seconds for `GET /api/health` (the first start loads the local
   engines), with a progress line every 15 seconds.
9. **Sign-in**: `abstractgateway-config claim-url` mints a one-time link valid for 10 minutes on
   this machine; if no link can be created, the script prints the path of the admin token file
   (`~/Library/Application Support/AbstractGateway/auth/bootstrap-admin-token`).
10. **Browser**: opens the claim link (`http://127.0.0.1:8080/console#claim=…`). The first-run guide
    walks through engines (detected on this Mac, with one-click installs), a default model that fits
    the machine, and the apps; the **Models** and **Engines** tabs stay available afterwards.

## First install on Linux

Same one-liner and steps, with these differences:

- Profile: `gpu` when `nvidia-smi` or `rocminfo` works, else `light`. The tray extra is added only
  when a display (`DISPLAY`/`WAYLAND_DISPLAY`) exists.
- Data: `${XDG_DATA_HOME:-~/.local/share}/abstractgateway`.
- Service: a `systemd --user` unit when a user session bus exists
  (`systemctl --user show-environment`). Containers and minimal SSH hosts have none; the script
  then starts the gateway in the background and says so. For always-on servers use the container
  deployment in [Install](../install.md#container-deployment).
- Browser: `xdg-open` when a display exists; otherwise the script prints the URL and an SSH tunnel
  hint (`ssh -L 8080:127.0.0.1:8080 <host>`).
- `--with-ollama` runs Ollama's Linux installer, which uses sudo, installs into `/usr/local` and
  creates a system service. The script announces this before running it.
- Linux ARM64: gateways before 0.3.0 (selected with `--pin`) pull an AbstractCore that caps
  `psutil` below 6, which has no aarch64 wheel, so a C compiler is needed
  (`sudo apt-get install -y gcc`). The preflight warns when none is found.

## First install on Windows

1. In PowerShell:
   `powershell -ExecutionPolicy ByPass -c "irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1 | iex"`
2. **Preflight**: Windows build (22H2/19045 or later), architecture, execution policy set by Group
   Policy (reported and explained), long-path support, free disk, port.
3. **uv**: the official `install.ps1` from astral.sh into `%USERPROFILE%\.local\bin`.
4. **Python and gateway**: `uv python install 3.12`, then
   `uv tool install --python 3.12 "abstractgateway[tray]==<pin>"` (`[gpu,tray]` when `nvidia-smi`
   works).
5. **Optional**: `-WithApps` (nodejs-wheel, no UAC), `-WithOllama` (`winget install Ollama.Ollama
   --scope user`, or Ollama's `install.ps1`), `-WithLmStudio` (`winget install
   ElementLabs.LMStudio --scope user`, or LM Studio's headless `install.ps1`).
6. **Autostart**: `abstractgateway service install` (a Startup-folder entry, experimental on
   Windows). With `-NoService` the gateway starts once in a hidden window instead.
7. **Start, health, sign-in, browser**: hidden-window start, `/api/health`, claim link or the token
   file under `%LOCALAPPDATA%\AbstractGateway\auth\`, then the console opens.

To pass options through the one-liner, use a script block:
`& ([scriptblock]::Create((irm https://raw.githubusercontent.com/lpalbou/AbstractFramework/main/scripts/install.ps1))) -WithApps`.

## Apps

Browser apps (Flow, Code, Observer, Continuum, Entity) run on demand with
`npx -y @abstractframework/<app>`; nothing is installed globally. They talk to the gateway at
`ABSTRACTGATEWAY_URL` (default `http://127.0.0.1:8080`). `--with-apps` only makes sure Node 18+
exists.

## Upgrade and repair

Re-run the same one-liner. The script keeps the previous port and profile, installs the pin from
the current manifest (a no-op when it is already installed), restarts the gateway only when the
package changed, and re-checks health. `uv tool upgrade abstractgateway` and the tray's "Check for
updates" are equivalent for uv-tool installs.

## Uninstall

`sh install.sh --uninstall` (or `install.ps1 -Uninstall`) removes the service or Startup shortcut,
stops the gateway, and uninstalls the `abstractgateway` uv tool (and `nodejs-wheel` when the script
installed it). The data directory is kept unless you add `--purge` (`-Purge`). uv, Ollama and LM
Studio stay installed; remove them with their own uninstallers.

## Dry run

`--print` (`-Print`, or `-WhatIf`) runs the read-only preflight and prints every command the
install would run, then stops.

--- docs/installers/components.md ---
# Components and Packaging Matrix

How each component reaches a user's machine under the [script bootstrap](strategy.md).

## Installed by the bootstrap

| Component | Delivered as | Installed by | Notes |
|---|---|---|---|
| uv | Static binary | astral.sh `install.sh` / `install.ps1` | Skipped when already on the machine. Lives in `~/.local/bin` (`%USERPROFILE%\.local\bin`). |
| Python 3.12 | python-build-standalone | `uv python install 3.12` | Isolated from any system Python. |
| AbstractGateway (+ AbstractCore, AbstractRuntime, AbstractAgent, AbstractMemory as dependencies) | PyPI wheel, uv tool | `uv tool install --python 3.12 "abstractgateway[<profile>,tray]==<pin>"` | Exposes `abstractgateway` and `abstractgateway-config`; `--with-core-cli` also exposes `abstractcore`. |
| Gateway service | LaunchAgent / systemd user unit / Startup entry | `abstractgateway service install` | Default (gateway 0.3.0+). `--no-service`, a Linux host without a user systemd session, or an older `--pin` start the gateway in the background instead (Windows: Startup-folder shortcut). |
| Node.js (optional) | `nodejs-wheel` uv tool | `--with-apps` | Only when no Node 18+ is present; no admin, same bin directory. |

## Run on demand

| Component | Command | Notes |
|---|---|---|
| Gateway web console | `http://127.0.0.1:8080/console` | Built into the gateway; the guided setup UI. |
| Flow editor, Code web, Observer, Continuum, Entity | `npx -y @abstractframework/<app>` | Need Node 18+ and a running gateway. |

## Optional tools (flags)

| Component | Flag | Installed with |
|---|---|---|
| Gateway terminal console | `--with-console` | `cargo install --locked abstractgateway-console` (needs Rust; the script prints the command when cargo is missing) |
| AbstractCode terminal client | `--with-code-cli` | `cargo install --locked abstractcode` |

## Third-party engines

| Engine | Flag | macOS / Linux | Windows | Detection (skip when found) |
|---|---|---|---|---|
| Ollama | `--with-ollama` | `curl -fsSL https://ollama.com/install.sh \| sh` (Linux: sudo, system service) | `winget install Ollama.Ollama --scope user`, else `irm https://ollama.com/install.ps1 \| iex` | `ollama` on PATH or `GET :11434/api/version` |
| LM Studio | `--with-lmstudio` | `curl -fsSL https://lmstudio.ai/install.sh \| bash` (headless daemon; Apple Silicon only on macOS) | `winget install ElementLabs.LMStudio --scope user`, else `irm https://lmstudio.ai/install.ps1 \| iex` | `lms`, `~/.lmstudio/bin/lms`, the app bundle, or `GET :1234/v1/models` |

The console's Engines tab offers the same installs later, with the command shown before it runs.

## Native apps (outside the bootstrap)

| Component | Delivery | Notes |
|---|---|---|
| AbstractAssistant | `pip install "abstractassistant[apple]"`; macOS `.app` build | The `.app` is the component that needs Developer ID signing and notarization. |
| Docker image | `ghcr.io/lpalbou/abstractgateway:<version>` | Server deployments; see [Install](../install.md#container-deployment). |

## Availability rules

- The `apple` profile is refused on anything but Apple Silicon with macOS 14+.
- The `gpu` profile warns (and continues) when no `nvidia-smi`/`rocminfo` works; engines then run
  on CPU.
- Engines that do not support the host (LM Studio on Intel Macs) are skipped with a message.

--- docs/installers/security-and-os-blocks.md ---
# OS Security and Installation Blocks

The bootstrap is a script, not a downloaded application, so the OS gates that block unsigned
installers do not apply to the one-line install. The Mac installer package is a downloaded file,
so Gatekeeper asks the user to allow it once. This page explains what each OS checks, what the
scripts do about it, and where code signing applies.

## macOS (Gatekeeper)

- Gatekeeper checks **quarantined applications** (downloaded by a browser) for a Developer ID
  signature and notarization. Files fetched by `curl` carry no `com.apple.quarantine` attribute,
  so `curl … | sh` and the binaries it installs (uv, the uv-managed Python, wheels) run without a
  Gatekeeper prompt.
- `AbstractFramework-Installer.pkg` is not signed with an Apple Developer ID. A browser download
  is quarantined, so the first double-click is blocked; the user allows it once with **Open
  Anyway** in **System Settings > Privacy & Security**. The package is payload-free: it copies the
  two `.command` files into the user's Library and opens `install.sh` in Terminal, installing
  nothing outside the home folder. `scripts/lib/build_macos_installer.sh` signs, notarizes and
  staples it when a Developer ID Installer identity and a notarytool profile are provided.
- The optional vendor installers (Ollama, LM Studio) are signed and notarized by their vendors.
- The gateway binds `127.0.0.1` by default, so the macOS Application Firewall does not ask to
  accept incoming connections. Choosing `lan` or `internet` in the Network setting binds all
  interfaces, and the firewall may then ask once.

## Windows (SmartScreen and execution policy)

- SmartScreen and Mark-of-the-Web apply to downloaded files. `irm … | iex` downloads the script
  into memory and writes no file, so SmartScreen does not prompt. The script launches `.exe`
  shims created by uv, not downloaded installers.
- **Execution policy** is the only script gate. Pasted commands always run; `.ps1` files do not
  under `Restricted` (the Windows client default) or `AllSigned`. The one-liner uses
  `powershell -ExecutionPolicy ByPass -c "…"`, which applies to that process only and is not
  persisted. Vendor one-liners run in a child `powershell -ExecutionPolicy ByPass` process.
- **Group Policy** (`MachinePolicy`/`UserPolicy`) overrides `-ExecutionPolicy ByPass`. The script
  detects an `AllSigned` or `Restricted` policy set this way and explains it: the pasted one-liner
  still works, a saved `install.ps1` does not, and managed machines may need an administrator.
- The autostart fallback is a Startup-folder shortcut that runs `powershell.exe -Command` (not a
  `.ps1` file), so the execution policy does not block it. No UAC prompt is needed: uv, Python,
  Node (`nodejs-wheel`), Ollama (`--scope user`) and LM Studio (`--scope user`) all install per
  user. `winget install OpenJS.NodeJS.LTS` is deliberately not used because it installs
  machine-wide and triggers UAC.

## Linux

- Nothing in the default path needs root. Everything installs under `~/.local` and
  `${XDG_DATA_HOME:-~/.local/share}`.
- `--with-ollama` runs Ollama's official installer, which **uses sudo**: it installs into
  `/usr/local`, creates an `ollama` user and a system `ollama.service`. The script prints this
  before running it. LM Studio's headless installer may ask for sudo to install `libatomic1`.
- On ARM64 without a C compiler, gateways before 0.3.0 (selected with `--pin`) fail while building
  `psutil`; install `gcc` from your distribution first. The pinned gateway needs no compiler.

## Sudo and UAC prompts at a glance

| Step | macOS | Linux | Windows |
|---|---|---|---|
| uv, Python, gateway, nodejs-wheel | none | none | none |
| Service / autostart | none (LaunchAgent) | none (`systemd --user`); `loginctl enable-linger` for always-on may need polkit | none (Startup folder) |
| `--with-ollama` | may ask for your password (`/usr/local/bin/ollama` link) | sudo | none (user scope) |
| `--with-lmstudio` | none | may ask sudo for `libatomic1` | none (user scope) |

## Network exposure

- The gateway binds `127.0.0.1` in every bootstrap path. Exposure beyond this machine is an
  explicit choice in the gateway's Network setting (`abstractgateway network set lan|internet`),
  which turns user accounts on; see [Network setting](../install.md#network-setting-who-can-reach-the-gateway).
- One-time sign-in links (`abstractgateway-config claim-url`) are single use, expire after 10
  minutes and are accepted only from a loopback client.
- The admin token file is written with mode `0600`; the scripts print its path, not its content.
- Remote access: use an SSH tunnel (`ssh -L 8080:127.0.0.1:8080 <host>`) or the container
  deployment with explicit auth.

## Integrity

- Every download uses HTTPS from the vendor's official host (astral.sh, pypi.org, ollama.com,
  lmstudio.ai, raw.githubusercontent.com). uv verifies package hashes from the index.
- Review before running: fetch the script, read it, then run it, or use `--print` to see every
  command without changing anything.

## Where code signing still applies

Native double-click artifacts need signatures to open without a prompt: the Mac installer package
(Developer ID Installer + notarization; unsigned releases ask for **Open Anyway** once), the
AbstractAssistant `.app` (Developer ID + notarization + stapling) and any future Windows
`.exe`/`.msi` launcher (Authenticode). The one-line bootstrap and everything it installs need no
signature of ours.

## GPU drivers

The scripts never install drivers. They offer the `gpu` profile only when `nvidia-smi` or
`rocminfo` works; otherwise local engines fall back to CPU with a warning.

--- docs/installers/release-and-manifest.md ---
# Release and Install Manifest

The install manifest is the machine-readable contract between a release and everything that
installs it: the bootstrap scripts, the docs, and any tool that wants the pinned versions. It is
generated from the root `abstractframework` release pins, never edited by hand:

```bash
abstractframework manifest                                   # print it
abstractframework manifest --write docs/installers/install-manifest.json
abstractframework manifest --check docs/installers/install-manifest.json
```

## Fields (schema version 2)

`install-manifest.json` is generated from `abstractframework/__init__.py` (`__version__`,
`RELEASE_VERSIONS`, `PACKAGE_DISTRIBUTIONS`, `NPM_RELEASE_VERSIONS`) and
`abstractframework/install_manifest.py`, and validated by `install-manifest.schema.json`.

| Field | Description |
|---|---|
| `schema_version` | `2` |
| `minimum_installer_version` | Oldest consumer that understands this manifest |
| `framework` | The `abstractframework` distribution, its version and `python_requires` |
| `source` | Repository URL and the Python symbol the pins come from |
| `profiles` | `light`, `apple`, `gpu`: pip requirement, platforms, prerequisites, whether local inference is installed |
| `python_packages` | Every pinned PyPI package: id, distribution name, version |
| `npm_apps` | Every npm app released with this version and its `npx` command |
| `bootstrap` | What the one-line scripts install: `gateway_version`, `python` (`3.12`), `tool_requirement`, `profile_extras`, `optional_extras`, `default_port`, script URLs and one-liners, the shared flag table (`sh`/`ps`/env names), the step list, and the uninstall commands |
| `post_install` | Console-first next steps: `entrypoint: "console"`, the `serve` command on `127.0.0.1`, `health_url`, `console_url`, the `claim` command and its token-file fallback, the `service` command, `doctor`, and the `npx` app commands |
| `security` | Whether secrets or signed native artifacts are present |

`scripts/install.sh` reads `bootstrap.gateway_version` from the manifest next to it and embeds the
same value for `curl | sh` use; `scripts/install.ps1` does the same. `python -m pytest -q` and
`bash scripts/tests/test_inventory.sh` fail when the scripts, the manifest and the root pins
disagree.

## Updating it for a release

1. Release the lower packages first, in the order of
   [ADR-0034](../adr/0034-framework-release-sequence-and-gates.md).
2. Change the pins in `pyproject.toml` and the matching dictionaries in
   `abstractframework/__init__.py`.
3. Regenerate the manifest: `abstractframework manifest --write docs/installers/install-manifest.json`
   (from the repository checkout, so it reads the edited source).
4. Update the embedded pins in `scripts/install.sh` (`AF_GATEWAY_PIN_DEFAULT`, `AF_NPM_APPS`,
   crates) and `scripts/install.ps1` (`$AfGatewayPinDefault`, `$AfNpmApps`, crates).
5. Run `python -m pytest -q` and `bash scripts/tests/test_inventory.sh`. CI's `bootstrap-smoke`
   job installs the published pin on Ubuntu, macOS and Windows.

## Release assets

The bootstrap installs PyPI and npm packages and vendor installers. Each GitHub release of this
repository carries `AbstractFramework-Installer.pkg`, built from the release tag by
`scripts/lib/build_macos_installer.sh --version <version>` so the embedded `install.sh` carries
that release's gateway pin; `releases/latest/download/AbstractFramework-Installer.pkg` serves the
newest one. The package is unsigned unless the build is given a Developer ID
(`AF_PKG_SIGN_IDENTITY`, `AF_NOTARY_PROFILE`). Native apps that need signing (the
AbstractAssistant `.app`) publish their artifacts on their own release pages; see
[security-and-os-blocks.md](security-and-os-blocks.md#where-code-signing-still-applies).

--- docs/installers/operations-and-support.md ---
# Operations and Support

Where a bootstrap install keeps its files, how to check it, and how to fix common problems. The
install itself is described in [user-journeys.md](user-journeys.md).

## Locations

| What | macOS | Linux | Windows |
|---|---|---|---|
| uv | `~/.local/bin/uv` | `~/.local/bin/uv` | `%USERPROFILE%\.local\bin\uv.exe` |
| Gateway commands (uv tool shims) | `~/.local/bin` | `~/.local/bin` | `%USERPROFILE%\.local\bin` |
| Gateway environment | `~/.local/share/uv/tools/abstractgateway` | same | `%APPDATA%\uv\data\tools\abstractgateway` |
| Gateway data | `~/Library/Application Support/AbstractGateway` | `${XDG_DATA_HOME:-~/.local/share}/abstractgateway` | `%LOCALAPPDATA%\AbstractGateway` |
| Admin token (0600) | `<data>/auth/bootstrap-admin-token` | same | same |
| Logs | `<data>/logs/install-*.log`; background start `<data>/logs/gateway.log`; login item `~/Library/Logs/AbstractGateway/gateway.{out,err}.log` | `<data>/logs/install-*.log`, `<data>/logs/gateway.log` | `<data>\logs\gateway.err.log`, `install-*.log` |
| Bootstrap state (port, mode, profile) | `<data>/bootstrap.env` | same | same |
| AbstractCore config | `~/.abstractcore/config/abstractcore.json` | same | `%USERPROFILE%\.abstractcore\config\abstractcore.json` |

`--data-dir` (or `AF_DATA_DIR`) moves the gateway data directory.

## Health checks

```bash
curl http://127.0.0.1:8080/api/health          # the gateway answers
uvx abstractframework doctor                    # host, tools, gateway, engines
abstractgateway-config status --json            # data dir, auth, defaults
```

`abstractframework doctor` only reads: it sends `GET /api/health` to `ABSTRACTGATEWAY_URL`
(default `http://127.0.0.1:8080`), `GET /api/version` to Ollama and `GET /v1/models` to LM Studio.
Add `--json` for tooling and `--no-network` to skip the HTTP probes.

## Troubleshooting

- **`abstractgateway: command not found`**: open a new terminal (the PATH change applies to new
  shells) or call `~/.local/bin/abstractgateway` directly.
- **Port 8080 in use**: the script picks the next free port and records it in `bootstrap.env`;
  pass `--port` to choose one. The summary prints the URL.
- **Gateway exited during start**: the script prints the last log lines; the full log is
  `<data>/logs/gateway.log`.
- **No gateway after reboot**: the gateway starts at login only when a service or Startup entry was
  registered (see the `Mode:` line of the summary). Re-run the script, or start it by hand.
- **Browser apps cannot connect**: check `ABSTRACTGATEWAY_URL` and that the gateway is healthy.
- **Local engine not reachable**: start Ollama or the LM Studio server; the console's Engines tab
  and `abstractframework doctor` show what is reachable.
- **Linux ARM64 build error for psutil** (gateways before 0.3.0, selected with `--pin`): install a
  C compiler (`sudo apt-get install -y gcc`) and re-run.

## Uninstall

`sh uninstall.sh` (or the **Uninstall AbstractFramework.command** file) asks before removing
anything; `sh install.sh --uninstall [--purge] [--remove-uv]` or `install.ps1 -Uninstall [-Purge]`
do the same without the questions. Data is kept unless you purge it. See
[Remove AbstractFramework](../install.md#remove-abstractframework).

More symptoms and fixes: [Troubleshooting](../troubleshooting.md).

--- docs/installers/implementation-plan.md ---
# Implementation Plan

The install experience follows [ADR-0038](../adr/0038-script-bootstrap-and-gateway-console-install.md):
a one-line bootstrap per OS plus the gateway console. This page tracks what is delivered and what
comes next.

## Delivered

- `scripts/install.sh` (macOS, Linux; POSIX `sh`, tested with dash, bash and zsh) and
  `scripts/install.ps1` (Windows PowerShell 5.1 and PowerShell 7): preflight, profile selection,
  uv + Python 3.12, `uv tool install` of the pinned gateway, optional Node/cargo tools/Ollama/LM
  Studio, service or background start, health wait, console sign-in, dry run, idempotent re-runs,
  uninstall.
- Install manifest schema v2: a `bootstrap` section (gateway pin, Python, extras per profile,
  script URLs, flags) and a console-first `post_install`.
- `abstractframework doctor`: Python range, macOS/Apple Silicon for `apple`, GPU tools, uv, Node
  18+ (system or `nodejs-wheel`), disk, and read-only probes of the gateway, Ollama and LM Studio.
- CI job `bootstrap-smoke` on Ubuntu, macOS and Windows.

## Gateway features the scripts use

The pinned gateway (0.4.3) provides all of them. The scripts still detect each one, so an older
gateway selected with `--pin` falls back as the last column says.

| Feature | Command | Without it (older `--pin`) |
|---|---|---|
| Per-user service (LaunchAgent, `systemd --user`, Windows logon) | `abstractgateway service install --port N`, `service uninstall` | Background start; Windows adds a Startup-folder shortcut |
| One-time console sign-in link | `abstractgateway-config claim-url --base-url URL` | The admin token file path is shown |
| First-run wizard in `/console` | opened by the claim link | Console sign-in form |
| Per-OS default data directory and loopback user auth | `abstractgateway serve` | The scripts set `ABSTRACTGATEWAY_DATA_DIR` and `ABSTRACTGATEWAY_USER_AUTH=1` |

## Next

- Validate `install.ps1` on physical Windows 10 22H2 and Windows 11 machines (x64 and ARM64),
  including the Startup shortcut, hidden-window start and winget installs.
- Host the scripts at a short URL (`abstractframework.ai/install.sh`, `…/install.ps1`).
- Validate `abstractgateway service install` end to end on each OS in CI (the smoke job runs with
  `--no-service`).
- Signed AbstractAssistant `.app`.

## Not planned in this model

A separate GUI installer manager, signed per-app installers for every component, enterprise
MSI/pkg packages and offline bundles. They would need a new ADR.

--- docs/comparisons/README.md ---
# Comparisons

This folder contains objective, architecture-focused comparisons between AbstractFramework and other approaches.

Guidelines:
- Focus on design trade-offs (durability, observability, deployment, security), not marketing.
- Prefer framework-level patterns over provider-specific hacks.

For a quick overview, start with the main [FAQ](../faq.md).

