Skip to content

Core Concepts

Legio uses Roman military terminology as its domain vocabulary. Each Latin term maps to a precise technical concept that avoids collision with generic programming terms like "agent", "message", or "session".

Agents

Caesar

The human operator. Caesar communicates exclusively through Telegram and has god-view access to the entire system — all messages, all memoria, all centuriones. Caesar's identity is verified by Telegram user ID configured in legio.toml.

Legatus

The orchestrator agent. Legatus wears two hats:

  1. Orchestrator (code) — parses @mentions via regex (?<![.\w/@])@(\w+), routes messages, manages centurio lifecycle
  2. Default responder (SDK agent) — when no centurio is mentioned, Legatus responds using its own ClaudeSDKClient session

The Legatus has MCP tools for creating/removing centuriones, dispatching messages, and accessing the full memoria system.

Legatus MCP Tools

ToolParametersDescription
create_centurioname: str, specialization: strCreate a centurio from blueprint template
remove_centurioname: strRemove a centurio and disconnect its session
list_centurionesList all registered centuriones with status and description
dispatch_to_centurioname: str, message: strSend a message to a specific centurio
post_nuntiustext: str, audience: strPost a nuntius to the praetorium (comma-separated audience)
get_historylimit: intRetrieve recent praetorium history as XML
+ All memoria toolsFull edicta, acta, and commentarii access

Legatus Rebuild Triggers

The Legatus SDK session is rebuilt when:

  • Roster hash changes — a centurio was added, removed, or had its description edited (SHA-256 of name:description pairs)
  • Prompt mtime changescastra/legatus/prompt.md was modified on disk

On rebuild, the _client_is_fresh flag is set, triggering praetorium history bootstrap on the next query.

Centurio (pl. Centuriones)

A specialist agent. Each centurio is defined by:

  • prompt.md — system prompt describing its role and capabilities
  • tools.json — MCP tool configuration
  • commentarii/ — private notes directory (append-only)

Centuriones are created from blueprints (templates) and live in castra/centuriones/<name>/. They are identified by lowercase names validated against a strict pattern (^[a-z][a-z0-9_-]*$).

Reserved names (caesar, legatus, all, praetorium) are blocked to prevent identity spoofing.

Centurio Data Model

python
@dataclass
class Centurio:
    name: str           # Validated in __post_init__
    centuria: Path      # Path to castra/centuriones/<name>/
    status: str = "idle"  # "idle" | "working" | "error"

    @property
    def prompt_path(self) -> Path       # centuria / "prompt.md"
    @property
    def tools_path(self) -> Path        # centuria / "tools.json"
    @property
    def commentarii_dir(self) -> Path   # centuria / "commentarii"

Centurio MCP Tools

Each centurio gets a scoped MCP server (memoria_<name>) with restricted access:

ToolParametersNotes
list_edictaRead-only (no publish/revoke)
read_edictumname: strRead-only
list_actaFull access
read_actumname: strFull access
publish_actumname: str, content: strAuthor auto-set to centurio name
list_commentariiAuto-scoped to owning centurio
read_commentariumname: strAuto-scoped to owning centurio
write_commentariumname: str, content: strAuto-scoped to owning centurio

Messages

Nuntius (pl. Nuntii)

The immutable message type. Every communication in the system — from Caesar's Telegram messages to centurio responses — is a Nuntius.

python
@dataclass(frozen=True)
class Nuntius:
    sender: str                   # "caesar", "legatus", or centurio name
    text: str                     # message body
    audience: tuple[str, ...] = ("all",)  # explicit recipients
    id: str                       # UUID4 (auto-generated)
    timestamp: datetime           # UTC timezone-aware (auto-generated)
    reply_to: str | None = None   # threading via UUID reference

Key properties:

  • Frozen — once created, never modified. Prevents mutation bugs in concurrent dispatch.
  • Audience-based visibility — centuriones only see nuntii addressed to them or to "all"
  • Caesar and Legatus see everything (god-view)
  • Threadingreply_to links responses to the originating message via UUID

Visibility Rules

Praetorium

The SQLite-backed message bus that stores all nuntii. Think of it as the shared communication log that gives agents conversational context.

sql
CREATE TABLE nuntii (
    id TEXT PRIMARY KEY,          -- UUID4
    sender TEXT NOT NULL,         -- "caesar", "legatus", or centurio name
    text TEXT NOT NULL,           -- message body
    audience TEXT NOT NULL,       -- JSON array: ["all"] or ["vorenus", "brutus"]
    timestamp TEXT NOT NULL,      -- ISO 8601 UTC
    reply_to TEXT,                -- UUID of parent nuntius
    FOREIGN KEY (reply_to) REFERENCES nuntii(id)
);

CREATE INDEX idx_nuntii_timestamp ON nuntii(timestamp);
CREATE INDEX idx_nuntii_sender ON nuntii(sender);

Praetorium API

MethodReturnsDescription
open()Open database, create schema, set WAL + foreign keys
close()Close the database connection
post(nuntius)Insert a nuntius into the database
get_visible_nuntii(viewer, limit=50)list[Nuntius]Return nuntii visible to viewer, newest first
get_history(limit=50)list[Nuntius]Return last N nuntii regardless of audience (admin view)

Features:

  • WAL mode for concurrent read/write performance
  • Visibility rules enforced at application level (no SQL LIKE — exact match filtering)
  • History injection — agents receive relevant past messages as XML context
  • Deduplication — subsequent dispatches only inject new messages since the last injection
  • Over-fetch heuristic — fetches limit * 5 rows when filtering for centuriones to compensate for interleaved audiences

Memory (Memoria)

Three layers of persistent storage, all filesystem-based with XML format:

Edicta (Standing Orders)

Global directives that all agents should read before starting any task. Mutable — can be published, updated, and revoked.

xml
<edictum name="policy" author="caesar" timestamp="2026-02-15T12:00:00+00:00">
  Always respond in formal English.
</edictum>

Example: "Always respond in formal English" or "Prefer TypeScript for code examples".

Acta (Shared Knowledge)

Published outputs available to all agents. Mutable — any agent can publish or update an actum.

xml
<actum name="findings" author="vorenus" timestamp="2026-02-15T12:00:00+00:00">
  Research findings on quantum computing approaches.
</actum>

Example: a research centurio publishes findings that a writing centurio later uses.

Commentarii (Private Notes)

Per-centurio append-only journal. Only the owning centurio can read and write its own commentarii. The Legatus can read any centurio's commentarii (admin privilege).

xml
<commentarium name="notes" timestamp="2026-02-15T12:00:00+00:00">
  Intermediate results from the analysis task.
</commentarium>

Example: a centurio keeps scratch notes, intermediate results, or task state across sessions.

MCP Tool Access Matrix

ToolCenturioLegatus
list_edicta✅ read✅ read
read_edictum✅ read✅ read
publish_edictum✅ write
revoke_edictum✅ write
list_acta✅ read✅ read
read_actum✅ read✅ read
publish_actum✅ write✅ write
list_commentarii🔒 own only✅ all
read_commentarium🔒 own only✅ all
write_commentarium🔒 own only✅ all

Workspace

Castra

The runtime workspace directory tree:

castra/
  legatus/
    prompt.md               # Legatus system prompt
  centuriones/
    vorenus/
      prompt.md             # Centurio-specific prompt
      tools.json            # MCP tool config
      commentarii/           # Private notes (gitignored)
  edicta/                    # Standing orders (tracked)
  acta/                      # Shared knowledge (tracked)
  praetorium.db              # Message history (gitignored)

Blueprints

Template directory (blueprints/) used when creating new centuriones. Contains prompt.md.template and tools.json.template with and placeholders. Committed to git, shared across deployments.

On first run, ensure_castra() copies blueprints/legatus/prompt.md.template to castra/legatus/prompt.md.

Error Hierarchy

All domain errors inherit from a single base class, caught at Telegram boundaries:

Rules:

  • Domain code raises domain exceptions, never stdlib (KeyError, FileNotFoundError)
  • Catch at boundaries (Telegram handler), propagate in domain code
  • Never swallow exceptions — log at DEBUG and re-raise
  • Generic error messages at Telegram boundary (❌ An error occurred) prevent information disclosure

Authorization (Auctoritas)

Pending authorization requests for destructive actions like centurio removal or edictum revocation.

python
@dataclass
class Auctoritas:
    id: str             # UUID4
    action: str         # "remove_centurio" or "revoke_edictum"
    payload: dict       # {"name": "vorenus"}
    chat_id: int        # Telegram chat where request was made
    user_id: int        # Telegram user who initiated the request
    message_id: int     # Telegram message ID of OTP prompt (for cleanup)
    expires_at: datetime  # TTL = 120 seconds (hardcoded)
    attempts: int       # Max 3 attempts (hardcoded)

The AuctoritasStore is keyed by (chat_id, user_id) to prevent group-chat bypasses. Expired or exhausted requests are automatically cleaned up on the next get_pending() call.

Security Model

Authentication

  • Caesar is verified by Telegram user ID (configured in legio.toml)
  • Only the verified Caesar can interact with the bot
  • All non-Caesar messages are silently dropped at framework level
  • Destructive actions (removing a centurio, revoking an edictum) require TOTP authorization

Name Validation

Every centurio and memoria entry name is validated:

TargetRegexReserved Names
Centurio^[a-z][a-z0-9_-]*$caesar, legatus, all, praetorium
Memoria^[a-z0-9][a-z0-9_-]*$

This prevents path traversal (../../etc/passwd), identity spoofing, and filesystem safety issues.

XML Escaping

All text injected into XML context blocks is escaped via xml.sax.saxutils.escape() and quoteattr(). This prevents prompt injection through crafted message text.

Every filesystem operation checks path.is_symlink() and rejects symlinks to prevent symlink-follow attacks.

Memoria Safety

  • Path traversal protection at every filesystem boundary
  • Commentarii are append-only — no overwrite, no delete
  • XML content always wrapped with escaped attributes

Built with Roman discipline.