Skip to content

Legio v3 — Fix Everything, Improve Everything

Context

Comprehensive audit against dev-memo/plan.md revealed 3 high-severity issues (path traversal, dead token tracking, imprecise SQL), 4 medium issues, and numerous refactoring/improvement opportunities. All 230 tests pass at 100% coverage. This plan fixes all gaps, bugs, and security holes while maintaining that 100% green bar.


Work Items (ordered by priority)

1. Security: Path traversal protection in MemoriaStore + Legatus

Files: legio/memoria/store.py, legio/legatus.py, legio/errors.py, tests/test_memoria_store.py, tests/test_legatus.py

  • Add _validate_name(name: str) -> None helper in store.py that raises MemoriaError if name doesn't match ^[a-z0-9][a-z0-9_-]*$ or contains path separators.
  • Call it at the top of every public method that takes name or centurio_name: read_edictum, publish_edictum, revoke_edictum, read_actum, publish_actum, read_commentarium, write_commentarium, list_commentarii.
  • Add same validation in legatus._create_centurio() for centurio name.
  • Add _RESERVED_NAMES = frozenset({"caesar", "legatus", "all", "praetorium"}) — block these as centurio names.
  • Add # SECURITY: prevent path traversal via crafted name comments.
  • Add @pytest.mark.security tests: null bytes, ../, absolute paths, reserved names.

2. Bug fix: Audience LIKE query false positives in Praetorium

Files: legio/praetorium.py, tests/test_praetorium.py

  • Replace the LIKE '%"viewer"%' query with application-level filtering. Query all nuntii ordered by timestamp DESC with a larger over-fetch limit, then filter in Python using Nuntius.is_visible_to(), and return only the first limit matches.
  • Alternative considered: json_each() — but aiosqlite's SQLite version may not support it reliably on all platforms. Application-level is safer and uses the existing is_visible_to() method.
  • Add @pytest.mark.security test: centurio "a" must NOT see messages for "alpha".

3. Bug fix: Token tracker never updated (D19 dead code)

Files: legio/legatus.py, tests/test_legatus.py

  • Modify _collect_response() to accept an optional SessionTokenTracker and call tracker.update(msg) on each ResultMessage.
  • In dispatch_to_centurio(), pass session.tracker to _collect_response().
  • Add test verifying that after dispatch, session tracker has non-zero cumulative_input.

4. Feature: Start idle session reaper task (D29)

Files: legio/legatus.py, legio/__main__.py, tests/test_legatus.py

  • Add start_idle_reaper() method to Legatus that creates the periodic asyncio.Task stored in self._idle_task. Loop: await asyncio.sleep(60), then await self.cleanup_idle_sessions().
  • Call legatus.start_idle_reaper() in __main__.py after legatus creation, before bot start.
  • shutdown() already cancels _idle_task — no change needed.
  • Add test that reaper task runs and cleans up.

5. Security: XML escaping in format_history_xml and memoria XML wrappers

Files: legio/legatus.py, legio/memoria/store.py, tests/test_legatus.py, tests/test_memoria_store.py

  • Use xml.sax.saxutils.escape() for text content and xml.sax.saxutils.quoteattr() for attribute values.
  • In format_history_xml(): escape n.text, n.sender, n.id.
  • In _wrap_xml() and _wrap_commentarium_xml(): escape name, author, content.
  • Add # SECURITY: prevent XML injection in context blocks comments.
  • Add @pytest.mark.security tests with payloads like </nuntius><injected>.

6. Refactor: Deduplicate MCP tool builders in memoria/tools.py

Files: legio/memoria/tools.py, tests/test_memoria_tools.py

  • Extract shared read-only tools (list_edicta, read_edictum, list_acta, read_actum) into a _build_shared_readonly_tools(store) helper function.
  • build_memoria_full_server = shared + write edicta + revoke + write acta + full commentarii.
  • build_memoria_centurio_server = shared + scoped write acta + scoped commentarii.
  • ~40 lines eliminated. Tests unchanged (same public API).

7. Improvement: Add centurio name validation on Centurio dataclass

Files: legio/centurio.py, tests/test_centurio.py

  • Extend __post_init__ to reject names matching reserved names and names with invalid characters (path separators, dots, etc). Pattern: ^[a-z][a-z0-9_-]*$.
  • This provides defense-in-depth alongside MemoriaStore validation.

8. Improvement: Add # SECURITY: comment on Telegram error boundary

Files: legio/telegram/bot.py

  • Add # SECURITY: generic error prevents leaking internal details to chat at bot.py:165.

9. Fix: .claude/rules/ path scoping

Files: All .claude/rules/**/*.md files

  • Change paths: ["src/**/*.py"] to paths: ["legio/**/*.py"] and paths: ["legio/**/*.py", "tests/**/*.py"] as appropriate. The project has no src/ directory.

10. Improvement: Missing templates/centurio/ directory (D20)

Files: templates/centurio/prompt.md.template, templates/centurio/tools.json.template

  • Create both template files on disk matching plan spec.
  • The _render_centurio_prompt() inline template stays (it's the runtime implementation) but now the templates serve as the canonical editable scaffolds per "prompt over code."

11. Improvement: Startup warning for unconfigured telegram_id

Files: legio/__main__.py

  • Add if config.caesar.telegram_id == 0: logger.warning("telegram_id is 0 — bot will reject all users") after config load.

12. Compact: Remove unused MemoriaServer.config field

Files: legio/memoria/tools.py, tests/test_memoria_tools.py

  • Remove the config field from MemoriaServer dataclass. Remove create_sdk_mcp_server() calls inside both builder functions — they're only used to populate this field. The actual MCP server config is built by the Legatus when it composes tools.
  • Update the test that checks server.config.

13. Improvement: WAL mode for SQLite

Files: legio/praetorium.py

  • Add await self._db.execute("PRAGMA journal_mode=WAL") after connection in open().

14. Minor: /create should show working indicator (D21 consistency)

Files: legio/telegram/bot.py

  • Add ⏳ + typing indicator pattern to _handle_create, matching _handle_message.

Verification

After all changes:

bash
# Must all pass
cd /Users/joker/github/xiaolai/myprojects/legio
.venv/bin/pytest                          # 100% coverage, all green
.venv/bin/ruff check legio/ tests/        # zero violations
.venv/bin/ruff format --check legio/ tests/  # formatting clean

Security tests specifically:

bash
.venv/bin/pytest -m security -v           # all @pytest.mark.security pass

Built with Roman discipline.