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
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")
Diagram

๐ŸŒ Client

๐Ÿ”’ 127.0.0.1:PORT

๐Ÿ’ฌ /chat

โš™๏ธ /train/*

โค๏ธ /health

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
    curl http://127.0.0.1:PORT/health
  3. Call a route

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


Routes

RoutePurpose
GET /healthIncludes data_dir, version, shell_version, and agents_version
GET /settingsCurrent settings; secrets returned masked as bullets
POST /settingsMerge 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 /frameworksWhich agent frameworks this install can run, plus install hints for the ones it cannot. Returns {available, known, hints, default, error}. See Agent Framework.
POST /chatSSE stream (covered in Chat & Streaming)
POST /approve/{approval_id}Approve or deny a pending tool call; missing body means deny
GET /mcp, POST /mcpList and manage MCP servers
GET /media/capabilitiesWhat media generation is available; now returns video_models and a video_hint
GET /media/video/modelsThe video model catalog with a per-model configured flag
POST /media/videoGenerate a video; optional model field defaults to replicate/minimax/video-01. See Video generation
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/startStart 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=NReplay from a cursor then follow; 400 on a malformed cursor
GET /train/statusThe live run and a tail of its metrics
GET /train/runsFinished runs, bounded to the last 50

GET /frameworks response shape

json
{
  "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.

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

json
{
  "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
// 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
{ "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.

Diagram

Allowed

Refused

๐ŸŒ Browser Request

๐Ÿ” Origin Check

โœ… 200 + echoed ACAO

โ›” 403 ยท no CORS

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.

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


Status Codes

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

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

Bounded Responses

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

RouteBound
GET /train/runsLast 50 finished runs (MAX_HISTORY=50)
GET /train/statusUp 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
{
  "ok": true,
  "version": 2,
  "shell_version": "unknown",
  "agents_version": "1.7.2",
  "data_dir": "/Users/you/Library/Application Support/PraisonAI"
}

Best Practices

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

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

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

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

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