From 52d288406a76d418f5a564f974c48854762e5af3 Mon Sep 17 00:00:00 2001 From: Seth Date: Sun, 15 Mar 2026 19:56:50 -0400 Subject: [PATCH] Add sudo translator agent with whitelist and user lock - New sudo chat trigger: 'sudo ' - Authorized user only (configurable, default slingshooter08) - Uses command_model to translate natural language to JSON commands - Executes commands through existing whitelist/validator pipeline - No God persona or speech call in sudo mode - Added sudo_enabled/sudo_user/sudo_max_commands config keys - Added common give-item alias normalization (wood->oak_log, bed->white_bed) - Updated README with sudo usage and config docs --- README.md | 26 ++++++++ mc_aigod.py | 140 +++++++++++++++++++++++++++++++++++++++++-- mc_aigod_shrink.json | 3 + 3 files changed, 165 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e8b1591..39af29d 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,9 @@ Minecraft_Ai_God.md # Full design document with architecture details | `max_commands_per_response` | int | `6` | Max commands God can issue per prayer | | `interventions_per_day` | float | `4` | Avg unprompted interventions per 24h. `0` to disable | | `debug_commands` | bool | `false` | Show executed commands in-game via dark gray tellraw | +| `sudo_enabled` | bool | `true` | Enable sudo translator mode | +| `sudo_user` | string | `"slingshooter08"` | Only this username can execute sudo commands | +| `sudo_max_commands` | int | `3` | Max translated commands per sudo request | | `memory_path` | string | see below | Path to persist prayer memory JSON | | `god_chat_prefix` | string | `"[GOD]"` | Chat prefix (supports Minecraft color codes) | @@ -120,6 +123,9 @@ Default memory path: `/aigod_memory.json` "max_commands_per_response": 6, "interventions_per_day": 4, "debug_commands": false, + "sudo_enabled": true, + "sudo_user": "slingshooter08", + "sudo_max_commands": 3, "memory_path": "/path/to/minecraft/aigod_memory.json", "god_chat_prefix": "[§6§lGOD§r]" } @@ -134,6 +140,26 @@ Type in chat (no slash — vanilla 1.21 rejects unknown slash commands client-si ``` pray — send a prayer to God bible — show help/guidance +sudo — command translator mode (authorized user only) +``` + +### Sudo Translator Mode + +`sudo` is a separate agent path bundled in the same script. It does not use God's persona or speech pipeline. + +- Trigger: `sudo ` +- Authorization: only `sudo_user` (default `slingshooter08`) +- Model: uses `command_model` +- Output: JSON commands only, then executes via same whitelist validator +- No divine speech generated + +Example: +``` +sudo give me 500 wood +``` +Best-effort translation: +``` +give slingshooter08 minecraft:oak_log 500 ``` On login players see: diff --git a/mc_aigod.py b/mc_aigod.py index c577a72..a8d31d7 100644 --- a/mc_aigod.py +++ b/mc_aigod.py @@ -31,6 +31,10 @@ BIBLE_PATTERNS = [ re.compile(r'\[.*?\]: <(\w+)> [Bb]ible\s*$'), ] +SUDO_PATTERNS = [ + re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'), +] + JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game') LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game') @@ -603,6 +607,20 @@ COMMANDS_SYSTEM_PROMPT = ( + ITEM_LIBRARY ) +SUDO_COMMANDS_SYSTEM_PROMPT = ( + "You are a Minecraft command translator. Convert a player's natural-language request into " + "Minecraft server commands. You do NOT roleplay.\n\n" + "Respond ONLY with valid JSON:\n" + "{\"commands\": [\"cmd1\", \"cmd2\"]}\n\n" + "Rules:\n" + "- Use commands from this whitelist only: give, effect, xp, tp, time, weather, execute, kill, summon, tellraw, worldborder.\n" + "- If the request cannot be mapped safely, return commands: [].\n" + "- If player says 'me' or 'my', target the requesting player.\n" + "- For give syntax: give minecraft: \n" + "- Count is last. Namespace minecraft: is required.\n" + "- Return commands only. No commentary.\n" +) + def build_message_system_prompt(config) -> str: base = ( "You are God in a Minecraft server. You are benevolent but just. " @@ -816,18 +834,34 @@ def fix_give_command(cmd: str) -> str: return cmd player, arg2, arg3, rest = m.group(1), m.group(2), m.group(3), m.group(4) + def normalize_item(item: str) -> str: + # Strip namespace for alias mapping, then re-apply + raw = item.replace("minecraft:", "") + aliases = { + "wood": "oak_log", + "logs": "oak_log", + "log": "oak_log", + "planks": "oak_planks", + "plank": "oak_planks", + "food": "bread", + "heal": "golden_apple", + "healing": "golden_apple", + "bed": "white_bed", + } + raw = aliases.get(raw, raw) + return f"minecraft:{raw}" + # Detect transposed order: give player if arg2.isdigit(): count, item = arg2, arg3 - if not item.startswith("minecraft:"): - item = f"minecraft:{item}" + item = normalize_item(item) fixed = f"give {player} {item} {count}{rest}" log.warning(f"Fixed transposed give: '{cmd}' -> '{fixed}'") return fixed # Detect missing namespace: give player - if not arg2.startswith("minecraft:") and not arg2.startswith("{"): - item = f"minecraft:{arg2}" + if not arg2.startswith("{"): + item = normalize_item(arg2) fixed = f"give {player} {item} {arg3}{rest}" log.warning(f"Fixed missing namespace: '{cmd}' -> '{fixed}'") return fixed @@ -910,6 +944,86 @@ def execute_response(response, context, config, praying_player=None): elif "clear" in resolved: config["_weather_state"] = "clear" time.sleep(0.3) +def process_sudo(player, prompt, config): + """ + sudo translator mode: + - no God persona + - no speech generation + - translates natural language to whitelisted commands + - only authorized user can execute + """ + if not config.get("sudo_enabled", True): + return + + sudo_user = config.get("sudo_user", "slingshooter08") + if player != sudo_user: + # Keep this private and quiet + rcon( + f'tellraw {player} {{"text":"[SUDO] Unauthorized.","color":"red"}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + return + + # Immediate private ack + rcon( + f'tellraw {player} {{"text":"[SUDO] Translating...","color":"gray","italic":true}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + + online = players_online(config) + context_hint = ( + f"Requesting player: {player}\n" + f"Online players: {', '.join(online) or 'none'}\n" + f"Natural language request: {prompt}\n" + ) + + command_model = config.get("command_model", config["model"]) + try: + content = _llm_call( + model=command_model, + system=SUDO_COMMANDS_SYSTEM_PROMPT, + user=context_hint, + config=config, + fmt="json", + temperature=0.1, + max_tokens=180, + ) + parsed = _parse_llm_json(content) + commands = parsed.get("commands") or [] + except Exception as e: + log.error(f"SUDO translation failed: {e}") + rcon( + f'tellraw {player} {{"text":"[SUDO] Translation failed.","color":"red"}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + return + + max_cmds = config.get("sudo_max_commands", 3) + commands = commands[:max_cmds] + + if not commands: + rcon( + f'tellraw {player} {{"text":"[SUDO] No safe command generated.","color":"yellow"}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + return + + # Show translated command(s) privately + safe_preview = " | ".join(commands).replace("\\", "\\\\").replace('"', '\\"') + rcon( + f'tellraw {player} {{"text":"[SUDO] {safe_preview}","color":"dark_gray","italic":true}}', + config["rcon_host"], config["rcon_port"], config["rcon_password"] + ) + + for cmd in commands: + resolved, is_safe = validate_command(cmd, online, player) + if not is_safe: + continue + log.info(f"SUDO execute: {resolved}") + result = rcon(resolved, config["rcon_host"], config["rcon_port"], config["rcon_password"]) + log.info(f"SUDO result: {result!r}") + time.sleep(0.2) + # --------------------------------------------------------------------------- # Prayer handler # --------------------------------------------------------------------------- @@ -1097,6 +1211,24 @@ def main(): # Feed every line into the rolling log buffer add_log_event(line) + # sudo translator + matched = False + for pat in SUDO_PATTERNS: + m = pat.search(line) + if m: + player = m.group(1) + prompt = m.group(2).strip() + log.info(f"SUDO from {player}: {prompt}") + try: + process_sudo(player, prompt, config) + except Exception as e: + log.error(f"Error processing sudo: {e}", exc_info=True) + matched = True + break + + if matched: + continue + # /pray matched = False for pat in PRAY_PATTERNS: diff --git a/mc_aigod_shrink.json b/mc_aigod_shrink.json index 396a4b6..159d6fa 100644 --- a/mc_aigod_shrink.json +++ b/mc_aigod_shrink.json @@ -14,6 +14,9 @@ "interventions_per_day": 48, "god_chat_prefix": "[§6§lGOD§r]", "debug_commands": true, + "sudo_enabled": true, + "sudo_user": "slingshooter08", + "sudo_max_commands": 3, "god_lore": "This is the shrink-world server. The world border started at 1000x1000 and shrinks by 1 block each time a player dies, alternating N/S and E/W sides. There are more creepers than normal (5x spawn rate). The goal is survival. Players who die too often will eventually have nowhere left to go.", "memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json" }