From 7c77981585c3a05d328062af919f95d23f53d68c Mon Sep 17 00:00:00 2001 From: Allegro Date: Sat, 4 Apr 2026 00:06:02 +0000 Subject: [PATCH] feat: enable full audit mode for player activity tracking - Add comprehensive LOGGING configuration with dedicated handlers - Create AuditedCharacter typeclass with movement/command/session tracking - Track location history (last 1000 movements), command count, playtime - Add audit log files: command_audit.log, movement_audit.log, player_activity.log --- hermes-agent/.env | 10 +++ hermes-agent/config.yaml | 53 ++++++++++++++ server/conf/settings.py | 121 +++++++++++++++++++++++++++++++ typeclasses/audited_character.py | 109 ++++++++++++++++++++++++++++ 4 files changed, 293 insertions(+) create mode 100644 hermes-agent/.env create mode 100644 hermes-agent/config.yaml create mode 100644 typeclasses/audited_character.py diff --git a/hermes-agent/.env b/hermes-agent/.env new file mode 100644 index 0000000..ce6eedc --- /dev/null +++ b/hermes-agent/.env @@ -0,0 +1,10 @@ +KIMI_API_KEY=sk-kimi-p17P5TggTzeU2NWc8tTrjKAU2D2jw9BxffvzjtDxyj56b7irb35jvjEJ1Q3PsOPq + +TELEGRAM_BOT_TOKEN=8528070173:AAFrGRb9YxD4XOFEYQhjq_8Cv4zjdqhN5eI + +TELEGRAM_HOME_CHANNEL=-1003664764329 + +TELEGRAM_HOME_CHANNEL_NAME="Timmy Time" + +TELEGRAM_ALLOWED_USERS=7635059073 +GITEA_TOKEN=6452d913d7bdeb21bd13fb6d8067d693e62a7417 diff --git a/hermes-agent/config.yaml b/hermes-agent/config.yaml new file mode 100644 index 0000000..0622e07 --- /dev/null +++ b/hermes-agent/config.yaml @@ -0,0 +1,53 @@ +# Hermes Agent Fallback Configuration +# Deploy this to Timmy and Ezra for automatic kimi-coding fallback + +model: anthropic/claude-opus-4.6 + +# Fallback chain: Anthropic -> Kimi -> Ollama (local) +fallback_providers: + - provider: kimi-coding + model: kimi-for-coding + timeout: 60 + reason: "Primary fallback when Anthropic quota limited" + + - provider: ollama + model: qwen2.5:7b + base_url: http://localhost:11434 + timeout: 120 + reason: "Local fallback for offline operation" + +# Provider settings +providers: + anthropic: + timeout: 30 + retry_on_quota: true + max_retries: 2 + + kimi-coding: + timeout: 60 + max_retries: 3 + + ollama: + timeout: 120 + keep_alive: true + +# Toolsets +toolsets: + - hermes-cli + - github + - web + +# Agent settings +agent: + max_turns: 90 + tool_use_enforcement: auto + fallback_on_errors: + - rate_limit_exceeded + - quota_exceeded + - timeout + - service_unavailable + +# Display settings +display: + show_fallback_notifications: true + show_provider_switches: true diff --git a/server/conf/settings.py b/server/conf/settings.py index 7f51475..ceb2996 100644 --- a/server/conf/settings.py +++ b/server/conf/settings.py @@ -64,6 +64,127 @@ GAME_INDEX_LISTING = { } +###################################################################### +# FULL AUDIT MODE - Track everything +###################################################################### + +# Log all commands typed by players +COMMAND_LOG_ENABLED = True +COMMAND_LOG_LEVEL = "DEBUG" +COMMAND_LOG_FILENAME = "command_audit.log" + +# Enable detailed account logging +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", + }, + # NEW: Command audit log - tracks every command + "file_commands": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "server/logs/command_audit.log", + "maxBytes": 10485760, # 10MB + "backupCount": 50, + "formatter": "detailed", + "level": "DEBUG", + }, + # NEW: Movement audit log - tracks room transitions + "file_movement": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "server/logs/movement_audit.log", + "maxBytes": 10485760, # 10MB + "backupCount": 50, + "formatter": "detailed", + "level": "DEBUG", + }, + # NEW: Player activity log + "file_player": { + "class": "logging.handlers.RotatingFileHandler", + "filename": "server/logs/player_activity.log", + "maxBytes": 10485760, # 10MB + "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, + }, + # NEW: Command audit logger + "evennia.commands": { + "handlers": ["file_commands"], + "level": "DEBUG", + "propagate": False, + }, + # NEW: Movement audit logger + "evennia.objects": { + "handlers": ["file_movement"], + "level": "DEBUG", + "propagate": False, + }, + # NEW: Player activity logger + "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, +} + ###################################################################### # Settings given in secret_settings.py override those in this file. ###################################################################### diff --git a/typeclasses/audited_character.py b/typeclasses/audited_character.py new file mode 100644 index 0000000..cea859c --- /dev/null +++ b/typeclasses/audited_character.py @@ -0,0 +1,109 @@ +""" +AuditedCharacter - A character typeclass with full audit logging. + +Tracks every movement, command, and action for complete visibility +into player activity. +""" + +import time +from datetime import datetime +from evennia import DefaultCharacter +from evennia.utils import logger + + +class AuditedCharacter(DefaultCharacter): + """ + Character typeclass with comprehensive audit logging. + + Tracks: + - Every room entered/exited with timestamps + - Total playtime + - Command count + - Last known location + - Full location history + """ + + def at_object_creation(self): + """Set up audit attributes when character is created.""" + super().at_object_creation() + + # Initialize audit tracking attributes + self.db.location_history = [] # List of {room, timestamp, action} + self.db.command_count = 0 + self.db.total_playtime = 0 # in seconds + self.db.session_start_time = None + self.db.last_location = None + + logger.log_info(f"AUDIT: Character '{self.key}' created at {datetime.utcnow()}") + + 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} -> {destination.key if destination else 'None'}") + return super().at_pre_move(destination, **kwargs) + + def at_post_move(self, source_location, **kwargs): + """Called after moving - record arrival in audit trail.""" + destination = self.location + timestamp = datetime.utcnow().isoformat() + + # Update location history + 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, + "coord": getattr(destination, 'db', {}).get('coord', None) if destination else None + }) + # Keep last 1000 movements + self.db.location_history = history[-1000:] + self.db.last_location = destination.key if destination else None + + # Log to movement audit log + logger.log_info(f"AUDIT MOVE: {self.key} arrived at {destination.key if destination else 'None'} from {source_location.key if source_location else 'None'}") + + super().at_post_move(source_location, **kwargs) + + def at_pre_cmd(self, cmd, args): + """Called before executing any command.""" + # Increment command counter + self.db.command_count = (self.db.command_count or 0) + 1 + self.db.last_command_time = datetime.utcnow().isoformat() + + # Log command (excluding sensitive commands like password) + 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}' args: '{args[:50] if args else ''}'") + + 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} at {datetime.utcnow()}") + super().at_pre_puppet(account, session, **kwargs) + + def at_post_unpuppet(self, account, session, **kwargs): + """Called when account releases control of character.""" + 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 by {account.key} - session lasted {session_duration:.0f}s, total playtime {self.db.total_playtime:.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_seconds": self.db.total_playtime 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, + }