Skip to main content
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.
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.
3

Call a route

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

Routes

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

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.

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. 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. 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.
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.
/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.

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.

Best Practices

The path can be overridden by an environment variable. Ask /health for data_dir instead of assuming the platform default.
POST /train/stop/{run_id} names the run to cancel, so a stale client cannot cancel a newer run. Always include the id.
GET /train/progress replays from cursor=N, so a dropped connection resumes without missing events. A malformed cursor returns 400.
POST /approve/{approval_id} defaults to deny when the body is absent or malformed — never rely on an empty body to allow.
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.

Fine-Tuning

The /train/* routes in context

Environment Variables

What shifts data_dir and the secret store

Chat & Streaming

The /chat SSE stream

Data & Privacy

Why everything stays on loopback