> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Engine API

> The loopback HTTP surface the Desktop engine exposes on 127.0.0.1

The Desktop engine is a small HTTP server on `127.0.0.1` — the same surface the app uses, stable enough to wire an alternate UI or an integration against.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Assistant",
    instructions="You are a helpful assistant.",
)
# The Desktop app talks to an engine exactly like this one over 127.0.0.1.
agent.start("Hello")
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Client[🌐 Client] --> Loop[🔒 127.0.0.1:PORT]
    Loop --> Chat[💬 /chat]
    Loop --> Train[⚙️ /train/*]
    Loop --> Health[❤️ /health]

    classDef client fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef loop fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef route fill:#10B981,stroke:#7C90A0,color:#fff

    class Client client
    class Loop loop
    class Chat,Train,Health route
```

Every endpoint binds to `127.0.0.1`, and requests from a browser origin the engine does not recognise are refused with `403`. Nothing leaves your machine unless the model itself does.

## Quick Start

**1. Find the port**

The app prints `engine :PORT` when the engine is healthy. That is the loopback port every route below uses.

**2. Probe /health**

`GET /health` confirms the engine is up and returns the true data directory.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
curl http://127.0.0.1:PORT/health
```

**3. Call a route**

Every response is JSON except `/chat` (SSE) and `/train/progress` (SSE).

***

## Routes

| Route | Purpose |
| - | - |
| `GET /health` | Includes `data_dir`, `version`, `shell_version`, and `agents_version` |
| `GET /settings` | Current settings; secrets returned masked as bullets |
| `POST /settings` | Merge a settings patch; the response uses the same redaction as `GET` — a write to any field returns the full settings dict with secrets masked |
| `GET /frameworks` | Which agent frameworks this install can run, plus install hints for the ones it cannot. Returns `{available, known, hints, default, error}`. See [Agent Framework](https://praison.ai/docs/features/desktop/frameworks.md). |
| `POST /chat` | SSE stream (covered in [Chat & Streaming](https://praison.ai/docs/features/desktop/chat.md)) |
| `POST /approve/{approval_id}` | Approve or deny a pending tool call; missing body means deny |
| `GET /mcp`, `POST /mcp` | List and manage MCP servers |
| `GET /media/capabilities` | What media generation is available; now returns `video_models` and a `video_hint` |
| `GET /media/video/models` | The video model catalog with a per-model `configured` flag |
| `POST /media/video` | Generate a video; optional `model` field defaults to `replicate/minimax/video-01`. See [Video generation](https://praison.ai/docs/features/desktop/video.md) |
| `GET /search?q=` | Full-text search across transcripts |
| `POST /fork/{cid}/{idx}` | Fork a conversation at a message |
| `DELETE /messages/{cid}/{idx}` | Delete a message |
| `POST /project/{cid}` | Assign a conversation to a project |
| `DELETE /chats/{cid}` | Delete a conversation |
| `POST /train/start` | Start a fine-tune; `400` on invalid method or column set |
| `POST /train/stop/{run_id}` | Kill the named run; stale-tab safe |
| `GET /train/progress?run=…&cursor=N` | Replay from a cursor then follow; `400` on a malformed cursor |
| `GET /train/status` | The live run and a tail of its metrics |
| `GET /train/runs` | Finished runs, bounded to the last 50 |

> [!NOTE]
>
> Both `/settings` exits share a single `redacted()` helper, so `GET` and `POST` mask secrets identically — a `POST` that only changed `theme` still returns the `api_key` as bullets. Redaction is driven by the `SECRET_KEYS` list, so future secrets are covered without new code. An empty secret comes back as `""`, not bullets, so a client can tell "set" from "unset". What is **stored** is unchanged; only what leaves the process is masked.

### `GET /frameworks` response shape

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "available": ["praisonai"],
  "known": ["praisonai", "crewai", "autogen", "langgraph", "agno", "google-adk", "openai-agents", "pydantic-ai"],
  "hints": {
    "crewai": "pip install \"praisonai-frameworks[crewai]\"",
    "…": "…"
  },
  "default": "praisonai",
  "error": null
}
```

`available` is always at least `["praisonai"]`. When the `praisonai` wrapper is missing from the engine venv the endpoint still answers `200` with an `error` string explaining why, so the UI can report capability instead of failing the request.

***

## Media routes

The `/media/*` routes back the Video tab. Generation is a single synchronous `POST /media/video` — there is no job store or polling. Full walkthrough: [Video generation](https://praison.ai/docs/features/desktop/video.md).

`GET /media/video/models` lists the catalog with a per-model `configured` flag:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "ok": true,
  "models": [
    { "id": "replicate/minimax/video-01", "display_name": "MiniMax video-01 (Replicate)", "provider": "replicate", "configured": true },
    { "id": "openai/sora-2", "display_name": "OpenAI Sora 2", "provider": "openai", "configured": false }
  ]
}
```

`POST /media/video` accepts `prompt` and an optional `model`. The Replicate path returns a hosted `url`; SDK paths (Sora, Veo) return a `data_url` and a null `url`:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
// Request
{ "prompt": "A cat playing with yarn", "model": "openai/sora-2" }

// SDK response
{ "id": "vid_…", "path": ".../media-output/videos/vid_….mp4",
  "url": null, "data_url": "data:video/mp4;base64,…", "model": "openai/sora-2", "prompt": "…" }
```

`GET /media/capabilities` includes `video_models` and a `video_hint` alongside the image fields:

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{ "ok": true, "video": true,
  "video_models": [ { "id": "replicate/minimax/video-01", "configured": true } ],
  "video_hint": "Set a provider key … (REPLICATE_API_TOKEN for MiniMax, OPENAI_API_KEY for Sora, GEMINI_API_KEY for Veo)." }
```

***

## Browser Origin Gate

The engine refuses browser origins it does not recognise, because loopback keeps other machines out but not other pages a user has open.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Req[🌐 Browser Request] --> Check{🔍 Origin Check}
    Check -->|Allowed| Ok[✅ 200 + echoed ACAO]
    Check -->|Refused| No[⛔ 403 · no CORS]

    classDef req fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef check fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef ok fill:#10B981,stroke:#7C90A0,color:#fff
    classDef no fill:#189AB4,stroke:#7C90A0,color:#fff

    class Req req
    class Check check
    class Ok ok
    class No no
```

A loopback address keeps other machines out, but any page open in the user's browser can reach `127.0.0.1` with a two-line `fetch`. Matching is on the **parsed hostname**, so lookalike hosts never slip through.

| Origin | Verdict |
| - | - |
| `tauri://localhost` (macOS, Linux webview) | Allowed |
| `http://tauri.localhost` (Windows, Android webview) | Allowed |
| `http://localhost:<port>` / `http://127.0.0.1:<port>` (dev server) | Allowed |
| *No `Origin` header* (CLI, `curl`, tests, local processes) | Allowed |
| Any other `Origin` (a real web page) | **403** |
| `Origin: null` (sandboxed iframe, `file://`) | **403** |
| Lookalike hosts (`http://tauri.localhost.evil.com`, `http://localhost.evil.com`) | **403** |
| Bogus scheme (`javascript:…`) | **403** |

Allowed responses **echo the caller's own `Origin`** (with `Vary: Origin`) instead of `*` — `Access-Control-Allow-Origin: *` no longer appears anywhere on the surface.

A refusal carries **no CORS headers**, so the calling page cannot read the reply either. `OPTIONS` is gated the same way — a refused preflight returns `403`, which is what stops the follow-up request in a real browser.

> [!WARNING]
>
> This closes the **browser** vector only. A local process on the same machine — a script, `curl`, or any program with no `Origin` header — can still call these routes. Locking that down needs a per-launch token the Rust shell carries into the webview; that follow-up is out of scope for this change.

***

## Status Codes

The engine returns a status that matches the cause, so a client shows the right thing instead of a bare drop.

| Code | Meaning |
| - | - |
| `200` | Success |
| `400` | Bad request — missing fields, malformed `/train/progress?cursor=`, or a rejected run id |
| `403` | Browser origin refused (the engine does not recognise it) |
| `404` | No such run, chat, or pending approval |
| `409` | A training run is already live — one GPU runs one job |
| `500` | The runs directory could not be created |

> [!NOTE]
>
> `/train/progress` with a malformed `cursor` now returns `400 Bad Request`. It used to drop the connection with no response at all.

***

## Bounded Responses

The training routes read from bounded buffers, so a long-running engine cannot grow without limit.

| Route | Bound |
| - | - |
| `GET /train/runs` | Last 50 finished runs (`MAX_HISTORY=50`) |
| `GET /train/status` | Up to the last 500 metric points; the series itself is capped at `MAX_METRICS=5000` |

***

## The /health Response

`/health` reports the true data directory, honouring `PRAISONAI_DESKTOP_HOME`, `XDG_DATA_HOME`, and `APPDATA` — so the app can copy the real path rather than reproduce a default.

```json theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
{
  "ok": true,
  "version": 2,
  "shell_version": "unknown",
  "agents_version": "1.7.2",
  "data_dir": "/Users/you/Library/Application Support/PraisonAI"
}
```

***

## Best Practices

> [accordion] Read data\_dir from /health, not the default
>
> The path can be overridden by an environment variable. Ask `/health` for `data_dir` instead of assuming the platform default.

> [accordion] Stop by run id
>
> `POST /train/stop/{run_id}` names the run to cancel, so a stale client cannot cancel a newer run. Always include the id.

> [accordion] Reconnect with a cursor
>
> `GET /train/progress` replays from `cursor=N`, so a dropped connection resumes without missing events. A malformed cursor returns `400`.

> [accordion] Treat a missing approval body as deny
>
> `POST /approve/{approval_id}` defaults to deny when the body is absent or malformed — never rely on an empty body to allow.

> [accordion] Send an Origin the engine trusts
>
> Third-party clients should call from a webview served on `localhost` (dev server) or the Tauri webview origin; a browser tab on any other origin gets `403`. A local process (CLI, script, `curl`) with no `Origin` header is allowed.

***

## Related

> [card] [Fine-Tuning](https://praison.ai/docs/features/desktop/fine-tune.md)
>
> The `/train/*` routes in context

> [card] [Environment Variables](https://praison.ai/docs/features/desktop/environment-variables.md)
>
> What shifts `data_dir` and the secret store

> [card] [Chat & Streaming](https://praison.ai/docs/features/desktop/chat.md)
>
> The `/chat` SSE stream

> [card] [Video generation](https://praison.ai/docs/features/desktop/video.md)
>
> The `/media/*` routes behind the Video tab

> [card] [Data & Privacy](https://praison.ai/docs/features/desktop/data.md)
>
> Why everything stays on loopback


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.