Skip to content

Filesystem Architecture

Legio separates immutable blueprints from mutable runtime state. All file operations go through validated paths with name regex + symlink rejection.

Directory Layout

legio/                          # Python package (code)
  centurio.py                   # Agent identity and validation
  nuntius.py                    # Immutable message type
  legatus.py                    # Orchestrator agent
  session.py                    # SDK session lifecycle
  praetorium.py                 # SQLite message bus
  config.py                     # Configuration loading
  errors.py                     # Exception hierarchy (7 classes)
  rendering.py                  # XML/template rendering
  auctoritas.py                 # TOTP authorization store
  totp.py                       # TOTP verification
  telegram/                     # Telegram infrastructure
    bot.py                      # Bot handlers and filters
    commands.py                 # Slash command handlers
    utils.py                    # Shared helpers
    markdown_render.py          # Markdown → Telegram HTML
  memoria/                      # Memory layer
    store.py                    # Filesystem CRUD operations
    tools.py                    # MCP tool wrappers

blueprints/                     # Default templates (committed)
  centurio/
    prompt.md.template          # {{name}} and {{specialization}}
    tools.json.template
  legatus/
    prompt.md.template

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

Tracking Policy

The castra subtree is partially tracked in git:

ContentTracked?Reason
legatus/prompt.mdYesLegatus prompt is the core personality — version-controlled
Centurio prompt.mdYesPrompts are code — version-controlled
Centurio tools.jsonYesTool config defines capabilities
Edicta (.xml files)YesStanding orders define system behavior
Acta (.xml files)YesShared knowledge is team state
CommentariiNoEphemeral per-session scratch
praetorium.dbNoBinary runtime data
praetorium.db-walNoWAL journal (runtime)
praetorium.db-shmNoShared memory (runtime)

Init Behavior

ensure_castra() runs at startup via __main__.py:

Key behaviors:

  1. Creates all subdirectories with exist_ok=True (idempotent)
  2. Copies legatus prompt from blueprints/ only if missing (copy-if-missing)
  3. Centurio blueprints are consumed at creation time, not at init
  4. The praetorium database is created later when Praetorium.open() runs

Centurio Creation

When the Legatus creates a centurio via the create_centurio MCP tool:

Template Rendering

Templates use and placeholders:

SourceDestinationPlaceholders
blueprints/centurio/prompt.md.templatecastra/centuriones/<name>/prompt.md,
blueprints/centurio/tools.json.templatecastra/centuriones/<name>/tools.jsonNone

If the blueprint file is missing on disk, an inline fallback template is used (defined in rendering.py).

Centurio Removal

When the Legatus removes a centurio:

  1. Verify centurio exists in registry
  2. Disconnect SDK session via SessionManager
  3. Remove from in-memory registry
  4. Delete directory tree via shutil.rmtree(centuria)

Memoria Layers

The Memoria system has three layers, all stored as XML files:

Layer 1: Edicta (Standing Orders)

PropertyValue
Pathcastra/edicta/<name>.xml
SemanticsMutable (create/overwrite/delete)
Name regex^[a-z0-9][a-z0-9_-]*$
ScopeGlobal — all agents read these before tasks
TOTP gatedRevoke only (/revoke command)

XML schema:

xml
<edictum name="code-style" author="caesar" timestamp="2025-01-15T10:30:00+00:00">
All Python code must use type annotations and pass ruff checks.
</edictum>

Layer 2: Acta (Shared Knowledge)

PropertyValue
Pathcastra/acta/<name>.xml
SemanticsMutable (create/overwrite)
Name regex^[a-z0-9][a-z0-9_-]*$
ScopeGlobal — any agent can read or publish
AuthorAuto-set to centurio name (scoped server) or explicit (full server)

XML schema:

xml
<actum name="quantum-research" author="vorenus" timestamp="2025-01-15T11:00:00+00:00">
Latest advances in quantum computing include superconducting qubits...
</actum>

Layer 3: Commentarii (Private Notes)

PropertyValue
Pathcastra/centuriones/<owner>/commentarii/<name>.xml
SemanticsAppend-only (create new files only, no overwrite)
Name regex^[a-z0-9][a-z0-9_-]*$
ScopePer-centurio — only the owning centurio can read/write
EnforcementClosure-scoped MCP server captures centurio_name at creation

XML schema (no author attribute — scoped to owner):

xml
<commentarium name="research-notes" timestamp="2025-01-15T11:15:00+00:00">
Found three key papers on quantum error correction...
</commentarium>

XML Escaping

All XML values are escaped to prevent injection:

FieldEscape FunctionLibrary
Tag namesHardcoded strings
name attributequoteattr()xml.sax.saxutils
author attributequoteattr()xml.sax.saxutils
timestamp attributeISO format string (no user input)
Content bodyescape()xml.sax.saxutils

MCP Tool Access

Two different MCP server configurations control tool access:

Full Server (Legatus) — 17 tools

The full server requires explicit centurio_name parameter for commentarii operations. The Legatus can access any centurio's private notes.

Scoped Server (Centurio) — 8 tools

Scoping is enforced via Python closures:

python
def build_memoria_centurio_server(store, centurio_name):
    # centurio_name captured in closure — impossible to access another's data
    tool("list_commentarii", ...,
         lambda _a: store.list_commentarii(centurio_name))
    tool("write_commentarium", ...,
         lambda a: store.write_commentarium(centurio_name, a["name"], a["content"]))

Access Matrix

ToolLegatusCenturio
list_edicta✅ (read-only)
read_edictum✅ (read-only)
publish_edictum
revoke_edictum
list_acta
read_actum
publish_actum✅ (explicit author)✅ (author auto-set)
list_commentarii✅ (any centurio)✅ (own only)
read_commentarium✅ (any centurio)✅ (own only)
write_commentarium✅ (any centurio)✅ (own only)

Security Properties

Every file operation has defense-in-depth protections:

ThreatDefenseModule
Path traversalName regex ^[a-z0-9][a-z0-9_-]*$ rejects /, .., spacesmemoria/store.py
Symlink attackspath.is_symlink() rejection before every read/write/deletememoria/store.py
Centurio name collisionReserved names {caesar, legatus, all, praetorium} rejectedcenturio.py
XML injectionescape() + quoteattr() on all user-supplied contentmemoria/store.py
Overwrite of private notesAppend-only semantics — write_commentarium raises if file existsmemoria/store.py
Cross-centurio accessClosure scoping in MCP server captures centurio_name at build timememoria/tools.py

File Naming Conventions

CategoryPatternExtensionExample
Centurio name^[a-z][a-z0-9_-]*$— (directory)vorenus/
Memoria entry^[a-z0-9][a-z0-9_-]*$.xmlcode-style.xml
Blueprint templatedescriptive.templateprompt.md.template
Prompt fileprompt.mdprompt.md
Tool configtools.jsontools.json
Databasepraetorium.dbpraetorium.db

Built with Roman discipline.