- Add EPIC.md with resurrection plan - Create Hermes profile with Bezalel persona - Add llama-server.sh for Gemma 4 inference - Update start_bezalel.sh with stack checks - Add README with quick start guide Backend: llama.cpp Model: Gemma 4 26B MoE (Apache 2.0) Frontend: Hermes profile No OpenAI. No cloud. Pure sovereign stack.
10 KiB
[KT-001] Enable Full Audit Mode on Evennia Server
Status: Open
Priority: High
Assignee: @ezra
Labels: infrastructure, audit, evennia
Objective
Enable comprehensive audit logging on the Timmy Academy Evennia MUD server to track all player activity.
Background
The Evennia server currently only tracks basic login/logout events. We need full visibility into character movements, commands executed, and player sessions for accountability, debugging, and game history review.
Implementation Tasks
Task 1: Update Settings Configuration
File: server/conf/settings.py
Add the following configuration:
# FULL AUDIT MODE - Track everything
COMMAND_LOG_ENABLED = True
COMMAND_LOG_LEVEL = "DEBUG"
COMMAND_LOG_FILENAME = "command_audit.log"
AUDIT_LOG_ENABLED = True
# Custom logging configuration for full audit trail
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"timestamped": {
"format": "%(asctime)s [%(levelname)s]: %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
"detailed": {
"format": "%(asctime)s | %(name)s | %(levelname)s | %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "timestamped",
"level": "INFO",
},
"file_server": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "server/logs/server.log",
"when": "midnight",
"backupCount": 30,
"formatter": "detailed",
"level": "INFO",
},
"file_portal": {
"class": "logging.handlers.TimedRotatingFileHandler",
"filename": "server/logs/portal.log",
"when": "midnight",
"backupCount": 30,
"formatter": "detailed",
"level": "INFO",
},
"file_commands": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "server/logs/command_audit.log",
"maxBytes": 10485760, # 10MB
"backupCount": 50,
"formatter": "detailed",
"level": "DEBUG",
},
"file_movement": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "server/logs/movement_audit.log",
"maxBytes": 10485760,
"backupCount": 50,
"formatter": "detailed",
"level": "DEBUG",
},
"file_player": {
"class": "logging.handlers.RotatingFileHandler",
"filename": "server/logs/player_activity.log",
"maxBytes": 10485760,
"backupCount": 50,
"formatter": "detailed",
"level": "DEBUG",
},
},
"loggers": {
"evennia": {
"handlers": ["console", "file_server"],
"level": "INFO",
"propagate": False,
},
"portal": {
"handlers": ["console", "file_portal"],
"level": "INFO",
"propagate": False,
},
"evennia.commands": {
"handlers": ["file_commands"],
"level": "DEBUG",
"propagate": False,
},
"evennia.objects": {
"handlers": ["file_movement"],
"level": "DEBUG",
"propagate": False,
},
"evennia.accounts": {
"handlers": ["file_player"],
"level": "DEBUG",
"propagate": False,
},
},
"root": {
"handlers": ["console"],
"level": "WARNING",
},
}
# Store additional character state for audit trail
CHARACTER_ATTRIBUTES_DEFAULT = {
"last_location": None,
"location_history": [],
"command_count": 0,
"playtime_seconds": 0,
"last_command_time": None,
}
Task 2: Create AuditedCharacter Typeclass
File: typeclasses/audited_character.py
"""
AuditedCharacter - A character typeclass with full audit logging.
"""
import time
from datetime import datetime
from evennia import DefaultCharacter
from evennia.utils import logger
class AuditedCharacter(DefaultCharacter):
"""Character typeclass with comprehensive audit logging."""
def at_object_creation(self):
"""Set up audit attributes when character is created."""
super().at_object_creation()
self.db.location_history = []
self.db.command_count = 0
self.db.total_playtime = 0
self.db.session_start_time = None
self.db.last_location = None
logger.log_info(f"AUDIT: Character '{self.key}' created")
def at_pre_move(self, destination, **kwargs):
"""Called before moving - log departure."""
current = self.location
if current:
logger.log_info(f"AUDIT MOVE: {self.key} leaving {current.key}")
return super().at_pre_move(destination, **kwargs)
def at_post_move(self, source_location, **kwargs):
"""Called after moving - record arrival."""
destination = self.location
timestamp = datetime.utcnow().isoformat()
history = self.db.location_history or []
history.append({
"from": source_location.key if source_location else "Nowhere",
"to": destination.key if destination else "Nowhere",
"timestamp": timestamp,
})
self.db.location_history = history[-1000:] # Keep last 1000
self.db.last_location = destination.key if destination else None
logger.log_info(f"AUDIT MOVE: {self.key} arrived at {destination.key}")
super().at_post_move(source_location, **kwargs)
def at_pre_cmd(self, cmd, args):
"""Called before executing any command."""
self.db.command_count = (self.db.command_count or 0) + 1
self.db.last_command_time = datetime.utcnow().isoformat()
cmd_name = cmd.key if cmd else "unknown"
if cmd_name not in ("password", "@password"):
logger.log_info(f"AUDIT CMD: {self.key} executed '{cmd_name}'")
super().at_pre_cmd(cmd, args)
def at_pre_puppet(self, account, session, **kwargs):
"""Called when account takes control of character."""
self.db.session_start_time = time.time()
logger.log_info(f"AUDIT SESSION: {self.key} puppeted by {account.key}")
super().at_pre_puppet(account, session, **kwargs)
def at_post_unpuppet(self, account, session, **kwargs):
"""Called when account releases control."""
start_time = self.db.session_start_time
if start_time:
session_duration = time.time() - start_time
self.db.total_playtime = (self.db.total_playtime or 0) + session_duration
logger.log_info(f"AUDIT SESSION: {self.key} unpuppeted - session {session_duration:.0f}s")
self.db.session_start_time = None
super().at_post_unpuppet(account, session, **kwargs)
def get_audit_summary(self):
"""Return a summary of this character's audit trail."""
history = self.db.location_history or []
return {
"name": self.key,
"location": self.location.key if self.location else "None",
"commands_executed": self.db.command_count or 0,
"total_playtime_hours": round((self.db.total_playtime or 0) / 3600, 2),
"locations_visited": len(history),
"last_location_change": history[-1] if history else None,
"last_command": self.db.last_command_time,
}
Task 3: Migrate Existing Characters
After implementing the above, run:
cd /root/workspace/timmy-academy
source ../evennia-venv/bin/activate
evennia reload
Then in Evennia shell:
from evennia.utils.search import search_object
# Migrate existing characters
for char in search_object("wizard"):
char.swap_typeclass("typeclasses.audited_character.AuditedCharacter", run_start_hooks=False)
print(f"Migrated: {char.key}")
Acceptance Criteria
Proof of Logs (REQUIRED)
Provide screenshots or terminal output showing:
-
server/logs/command_audit.logexists and contains entries like:2025-04-02 18:30:15 | evennia.commands | INFO | AUDIT CMD: wizard executed 'look' -
server/logs/movement_audit.logexists and contains entries like:2025-04-02 18:31:22 | evennia.objects | INFO | AUDIT MOVE: wizard arrived at Library -
server/logs/player_activity.logexists and contains entries like:2025-04-02 18:30:10 | evennia.accounts | INFO | AUDIT SESSION: wizard puppeted by Alexander
Game History Link/Access (REQUIRED)
-
Document the command to query audit data in-game:
@py from evennia.utils.search import search_object; print(search_object("wizard")[0].get_audit_summary()) -
Provide URL for viewing logs (if web access configured) OR
-
Provide SSH/terminal command to tail logs:
tail -f /root/workspace/timmy-academy/server/logs/command_audit.log -
Include sample
get_audit_summary()output showing:- Character name
- Current location
- Commands executed count
- Total playtime in hours
- Locations visited count
- Last activity timestamp
Verification Steps
- Connect to game:
telnet 167.99.126.228 4000 - Login as:
Alexander / WizardKing2025! - Execute commands:
look,who,@users - Verify logs: Check that
command_audit.logrecorded these commands - Move between rooms: Walk to different locations
- Verify movement: Check
movement_audit.logfor movement entries - Check audit summary: Run
@pycommand from acceptance criteria
Definition of Done
- All three log files exist and are being written to
- Log entries include timestamps, player names, and action details
- In-game audit query command documented and tested
- At least one character migrated to AuditedCharacter class
- Sample audit summary output provided in ticket comments
- Link/command to view game history documented
Notes
- Reference implementation already exists on server at
/root/workspace/timmy-academy/ - Contact @Allegro if implementation questions arise
- Server IP:
167.99.126.228 - Web client:
http://167.99.126.228:4001/webclient
Created by: @Allegro
Date: 2025-04-02