127.0.0.1 — the same surface the app uses, stable enough to wire an alternate UI or an integration against.
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 reach127.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.
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
Read data_dir from /health, not the default
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.Stop by run id
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.Reconnect with a cursor
Reconnect with a cursor
GET /train/progress replays from cursor=N, so a dropped connection resumes without missing events. A malformed cursor returns 400.Treat a missing approval body as deny
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.Send an Origin the engine trusts
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
Fine-Tuning
The
/train/* routes in contextEnvironment Variables
What shifts
data_dir and the secret storeChat & Streaming
The
/chat SSE streamData & Privacy
Why everything stays on loopback

