Compare commits

...

5 Commits

Author SHA1 Message Date
Alexander Whitestone
ff3d9ff238 fix: Add audit log rotation to prevent unbounded growth (closes #10)
- Add _audit_log() with per-session rate limiting (default 100 entries)
- Configurable audit_max_history (500) and audit_max_log_entries (100)
- Add prune_audit_history() for manual trimming
- Reset log counter on each puppet session
- Replace hardcoded 1000 cap with configurable audit_max_history
2026-04-21 03:13:09 -04:00
3afdec9019 Merge PR #21
Merged PR #21: security: add .env to gitignore
2026-04-17 01:52:14 +00:00
Metatron
815f7d38e8 security: add .env to gitignore, create .env.example (#17)
hermes-agent/.env contained API credentials committed to repo.

Fix:
- Add .env to .gitignore (prevent future commits)
- Create .env.example with placeholders
- NOTE: Exposed credentials need immediate rotation
2026-04-15 21:56:18 -04:00
0aa6699356 Merge PR #20: fix: Replace hardcoded path with dynamic derivatio 2026-04-15 06:17:27 +00:00
37cecdf95a fix: Replace hardcoded path with dynamic derivation (closes #18) 2026-04-15 03:45:02 +00:00
4 changed files with 97 additions and 25 deletions

5
.gitignore vendored
View File

@@ -54,3 +54,8 @@ nosetests.xml
# VSCode config
.vscode
# Environment variables — never commit secrets
.env
*.env
!.env.example

15
hermes-agent/.env.example Normal file
View File

@@ -0,0 +1,15 @@
# hermes-agent/.env.example
# Copy to .env and fill in real values. NEVER commit .env to git.
# Ref: #17
# API Keys (rotate if exposed)
KIMI_API_KEY=your-kimi-api-key-here
# Telegram
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
TELEGRAM_HOME_CHANNEL=your-channel-id-here
TELEGRAM_HOME_CHANNEL_NAME="Your Channel Name"
TELEGRAM_ALLOWED_USERS=comma-separated-user-ids
# Gitea
GITEA_TOKEN=your-gitea-token-here

View File

@@ -2,7 +2,8 @@
AuditedCharacter - A character typeclass with full audit logging.
Tracks every movement, command, and action for complete visibility
into player activity.
into player activity. Supports configurable log rotation to prevent
unbounded growth.
"""
import time
@@ -10,44 +11,81 @@ from datetime import datetime
from evennia import DefaultCharacter
from evennia.utils import logger
# Default audit retention limits
DEFAULT_MAX_HISTORY = 500
DEFAULT_MAX_LOG_ENTRIES = 100 # Max AUDIT log lines per character per server session
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
- Full location history (rotated)
Configurable via class attributes:
audit_max_history (int): Max location_history entries kept in db (default 500)
audit_max_log_entries (int): Max AUDIT log lines per server session (default 100)
"""
audit_max_history = DEFAULT_MAX_HISTORY
audit_max_log_entries = DEFAULT_MAX_LOG_ENTRIES
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
self.db.audit_log_count = 0 # Tracks log entries this session for rate limiting
logger.log_info(f"AUDIT: Character '{self.key}' created at {datetime.utcnow()}")
def _audit_log(self, message):
"""Write an audit log entry with rate limiting per server session."""
count = (self.db.audit_log_count or 0) + 1
if count <= self.audit_max_log_entries:
logger.log_info(message)
if count == self.audit_max_log_entries:
logger.log_info(
f"AUDIT: {self.key} reached log limit ({self.audit_max_log_entries}) "
f"- suppressing further audit logs this session"
)
self.db.audit_log_count = count
def prune_audit_history(self, max_entries=None):
"""Trim location_history to max_entries. Returns number of entries removed."""
max_entries = max_entries or self.audit_max_history
history = self.db.location_history or []
if len(history) > max_entries:
removed = len(history) - max_entries
self.db.location_history = history[-max_entries:]
return removed
return 0
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'}")
self._audit_log(
f"AUDIT MOVE: {self.key} leaving {current.key} "
f"-> {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({
@@ -56,41 +94,55 @@ class AuditedCharacter(DefaultCharacter):
"timestamp": timestamp,
"coord": getattr(destination, 'db', {}).get('coord', None) if destination else None
})
# Keep last 1000 movements
self.db.location_history = history[-1000:]
# Rotate: keep last audit_max_history movements
self.db.location_history = history[-self.audit_max_history:]
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'}")
self._audit_log(
f"AUDIT MOVE: {self.key} arrived at "
f"{destination.key if destination else 'None'} from "
f"{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 ''}'")
self._audit_log(
f"AUDIT CMD: {self.key} executed '{cmd_name}' "
f"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()}")
self.db.audit_log_count = 0 # Reset log counter each session
self._audit_log(
f"AUDIT SESSION: {self.key} puppeted by {account.key} "
f"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._audit_log(
f"AUDIT SESSION: {self.key} unpuppeted by {account.key} "
f"- session lasted {session_duration:.0f}s, "
f"total playtime {self.db.total_playtime:.0f}s"
)
self.db.session_start_time = None
super().at_post_unpuppet(account, session, **kwargs)

View File

@@ -9,7 +9,7 @@ and configures the Public channel.
Safe to rerun (idempotent).
Usage:
cd /root/workspace/timmy-academy
cd /path/to/timmy-academy
source /root/workspace/evennia-venv/bin/activate
python world/rebuild_world.py
"""
@@ -19,7 +19,7 @@ import re
import ast
os.environ["DJANGO_SETTINGS_MODULE"] = "server.conf.settings"
sys.path.insert(0, "/root/workspace/timmy-academy")
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import django
django.setup()