Sovrant.Server is an ASP.NET Core minimal-API HTTP server that wraps the sovrant engine and exposes an OpenAI-compatible interface. It is designed to sit behind a Node.js proxy (e.g. on Replit) so that a frontend application can stream chat responses without directly holding LLM credentials.
# Typical
LLM_API_KEY=sk-... \
LLM_BASE_URL=https://api.openai.com/v1 \
dotnet run --project src/Sovrant.ServerOn first run, register an admin user via POST /v1/auth/register (or use the desktop/web setup wizard). All subsequent API calls authenticate with that user's svt_* bearer token.
The server binds to http://127.0.0.1:5200 by default.
Port conflict:
launchSettings.jsondeclares port5091which Kestrel immediately overrides with5200. If you start a second instance before the first has released the socket you will seeSocketException (10048): address already in use. Always stop the running instance first (pkill -f Sovrant.Serveron Linux/macOS,Stop-Process -Name dotneton Windows) or setSOVRANT_PORTto a different port for the second instance.
| Variable | Required | Default | Description |
|---|---|---|---|
LLM_API_KEY |
Yes | — | API key forwarded to the LLM provider. Aliases: OPENAI_API_KEY, PROVIDER_API_KEY (checked in order) |
LLM_BASE_URL |
No | https://api.openai.com/v1 |
LLM provider base URL. Alias: OPENAI_BASE_URL |
SOVRANT_PORT |
No | 5200 |
TCP port Kestrel listens on |
PROVIDER_BASE_URL |
No | — | Enables the native messages API provider (/v1/messages format, e.g. Anthropic direct) |
PROVIDER_API_KEY |
No | — | API key for the native messages API provider |
OLLAMA_BASE_URL |
No | — | Enables the local Ollama provider when set (e.g. http://localhost:11434/v1) |
ROUTER_MODE |
No | Smart |
Smart (latency/cost scoring) or Fixed (always first provider) |
ROUTER_STRATEGY |
No | Balanced |
Balanced, Latency, or Cost |
AGENT_MODE |
No | isolated |
isolated (process-per-agent stdio, default) or shared (in-process async). Controls which IOrchestrationSystem backend is used by team tools (TeamCreate, TeamDelegate, etc.). |
LLM_WEB_SEARCH |
No | false |
Set to true to route through OpenAI Responses API with web_search_preview. No Brave/FireCrawl key required. |
BRAVE_API_KEY |
No | — | Enables WebSearch via Brave Search API |
FIRECRAWL_API_KEY |
No | — | Enables WebSearch via FireCrawl (fallback if BRAVE_API_KEY not set) |
SOVRANT_SESSION_TTL_SECONDS |
No | 3600 |
Idle session TTL in seconds before automatic eviction from the in-memory pool |
SOVRANT_MAX_SESSIONS |
No | 500 |
Maximum active sessions in the pool. LRU eviction when exceeded. |
SOVRANT_LOG_LEVEL |
No | Information |
Minimum log level: Verbose, Debug, Information, Warning, Error, Fatal |
SOVRANT_LOG_FILE |
No | ~/.sovrant/logs/sovrant-{Date}.log |
Rolling file path pattern. Empty string disables file logging. |
SOVRANT_LOG_CONSOLE |
No | true |
Write logs to stdout. Set to false to silence console output. |
SOVRANT_LOG_FORMAT |
No | text |
text (human-readable) or json (structured) |
SOVRANT_RATE_LIMIT_RPM |
No | 60 |
Per-session request rate limit (requests per minute). Keyed on X-Session-Id header or client IP. Returns 429 when exceeded. |
SOVRANT_DB_PATH |
No | ~/.sovrant/data/sovrant.db |
SQLite database file path. CLI and server share the same database. |
SOVRANT_USER_ID |
No | OS username | User identity for session ownership and audit logging |
SOVRANT_SESSION_JSONL |
No | false |
Set to true to also write sessions to legacy JSONL files (dual-write for migration) |
SOVRANT_AUDIT_JSONL |
No | false |
Set to true to also write audit events to legacy JSONL files (dual-write for migration) |
SOVRANT_UNSAFE_DONTASK |
No | false |
Set to true to disable graduated tool tier enforcement in DontAsk mode (auto-approve all tools, including Dangerous/Escalation) |
SOVRANT_RUNTIME_MODE |
No | embedded |
Controls how Sovrant.Web connects to the runtime. embedded runs the agentic loop in-process; remote connects to a Sovrant.Server instance via SignalR + REST. |
SOVRANT_SERVER_URL |
No | http://localhost:5200 |
Base URL of the remote Sovrant.Server instance. Required when SOVRANT_RUNTIME_MODE=remote. |
SOVRANT_API_TOKEN |
No | — | Bearer token sent to the remote server for auth. Required when SOVRANT_RUNTIME_MODE=remote. |
SOVRANT_COST_PROVIDER |
No | openrouter |
Cost tracking provider. Set to none to disable cost tracking. |
Every request (except GET /health, CORS preflight OPTIONS, and the auth endpoints — /v1/auth/login, /v1/auth/register, /v1/auth/use-reset-token, /v1/auth/registration/status) must include:
Authorization: Bearer <token>
Tokens are per-user strings of the form svt_<base64url-secret>, issued to a specific user via POST /v1/users/me/tokens (self-service) or POST /v1/users/{id}/tokens (admin). Tokens are stored as SHA-256 hashes in the api_tokens table; the plaintext is returned exactly once at issuance and is never recoverable. BearerTokenMiddleware resolves the caller's user_id and role from the token and stamps them onto HttpContext for downstream ownership checks. Revoked, expired, malformed, or missing tokens return 401 Unauthorized. Token comparison is timing-safe (CryptographicOperations.FixedTimeEquals).
A request is treated as admin when the authenticating user has users.role = 'admin'. Admins bypass ownership scoping on session/usage routes (see below) and can manage any user via /v1/users/{id}/*. Non-admin callers see only their own data.
All session-bearing endpoints — GET /v1/sessions, GET /v1/sessions/{id}, DELETE /v1/sessions/{id}, GET /v1/sessions/{id}/export, GET/PUT /v1/sessions/{id}/config, and GET /v1/usage — filter results by the caller's user_id. A non-admin caller attempting to read, modify, or delete another user's session receives 404 Not Found (not 403) so that session IDs cannot be enumerated across users. Admin callers see the full set.
Ownership is stamped at the SqliteSessionStore layer with INSERT OR IGNORE semantics: the first append to a given session_id records the owner in sessions.user_id; subsequent appends from any caller cannot overwrite it. The POST /v1/chat/completions path performs a pre-flight GetOwnerAsync check and rejects attempts to attach to an already-owned session belonging to a different user. The runtime session pool is keyed on {session_id}##{owner_user_id} so two concurrent users cannot race to share a pool entry.
- The SQLite file is created with mode
0600(user-only) on first initialization. PRAGMA secure_delete = ONso revoked tokens and deleted session entries do not linger in free pages.ISqliteConnectionFactory.CreateReadOnlyConnection()opens the database withMode=ReadOnlyandPRAGMA query_only = ONfor read-only code paths that must not mutate state.- Token hashes are 32 raw bytes (SHA-256) — no reversible encoding, no pepper stored alongside.
GET /health
Unauthenticated. Returns {"status":"ok"}. Safe for load-balancer probes.
OpenAI-compatible. Streams by default.
Request body
{
"model": "gpt-4o",
"messages": [
{ "role": "user", "content": "Hello" }
],
"stream": true,
"max_tokens": 4096,
"session_id": "optional-session-id"
}Per-request credential headers (optional):
X-LLM-Api-Key: sk-...
X-LLM-Base-Url: https://api.openai.com/v1
model— overrides server default for this requestsession_id— if supplied the server resumes that session from the SQLite databasestream— defaults totrue; setfalsefor a single JSON responseX-LLM-Api-Keyheader — per-request LLM API key; overrides the server's globalLLM_API_KEYfor this call only. The server never logs, persists, or includes this value in error responses.X-LLM-Base-Urlheader — per-request LLM base URL; overrides the server's globalLLM_BASE_URLfor this call only. When combined withX-LLM-Api-Key, creates a request-scoped provider.
When X-LLM-Api-Key is supplied, the server creates a request-scoped OpenAiCompatProvider using the provided credentials. Sessions are isolated by a composite key ({session_id}::{provider_name}) so two clients with different keys sharing the same session_id will not collide.
Streaming response (SSE)
data: {"id":"...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hi"},"index":0}]}
data: {"id":"...","choices":[{"delta":{},"finish_reason":"stop"}],"sovrant":{"event":"tool_use","tool_name":"Bash","tool_use_id":"tu_123","is_error":false}}
data: [DONE]
The sovrant extension field appears on tool-related chunks and carries:
| Field | Type | Description |
|---|---|---|
event |
string | tool_use, tool_result, text, clarification, plan_presented, or step_progress |
tool_name |
string? | Name of the tool being called |
tool_use_id |
string? | Correlates call with result |
is_error |
bool | Whether the tool result is an error |
clarification |
string? | Phase 59 — clarification question when intent is ambiguous |
plan_id |
string? | Phase 59 — unique plan identifier |
formatted_plan |
string? | Phase 59 — human-readable plan description |
requires_approval |
bool? | Phase 59 — whether user must approve before execution |
step_current |
int? | Phase 59 — current step number (1-based) |
step_total |
int? | Phase 59 — total steps in plan |
step_intent |
string? | Phase 59 — intent description for current step |
step_status |
string? | Phase 59 — step lifecycle status (started/completed/failed) |
Non-streaming response
{
"id": "chatcmpl-...",
"object": "chat.completion",
"choices": [{ "message": { "role": "assistant", "content": "Hi there" }, "finish_reason": "stop", "index": 0 }],
"usage": { "prompt_tokens": 12, "completion_tokens": 4, "total_tokens": 16 }
}Token counts are now captured correctly. OpenAI sends usage in a trailing SSE chunk after
finish_reason; the provider continues reading afterfinish_reasonand emits a finalMessageDeltawith the correctprompt_tokens/completion_tokens.
Returns the current live configuration (API key is never returned).
{
"model": "gpt-4o",
"base_url": "https://api.openai.com/v1",
"permission_mode": "DontAsk",
"pinned_provider": null
}Mutates configuration without restarting the server. All fields are optional.
{
"model": "gpt-4o-mini",
"api_key": "sk-new-key",
"base_url": "https://api.openai.com/v1",
"permission_mode": "DontAsk",
"provider": "openai"
}Returns 200 {"updated": true} on success, or 400 {"error": "..."} if the provider name is unknown.
Returns provider health, session pool state, and current settings.
{
"providers": [
{ "name": "openai", "healthy": true, "latency_ms": 142, "request_count": 50, "error_count": 1, "error_rate": "2.0%", "score": "0.91" }
],
"active_model": "gpt-4o",
"permission_mode": "DontAsk",
"pinned_provider": null,
"active_sessions": 3,
"max_sessions": 500,
"session_ttl_seconds": 3600
}OpenAI-compatible model list built from known providers.
{
"object": "list",
"data": [
{ "id": "openai", "object": "model", "created": 1700000000, "owned_by": "sovrant" }
]
}Lists saved session IDs. Non-admin callers see only sessions they own; admin callers see all sessions.
{ "sessions": [{ "id": "abc123" }, { "id": "def456" }] }Returns the user/assistant message history for a session. Non-admin callers attempting to read a session they do not own receive 404 Not Found.
{
"session_id": "abc123",
"messages": [
{
"role": "user",
"content": "Hello",
"timestamp": "2026-04-03T12:00:00+00:00",
"input_tokens": 5,
"output_tokens": 0
}
]
}Returns 404 if the session does not exist.
Permanently deletes the session and all its entries from the database. The deletion goes through ISessionStore.DeleteAsync which enforces ownership at the storage layer — non-admin callers attempting to delete a session they do not own receive 404 Not Found and the session is left untouched.
{ "deleted": "abc123" }Returns 404 if the session does not exist or is not visible to the caller.
Returns the effective configuration for an active session (model, permission mode).
{
"model": "gpt-4o",
"permission_mode": "dontask",
"is_overridden": false
}is_overridden is true if the session has a per-session model or permission mode override. Returns 404 if the session is not currently active in the pool.
Updates per-session config overlay. All fields are optional.
{
"model": "gpt-4o-mini",
"permission_mode": "Plan"
}Returns 200 {"updated": true} on success. Returns 404 if the session is not active.
Per-session config shadows the global defaults set via PUT /v1/config. Only the specified session is affected — other sessions continue using global defaults or their own overrides.
Returns per-session token usage. Non-admin callers see only their own sessions; admin callers see all sessions.
{
"sessions": [
{
"session_id": "user-123",
"input_tokens": 1200,
"output_tokens": 800,
"total_tokens": 2000
}
],
"total_input_tokens": 1200,
"total_output_tokens": 800,
"total_tokens": 2000
}Active sessions report live in-memory counters. Inactive sessions sum from persisted SQLite entries.
Allowed origins (no credentials required from the proxy — the proxy injects the bearer token):
http://localhost
http://localhost:3000
http://localhost:5173
http://localhost:8080
http://127.0.0.1
http://127.0.0.1:3000
http://127.0.0.1:5100
http://127.0.0.1:5173
http://127.0.0.1:8080
Requests from any other origin are blocked by the browser. The server itself does not enforce origin on server-to-server calls.
The server defaults to DontAsk — tools run without interactive prompts. This is required because there is no user to prompt over an HTTP stream.
Phase 59 — Graduated Tool Tiers:
DontAskmode no longer auto-approves all tools. Tools are classified into four tiers viaGraduatedToolTiers:
Tier Behavior in DontAsk Examples Safe Auto-approve Read, Glob, Grep, LS, WebFetch, WebSearch, Sleep, AskUserQuestion Moderate Auto-approve Write, Edit, NotebookEdit, TodoWrite, Skill, Artifact, McpProxy Dangerous Require confirmation Bash, PowerShell, REPL Escalation Always show plan Agent, TeamDelegate, Swarm, Mission To restore the old behavior (auto-approve everything), set
SOVRANT_UNSAFE_DONTASK=true.
Change the permission mode live via PUT /v1/config:
curl -X PUT http://127.0.0.1:5200/v1/config \
-H "Authorization: Bearer $SOVRANT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"permission_mode":"BypassPermissions"}'Valid values: Default, AcceptEdits, BypassPermissions, DontAsk, Plan
The
EnterPlanModeandExitPlanModetools change the permission mode for the current session only viaSessionConfig, not the global config. This prevents one user entering Plan mode from affecting other sessions. UsePUT /v1/sessions/{id}/configfor explicit per-session overrides, orPUT /v1/configfor server-wide defaults.
Every LLM provider interaction is optionally wrapped by the Sovrant Trust Boundary — a three-stage pipeline that runs as an ILlmProvider decorator (TrustBoundaryProvider):
- Intent Verification — bridges Phase 59's
IIntentGateto catch ambiguous or harmful intent before any data is sent - Ethical Harness — Sovrant-level content policy enforcement independent of model safety (works with uncensored models)
- Data Sanitizer — strips PII and corporate data from outbound prompts, restores placeholders on response
Configure via the Settings UI (/trust-boundary on Web, Governance → Trust Boundary on Desktop). Values persist to the workspace_settings table under trustboundary.* keys (V018 schema) and hot-reload on save through LiveSettingsRegistry.ReloadAll() — no restart needed (Phase 93 Bucket-B steps 3–5).
Resolution chain (env → DB → snapshot → defaults):
| Setting | DB key (workspace_settings) |
Env override |
|---|---|---|
| Master toggle | trustboundary.enabled |
— |
| Sanitizer enabled | trustboundary.sanitizer.enabled |
— |
| Sanitizer mode | trustboundary.sanitizer.mode |
— |
| Corporate domains | trustboundary.sanitizer.corporate_domains (JSON array) |
— |
| Custom patterns | trustboundary.sanitizer.custom_patterns (JSON List<CustomPattern>) |
— |
| Allow list | trustboundary.sanitizer.allow_list |
— |
| Exempt providers | trustboundary.sanitizer.exempt_providers |
— |
| Log redactions | trustboundary.sanitizer.log_redactions |
— |
| Intent enabled | trustboundary.intent.enabled |
— |
| Clarify ambiguous | trustboundary.intent.clarify_ambiguous |
— |
| Block harmful intent | trustboundary.intent.block_harmful |
— |
TrustBoundaryConfig.SaveToStoreAsync validates each value (sanitizer mode in the enum, custom regexes compile) before writing — invalid input throws ArgumentException instead of half-persisting. The legacy settings.json trust_boundary block still loads as a snapshot fallback so pre-Phase-93 installs keep working until the first save through the UI.
Blocked responses include StopReason = "trust_boundary_block" and a clear explanation of what was blocked and why. The ethical audit log records all blocks and flags for compliance reporting.
Sessions are stored in the SQLite database at ~/.sovrant/data/sovrant.db (override with SOVRANT_DB_PATH). Session entries include full-text search via FTS5.
Pass session_id in the chat request body to resume a conversation. If the ID does not exist a new session is created with that ID. Set SOVRANT_SESSION_JSONL=true to also write to legacy JSONL files.
For the full persistence architecture, see persistence.md.
The AskUserQuestion tool cannot pause an SSE stream to wait for a response. When the model calls it the server returns a fixed message:
[User input is not available in server mode. Please proceed without it.]
Returns all available eval suites found in .sovrant/evals/ and ~/.sovrant/evals/.
Returns trend data (pass rates, durations) for a named suite from ~/.sovrant/evals/results/.
{ "suite_name": "my-suite", "tag": "regression" }Runs the named suite (optionally filtered by tag) and returns the full report with per-eval results.
Starts a swarm orchestration. Returns an SSE stream of swarm events (plan creation, task start/complete/fail, quality gate, final result).
{
"prompt": "Refactor auth to use JWT",
"team": "backend-team",
"dry_run": false
}prompt(required) — the task to decompose and executeteam(optional) — orchestrated team whose members should be used as swarm workersdry_run(optional) — iftrue, only decompose and return the plan without executing
Returns the current status and task states for a swarm.
Replays the full event log for a swarm session from the swarm_events table (Phase 37.5 — previously read from JSONL files).
Returns recorded swarm session IDs, most-recently-active first. As of Phase 37.5 the response is sourced from the swarm_events table and supports server-side filtering:
| Query parameter | Meaning |
|---|---|
workspace_id |
Restrict to swarms whose events were stamped with this workspace |
project_id |
Restrict to swarms whose events were stamped with this project |
limit |
Maximum number of swarm IDs to return |
When POST /v1/swarm runs, the workspace/project scope is taken from WorkspaceContextMiddleware (HttpContext.Items["WorkspaceId"]) and the optional X-Project-Id request header — every event written during that swarm's run is stamped with that scope, so subsequent calls to GET /v1/swarm/sessions?workspace_id=… will only see swarms the caller was scoped to.
Legacy JSONL session files from before Phase 37.5 can be imported into the table with the CLI helper:
sovrant db import-swarm [--dir <path>] [--delete-source]
CRUD over the users table plus per-user data views. Auth model: these endpoints require an admin bearer token — an svt_* token belonging to a user with users.role = 'admin'. Non-admin callers receive 403 Forbidden. Self-service profile and token routes that don't require admin live under /v1/users/me/* (see Self-Service below).
Soft-delete only. DELETE /v1/users/{id} flips status='inactive'; the row is preserved so all FK references (workspaces, projects, sessions, audit) remain valid. Hard-delete is intentionally not exposed.
Mass-assignment protection. Request bodies only accept client-writable fields (username, email, role, team, status). user_id, created_at, and updated_at are server-controlled and cannot be set via JSON.
Validation.
usernamemust match^[a-zA-Z0-9._-]{1,64}$emailis optional, ≤ 254 chars, must contain@and.role∈ {user,admin}status∈ {active,inactive}teamis optional, ≤ 64 chars
Request:
{
"username": "alice",
"email": "alice@example.com",
"role": "user",
"team": "engineering"
}Response (201 Created):
{
"user_id": "usr_a1b2c3d4e5f60718",
"username": "alice",
"email": "alice@example.com",
"role": "user",
"team": "engineering",
"status": "active",
"created_at": "2026-04-06T...",
"updated_at": "2026-04-06T..."
}The server generates a usr_{16hex} user_id and auto-creates a personal workspace for the new user (mirrors the seeded default user behavior). Returns 400 for validation failures and 409 if the username or email is already taken.
Query params: status, role, team, limit (1–1000, default 100), offset (default 0).
Response:
{ "users": [ ... ], "count": 5, "total": 17 }Returns the user profile with derived stats: session_count, total_input_tokens, total_output_tokens, total_cost_usd, and last_seen_at (computed from MAX(updated_at) on the user's sessions; null if the user has no sessions).
Request body — every field optional; only the provided fields are changed:
{ "username": "alice2", "email": "new@example.com", "role": "admin", "team": "ops", "status": "active" }Returns 404 if the user does not exist, 400 for validation failures, 409 for unique-constraint violations.
Sets status='inactive'. Returns 409 if the target is the server boot identity (the row matching SOVRANT_USER_ID / OS username) — deactivating it would brick session seeding on the next restart.
Restores status='active' for a previously deactivated user.
Query params: limit, offset. Returns session IDs newest-first.
Query params: model, from, to. Returns aggregated token totals plus a per-model breakdown sourced from token_usage.
Query param: limit. Joins audit_governance through sessions.user_id. Sessions are stamped with the authenticated caller's user_id at creation (pulled from the svt_* token), so audit views reflect real per-user activity.
Routes scoped to the authenticated caller. Every endpoint resolves the target user from the svt_* token's identity — there is no {userId} path parameter to forge.
Returns the caller's own profile plus derived stats (same shape as GET /v1/users/{id}).
Issues a new svt_* bearer token for the caller. The plaintext secret is returned exactly once — capture it immediately.
Request:
{
"name": "laptop-dev",
"scopes": "chat,sessions",
"expires_at": "2026-12-31T23:59:59Z"
}All fields optional. scopes is a free-form comma-separated string persisted for future use (not yet enforced beyond admin/user roles). expires_at is optional — omit for a non-expiring token.
Response (201 Created):
{
"token": {
"token_id": "tok_...",
"user_id": "usr_...",
"name": "laptop-dev",
"scopes": "chat,sessions",
"created_at": "2026-04-09T...",
"last_used_at": null,
"expires_at": "2026-12-31T23:59:59+00:00",
"revoked_at": null
},
"plaintext": "svt_BASE64URL..."
}Returns the caller's tokens (metadata only — no plaintext, no hash).
Marks the token as revoked (revoked_at = now()). Callers cannot revoke tokens they do not own — the server verifies ownership by listing the caller's tokens before issuing the revoke, returning 404 otherwise.
Workspaces are the top-level organizational unit. Every user gets a personal workspace on creation. Team workspaces are created via the API.
Returns workspaces the authenticated user belongs to.
{ "name": "Engineering", "slug": "engineering" }Returns 201 with the created workspace. The caller is automatically added as owner.
{ "name": "New Name", "slug": "new-slug" }Personal workspaces cannot be deleted (returns 400).
{ "user_id": "usr_...", "role": "editor" }{ "email": "bob@example.com", "role": "editor" }Returns the invite with a one-time token.
{ "token": "invite-token-here" }{ "key": "value" }Returns aggregated token usage for the workspace.
Query param: layer (optional filter by memory layer).
{ "layer": "project", "content": "Remember this", "confidence": 0.9 }Projects belong to a workspace and provide scoping for sessions, artifacts, memory, and config.
Query param: includeArchived (optional, default false).
{ "name": "API", "slug": "api", "description": "Backend API" }{ "name": "New Name", "slug": "new-slug", "description": "Updated" }Admin only.
{ "user_id": "usr_...", "role": "editor" }Query param: resolved (optional, merge with workspace config).
Query param: layer (optional filter by memory layer).
Teams are named groups of agents that can be run together. Each team member has a role, system prompt, and optional agent template.
{ "name": "reviewers", "workspace_id": "ws_...", "project_id": "proj_..." }Query param: workspaceId (optional filter).
Returns the team record and its members.
Partial update for the per-team run profile. Any field omitted (or set to null) keeps its current value. Enum fields are case-insensitive strings; invalid values return 400. Field names use snake_case to match the rest of the route file.
{
"run_mode": "parallel",
"max_concurrent": 4,
"file_locks_enabled": true,
"quality_gate_enabled": true,
"quality_gate_threshold": 8,
"decomposition_mode": "roleAware"
}| Field | Type | Allowed values |
|---|---|---|
run_mode |
string | sequential, parallel, swarm |
max_concurrent |
int | ≥ 1 |
file_locks_enabled |
bool | — |
quality_gate_enabled |
bool | — |
quality_gate_threshold |
int | 0–10 |
decomposition_mode |
string | off, roleAware, open |
Returns { "team": { ...full updated team... } }. Returns 404 if the team does not exist. Used by the inline run-profile editor on the Orchestration page in both embedded and remote modes.
{ "name": "alice", "role": "reviewer", "template": "reviewer", "system_prompt": "..." }{
"goal": "Review auth module for security issues",
"decompose": true,
"lock_files": true,
"quality_gate": true,
"max_parallel": 4
}Returns the run result with run_id, status, output, and tokens_used.
Agent runs are recorded for teams, swarms, and missions.
Query params: workspaceId, userId, teamId, kind, status, limit.
POST /v1/webhook is the entry point for inbound events from third-party systems (Slack, Teams, Discord, custom). The endpoint normalizes payloads into a Sovrant turn and returns the agent's response. See docs/webhooks.md for the full integration guide, signature verification, and provider-specific payload shapes.
{
"source": "slack",
"user_id": "U123",
"thread_id": "T456",
"message": "list open PRs",
"callback_url": "https://hooks.slack.com/...",
"model": "claude-sonnet-4-6"
}Response: { "text": "...", "tool_calls": [...], "errors": [] }.
Missions are goal-driven, multi-step agent tasks tracked through their lifecycle.
{ "goal": "Migrate to v2 API", "workspace_id": "ws_...", "project_id": "proj_..." }Query params: ownerUserId, status, limit.
Drives the mission forward one engine cycle.
Returns the full event journal.
Query param: format (markdown default, or json).
Internal engine inspection and recovery.
Returns the full trace for a runtime run.
Returns runtime run IDs that crashed mid-step.
Attempts to recover crashed runs. Returns { "recovered": ["run_id_1", ...] }.
Deletes all trace rows for a runtime run. Returns { "runtime_run_id": "...", "deleted_rows": N }.
Artifacts are files produced by agent runs, scoped by workspace and project.
Query params: workspace_id, project_id, run_id.
Returns the artifact file content with appropriate content type.
Query params: workspace_id, project_id. Deletes all artifacts for a run.
Read-only registries for tools, skills, and agent templates.
Returns { "tools": [...], "count": N }.
Returns the tool definition including parameters schema.
Returns { "skills": [...], "count": N }.
Returns the skill definition including body (markdown content).
Returns { "templates": [...], "count": N }.
Returns the template including system prompt.
Aggregates everything currently in flight for the Command Center cockpit (Web /command, Desktop CommandCenterView). Read-only — no write controls in the MVP scope.
Query parameters
| Parameter | Required | Description |
|---|---|---|
owner_user_id |
No | Filter to a single owner. Defaults to the authenticated user. Admin tokens may pass any user. |
Response
{
"active_missions": 1,
"active_team_runs": 0,
"active_agent_runs": 2,
"active_sessions": 3,
"rows": [
{
"kind": "agent-run",
"title": "research-agent",
"status": "running",
"preview": "gathering sources...",
"last_activity": "2026-05-02T18:24:11Z",
"cost_usd": 0.0124,
"detail_route": "/activity?run=run-abc"
}
],
"generated_at": "2026-05-02T18:24:13Z"
}The cockpit polls this endpoint every 30 seconds. rows are sorted by last_activity descending, paginated. Each row points at an existing detail surface via detail_route so the Command Center never duplicates UIs. Private records appear with "title": null and "preview": null (masked) — their existence is acknowledged for accountability but content is hidden from non-owners.
Aggregates the signed-in user's cross-workspace activity. Differs from Command Center: shows completed and historical records (not just in-flight), scoped to what the requesting user owns or can see. Read-only.
Visibility rules (enforced server-side):
| Record type | Visible to requesting user |
|---|---|
| Own records (any privacy) | Always visible |
| Teammates' public records (same workspace) | Visible |
| Teammates' private records | Never returned — excluded entirely |
Response
{
"active_missions": 1,
"active_team_runs": 0,
"active_agent_runs": 2,
"active_sessions": 3,
"rows": [
{
"kind": "session",
"title": "my session",
"status": "idle",
"preview": "last message preview...",
"last_activity": "2026-05-26T10:00:00Z",
"is_private": false,
"owner_username": "alice",
"detail_route": "/activity?session=sess-abc"
}
],
"generated_at": "2026-05-26T10:00:01Z"
}rows include is_private so the frontend can render a privacy indicator. The dashboard polls every 30 seconds and preserves the current page across refreshes. Reached via the 👤 rail nav icon on both Web (/dashboard) and Desktop.
Returns cost tracking data from the OpenRouter pricing model. Query param: range (daily, weekly, monthly, all; default daily).
{
"range": "daily",
"total_cost_usd": 1.42,
"sessions": [ ... ],
"by_model": [ ... ]
}If cost tracking is disabled (SOVRANT_COST_PROVIDER=none), returns { "enabled": false, "message": "..." }.
Sovrant.Server exposes a SignalR hub at /hubs/chat for real-time streaming to the web frontend. This is the transport layer for SOVRANT_RUNTIME_MODE=remote.
Standard WebSocket upgrades cannot send Authorization headers. The BearerTokenMiddleware accepts ?access_token=<token> on /hubs/* paths as an alternative to the header.
| Method | Direction | Description |
|---|---|---|
StreamTurn(sessionId, userMessage) |
Client → Server | Streams an agentic turn as IAsyncEnumerable<RuntimeEventDto> |
ConfirmTool(toolUseId) |
Client → Server | Approves a pending tool confirmation |
DenyTool(toolUseId) |
Client → Server | Denies a pending tool confirmation |
CancelTurn |
Client → Server | Cancels the current turn |
All 13 RuntimeEvent subtypes are mapped to a flat JSON-friendly RuntimeEventDto with a string Type discriminator. The DTO lives in Sovrant.Runtime.Conversation so both Sovrant.Server (serialization) and Sovrant.Web (deserialization) can reference it without a cross-project dependency.
When a tool requires user confirmation, the hub emits a ToolConfirmationRequested event. The client calls ConfirmTool(toolUseId) or DenyTool(toolUseId) to resolve the pending TaskCompletionSource<bool>. Pending confirmations are keyed by {connectionId}:{toolUseId} and cleaned up on disconnect.
Sovrant.Web supports two runtime modes controlled by SOVRANT_RUNTIME_MODE:
embedded(default) — The agentic loop runs in-process viaAddSovrantRuntime(). The web app is self-contained with its own SQLite database.remote— The web app connects to an externalSovrant.Servervia SignalR + REST usingAddSovrantClient(). All Blazor components depend on interfaces (IRuntimeSessionPool,ISessionStore,IToolRegistry,IArtifactStore,IToolConfirmationHandler), so swapping modes is purely a DI registration concern.