# SAF: Server Implementation

> HTTP server layer — axum on port 8484.
> **Last updated:** 2026-05-12 (Full audit — memory CRUD, auth, instance registry)

---

## Implementation Status

| Area | Module | Status | Notes |
|------|--------|--------|-------|
| Server struct + lifecycle | `server/mod.rs` (228 lines) | ✅ | SouveraineServer, new(), run(), instance registry, compaction wiring |
| Database schema | `server/db.rs` (77 lines) | ✅ | agents + agent_instances tables, SQLite |
| API models | `api/models.rs` (190 lines) | ✅ | All request/response types |
| Session manager | `server/session_manager.rs` (111 lines) | ✅ | Sessions with SSE broadcast, conversation store wiring |
| Agent inventory | `server/agent_inventory.rs` (514 lines) | ✅ | CRUD + SQLite + per-agent seed init + subconscious creation + instance heartbeat |
| HTTP handlers | `api/handlers.rs` (261 lines) | ✅ | Agent, conversation, memory handlers |
| API routes | `api/mod.rs` (65 lines) | ✅ | Public + memory + web routes |
| Auth middleware | `api/auth.rs` (42 lines) | ✅ | Bearer-token protection for memory routes |
| Consciousness engine | `server/consciousness_engine.rs` (677 lines) | ✅ | N+1 Aster (full LLM tool loop), N+25 reflection, 3-tier compaction warnings |
| CLI integration | `main.rs` + `cli/commands.rs` | ✅ | `souveraine server` command |
| Gitea memory | `server/gitea_memory.rs` (115 lines) | ⚠️ | Opt-in, gracefully disabled if unreachable |
| Server conversation | `server/conversation.rs` (72 lines) | ⚠️ | Simplified Bifrost call, no tool loop (tool loop lives in LocalBackend) |

---

## What the Server Provides

### Endpoints

| Method | Path | Handler | Status |
|--------|------|---------|--------|
| GET | `/health` | health_check | ✅ |
| GET | `/v1/agents` | list_agents | ✅ |
| POST | `/v1/agents` | create_agent | ✅ (auto-creates subconscious + seed) |
| GET | `/v1/agents/:id` | get_agent | ✅ |
| PATCH | `/v1/agents/:id` | update_agent | ✅ |
| DELETE | `/v1/agents/:id` | delete_agent | ✅ |
| GET | `/v1/conversations` | list_conversations | ✅ |
| POST | `/v1/conversations` | create_conversation | ✅ |
| GET | `/v1/conversations/:id` | get_conversation | ✅ |
| POST | `/v1/conversations/:id/messages` | stream_messages (SSE) | ✅ |
| GET | `/v1/agents/:id/memory` | list_memory | ✅ (auth required) |
| GET | `/v1/agents/:id/memory/*path` | read_memory | ✅ (auth required) |
| PUT | `/v1/agents/:id/memory/*path` | write_memory | ✅ (auth required) |
| PATCH | `/v1/agents/:id/memory/*path` | append_memory | ✅ (auth required) |
| DELETE | `/v1/agents/:id/memory/*path` | delete_memory | ✅ (auth required) |
| GET | `/` | ServeDir(`web/dist/`) | ✅ (SPA fallback, no UI built) |

### Endpoints Not Implemented

No Letta-compatible block endpoints (`/v1/agents/:id/core-memory/blocks`) are planned — Souveraine committed to memfs-only memory per `docs/MEMORY_BLOCKS_DECISION.md`. The memory CRUD endpoints above replace Letta's block API.

### Instance Registry

The server maintains an `agent_instances` table (`server/db.rs`, `server/agent_inventory.rs`):
- `register_instance()` — creates one row per known agent per process, prunes stale rows (>5 min)
- `heartbeat_instance()` — bumps `last_seen_at` and increments `lifetime_active_seconds` in 30s ticks
- `instance_count()` — how many running instances for a given agent
- `lifetime_active_seconds()` — total lifecycle uptime for uptime percentage

A background tokio task in `SouveraineServer::new()` handles the 30s heartbeat loop.

### Auth

Memory routes require a bearer token (`Authorization: Bearer <token>`), enforced by middleware at `api/auth.rs`. Loopback requests (127.0.0.1 / ::1) can bypass auth when `auth.allow_loopback` is true (configurable in `souveraine.toml` `[server.auth]` section). Public routes (agents list, conversations, health) are unauthenticated.

---

## How to Run

```bash
souveraine server
# Binds to 127.0.0.1:8484 (configurable: [server] bind=, port=, or SOUVERAINE_SERVER_BIND env)
# Creates ~/.souveraine/server/
#   ├── agents/           # Agent directories with UUID naming
#   │   └── {uuid}/
#   │       ├── agent.json
#   │       └── conversations/
#   └── database.sqlite3  # Agent index + instance registry

# With custom bind
souveraine server --bind 0.0.0.0 --port 8484
```

The user-side memfs lives at `~/.souveraine/agents/{uuid}/memory/` — this is the single canonical path. Subconscious agents at `~/.souveraine/subconscious-agents/{id}-sub/`.

---

## Architecture Note

The server and LocalBackend share the same engine (`SouveraineServer`). The difference is the transport layer:
- **Server mode**: axum HTTP + SSE — client/server separation
- **Local mode**: in-process `Arc<SouveraineServer>` with direct `BackendEvent` streaming — no socket

Both paths run the same `run_turn()` tool loop (in `LocalBackend`), the same N+1 Aster pass, and the same N+25 reflection trigger. The server's `ServerConversation` (72 lines) is a simplified single-turn path used only by the SSE handler; the full tool loop with all 11 tools lives in `LocalBackend::run_turn()`.

---

## Debugging

There is currently no `souveraine server` debug output — `run()` uses `println!` not tracing. Memory CRUD endpoints are logged via auth middleware.
