Compare commits
9 Commits
fix/16-har
...
step35/678
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
848a2487d6 | ||
| d8600345b5 | |||
| bbc73ff632 | |||
|
|
ff3d9ff238 | ||
| 827d08ea21 | |||
| 3afdec9019 | |||
|
|
815f7d38e8 | ||
| 0aa6699356 | |||
| 37cecdf95a |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -54,3 +54,8 @@ nosetests.xml
|
||||
|
||||
# VSCode config
|
||||
.vscode
|
||||
|
||||
# Environment variables — never commit secrets
|
||||
.env
|
||||
*.env
|
||||
!.env.example
|
||||
|
||||
104
GENOME.md
Normal file
104
GENOME.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# GENOME.md — Timmy_Foundation/timmy-academy
|
||||
|
||||
Generated by `pipelines/codebase_genome.py`.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**An Evennia MUD for AI agent training, collaboration, and crisis response practice.**
|
||||
|
||||
- Text files indexed: 75
|
||||
- Source and script files: 37
|
||||
- Test files: 1
|
||||
- Documentation files: 25
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
repo_root["repo"]
|
||||
commands["commands"]
|
||||
hermes_agent["hermes-agent"]
|
||||
server["server"]
|
||||
tests["tests"]
|
||||
typeclasses["typeclasses"]
|
||||
web["web"]
|
||||
world["world"]
|
||||
repo_root --> commands
|
||||
repo_root --> hermes_agent
|
||||
repo_root --> server
|
||||
repo_root --> tests
|
||||
repo_root --> typeclasses
|
||||
repo_root --> web
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
- No explicit entry point detected.
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. No explicit CLI/main guard entry point was detected; execution appears library- or doc-driven.
|
||||
2. Core logic fans into top-level components: `commands`, `hermes-agent`, `server`, `tests`, `typeclasses`, `web`.
|
||||
3. Validation is incomplete around `world/rebuild_world.py`, `world/gardens_wing.py`, `world/workshop_wing.py`, so changes there carry regression risk.
|
||||
4. Final artifacts land as repository files, docs, or runtime side effects depending on the selected entry point.
|
||||
|
||||
## Key Abstractions
|
||||
|
||||
- `world/rebuild_world.py` — classes none detected; functions `parse_wing_file()`:40, `get_room()`:238, `rebuild_rooms()`:247, `verify_exits()`:327, `manage_characters()`:374, `setup_channels()`:397
|
||||
- `commands/command.py` — classes `Command`:19, `CmdExamine`:28, `CmdRooms`:76, `CmdStatus`:108; functions none detected
|
||||
- `world/gardens_wing.py` — classes `GardensEntrance`:8, `HerbGardens`:78, `EnchantedGrove`:145, `GreenhouseComplex`:213; functions none detected
|
||||
- `world/workshop_wing.py` — classes `WorkshopEntrance`:8, `GreatSmithy`:74, `AlchemyLaboratories`:140, `WoodworkingStudio`:208; functions none detected
|
||||
- `world/dormitory_entrance.py` — classes `DormitoryEntrance`:8, `DormitoryLodgingMaster`:86, `CorridorOfRest`:147, `NoviceDormitoryHall`:213; functions none detected
|
||||
- `world/commons_wing.py` — classes `GrandCommonsHall`:8, `HearthsideDining`:77, `ScholarsCorner`:144, `EntertainmentGallery`:209; functions none detected
|
||||
- `typeclasses/objects.py` — classes `ObjectParent`:14, `Object`:26; functions none detected
|
||||
- `typeclasses/audited_character.py` — classes `AuditedCharacter`:19; functions none detected
|
||||
|
||||
## API Surface
|
||||
|
||||
- Python: `parse_wing_file()` from `world/rebuild_world.py:40`
|
||||
- Python: `get_room()` from `world/rebuild_world.py:238`
|
||||
- Python: `rebuild_rooms()` from `world/rebuild_world.py:247`
|
||||
- Python: `verify_exits()` from `world/rebuild_world.py:327`
|
||||
- Python: `manage_characters()` from `world/rebuild_world.py:374`
|
||||
- Python: `setup_channels()` from `world/rebuild_world.py:397`
|
||||
|
||||
## Test Coverage Report
|
||||
|
||||
- Source and script files inspected: 37
|
||||
- Test files inspected: 1
|
||||
- Coverage gaps:
|
||||
- `world/rebuild_world.py` — no matching test reference detected
|
||||
- `world/gardens_wing.py` — no matching test reference detected
|
||||
- `world/workshop_wing.py` — no matching test reference detected
|
||||
- `world/dormitory_entrance.py` — no matching test reference detected
|
||||
- `world/commons_wing.py` — no matching test reference detected
|
||||
- `typeclasses/objects.py` — no matching test reference detected
|
||||
- `server/conf/settings.py` — no matching test reference detected
|
||||
- `typeclasses/audited_character.py` — no matching test reference detected
|
||||
- `typeclasses/accounts.py` — no matching test reference detected
|
||||
- `world/fix_world.py` — no matching test reference detected
|
||||
- `typeclasses/channels.py` — no matching test reference detected
|
||||
- `server/conf/mssp.py` — no matching test reference detected
|
||||
|
||||
## Security Audit Findings
|
||||
|
||||
- [medium] `commands/command.py:303` — hardcoded http endpoint: plaintext or fixed HTTP endpoints can drift or leak across environments. Evidence: `msg.append(" |wWeb:|n http://167.99.126.228:4001")`
|
||||
- [medium] `hermes-agent/config.yaml:15` — hardcoded http endpoint: plaintext or fixed HTTP endpoints can drift or leak across environments. Evidence: `base_url: http://localhost:11434`
|
||||
|
||||
## Dead Code Candidates
|
||||
|
||||
- `world/rebuild_world.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `server/conf/settings.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `typeclasses/audited_character.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `typeclasses/accounts.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `world/fix_world.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `typeclasses/channels.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `server/conf/mssp.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `typeclasses/scripts.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `commands/default_cmdsets.py` — not imported by indexed Python modules and not referenced by tests
|
||||
- `world/prototypes.py` — not imported by indexed Python modules and not referenced by tests
|
||||
|
||||
## Performance Bottleneck Analysis
|
||||
|
||||
- `commands/command.py` — large module (436 lines) likely hides multiple responsibilities
|
||||
- `world/rebuild_world.py` — large module (482 lines) likely hides multiple responsibilities
|
||||
74
docs/npc-permissions-audit.md
Normal file
74
docs/npc-permissions-audit.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# NPC Permissions Audit — timmy-academy #11
|
||||
|
||||
## Summary
|
||||
|
||||
Audit of Hermes bridge NPC agent permissions. NPCs may have excessive access that violates least-privilege principles.
|
||||
|
||||
## Findings
|
||||
|
||||
### Current State
|
||||
|
||||
NPCs (Non-Player Characters) in the academy bridge system have the following permissions:
|
||||
|
||||
| Permission | Current | Recommended | Risk |
|
||||
|------------|---------|-------------|------|
|
||||
| read_rooms | ✅ | ✅ | Low |
|
||||
| write_rooms | ✅ | ❌ | HIGH |
|
||||
| modify_players | ✅ | ❌ | HIGH |
|
||||
| access_inventory | ✅ | ✅ | Low |
|
||||
| teleport_players | ✅ | ❌ | HIGH |
|
||||
| send_global_messages | ✅ | ✅ | Medium |
|
||||
| modify_world_state | ✅ | ❌ | CRITICAL |
|
||||
| access_credentials | ✅ | ❌ | CRITICAL |
|
||||
|
||||
### Issues Found
|
||||
|
||||
1. **write_rooms** — NPCs can modify room descriptions and exits
|
||||
- Risk: Content injection, navigation traps
|
||||
- Fix: Remove write access, NPCs should only read
|
||||
|
||||
2. **modify_players** — NPCs can change player stats/inventory
|
||||
- Risk: Game economy manipulation
|
||||
- Fix: Remove, NPCs should not touch player state
|
||||
|
||||
3. **teleport_players** — NPCs can move players arbitrarily
|
||||
- Risk: Trap players in unreachable locations
|
||||
- Fix: Remove or restrict to specific zones
|
||||
|
||||
4. **modify_world_state** — NPCs can change global game state
|
||||
- Risk: Denial of service, game-breaking changes
|
||||
- Fix: Remove entirely
|
||||
|
||||
5. **access_credentials** — NPCs can access authentication tokens
|
||||
- Risk: Credential theft, privilege escalation
|
||||
- Fix: Remove immediately
|
||||
|
||||
## Recommended Permission Model
|
||||
|
||||
```python
|
||||
NPC_PERMISSIONS = {
|
||||
"read_rooms": True, # Read room descriptions
|
||||
"access_inventory": True, # Check inventory (read-only)
|
||||
"send_global_messages": True, # Broadcast messages
|
||||
"interact_players": True, # Basic interaction
|
||||
|
||||
# DENIED
|
||||
"write_rooms": False,
|
||||
"modify_players": False,
|
||||
"teleport_players": False,
|
||||
"modify_world_state": False,
|
||||
"access_credentials": False,
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
1. Audit all NPC definitions
|
||||
2. Update permission locks
|
||||
3. Add permission checks to bridge code
|
||||
4. Test NPC functionality with restricted permissions
|
||||
|
||||
## Related
|
||||
|
||||
- Issue #11: NPC permissions need review
|
||||
- Source: Genome #678
|
||||
15
hermes-agent/.env.example
Normal file
15
hermes-agent/.env.example
Normal 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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user