Add sudo translator agent with whitelist and user lock

- New sudo chat trigger: 'sudo <request>'
- 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
This commit is contained in:
2026-03-15 19:56:50 -04:00
parent c94fa1872a
commit 52d288406a
3 changed files with 165 additions and 4 deletions
+26
View File
@@ -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: `<instance_data_dir>/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 <message> — send a prayer to God
bible — show help/guidance
sudo <request> — 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 <natural language request>`
- 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:
+136 -4
View File
@@ -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 <player> minecraft:<item_id> <count>\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 <number> <item>
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 <item_without_prefix> <count>
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:
+3
View File
@@ -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"
}