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:
@@ -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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `memory_path` | string | see below | Path to persist prayer memory JSON |
|
||||||
| `god_chat_prefix` | string | `"[GOD]"` | Chat prefix (supports Minecraft color codes) |
|
| `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,
|
"max_commands_per_response": 6,
|
||||||
"interventions_per_day": 4,
|
"interventions_per_day": 4,
|
||||||
"debug_commands": false,
|
"debug_commands": false,
|
||||||
|
"sudo_enabled": true,
|
||||||
|
"sudo_user": "slingshooter08",
|
||||||
|
"sudo_max_commands": 3,
|
||||||
"memory_path": "/path/to/minecraft/aigod_memory.json",
|
"memory_path": "/path/to/minecraft/aigod_memory.json",
|
||||||
"god_chat_prefix": "[§6§lGOD§r]"
|
"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
|
pray <message> — send a prayer to God
|
||||||
bible — show help/guidance
|
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:
|
On login players see:
|
||||||
|
|||||||
+136
-4
@@ -31,6 +31,10 @@ BIBLE_PATTERNS = [
|
|||||||
re.compile(r'\[.*?\]: <(\w+)> [Bb]ible\s*$'),
|
re.compile(r'\[.*?\]: <(\w+)> [Bb]ible\s*$'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SUDO_PATTERNS = [
|
||||||
|
re.compile(r'\[.*?\]: <(\w+)> [Ss]udo (.+)'),
|
||||||
|
]
|
||||||
|
|
||||||
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
|
JOIN_PATTERN = re.compile(r'\[.*?\]: (\w+) joined the game')
|
||||||
LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game')
|
LEAVE_PATTERN = re.compile(r'\[.*?\]: (\w+) left the game')
|
||||||
|
|
||||||
@@ -603,6 +607,20 @@ COMMANDS_SYSTEM_PROMPT = (
|
|||||||
+ ITEM_LIBRARY
|
+ 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:
|
def build_message_system_prompt(config) -> str:
|
||||||
base = (
|
base = (
|
||||||
"You are God in a Minecraft server. You are benevolent but just. "
|
"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
|
return cmd
|
||||||
player, arg2, arg3, rest = m.group(1), m.group(2), m.group(3), m.group(4)
|
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>
|
# Detect transposed order: give player <number> <item>
|
||||||
if arg2.isdigit():
|
if arg2.isdigit():
|
||||||
count, item = arg2, arg3
|
count, item = arg2, arg3
|
||||||
if not item.startswith("minecraft:"):
|
item = normalize_item(item)
|
||||||
item = f"minecraft:{item}"
|
|
||||||
fixed = f"give {player} {item} {count}{rest}"
|
fixed = f"give {player} {item} {count}{rest}"
|
||||||
log.warning(f"Fixed transposed give: '{cmd}' -> '{fixed}'")
|
log.warning(f"Fixed transposed give: '{cmd}' -> '{fixed}'")
|
||||||
return fixed
|
return fixed
|
||||||
|
|
||||||
# Detect missing namespace: give player <item_without_prefix> <count>
|
# Detect missing namespace: give player <item_without_prefix> <count>
|
||||||
if not arg2.startswith("minecraft:") and not arg2.startswith("{"):
|
if not arg2.startswith("{"):
|
||||||
item = f"minecraft:{arg2}"
|
item = normalize_item(arg2)
|
||||||
fixed = f"give {player} {item} {arg3}{rest}"
|
fixed = f"give {player} {item} {arg3}{rest}"
|
||||||
log.warning(f"Fixed missing namespace: '{cmd}' -> '{fixed}'")
|
log.warning(f"Fixed missing namespace: '{cmd}' -> '{fixed}'")
|
||||||
return fixed
|
return fixed
|
||||||
@@ -910,6 +944,86 @@ def execute_response(response, context, config, praying_player=None):
|
|||||||
elif "clear" in resolved: config["_weather_state"] = "clear"
|
elif "clear" in resolved: config["_weather_state"] = "clear"
|
||||||
time.sleep(0.3)
|
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
|
# Prayer handler
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1097,6 +1211,24 @@ def main():
|
|||||||
# Feed every line into the rolling log buffer
|
# Feed every line into the rolling log buffer
|
||||||
add_log_event(line)
|
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
|
# /pray
|
||||||
matched = False
|
matched = False
|
||||||
for pat in PRAY_PATTERNS:
|
for pat in PRAY_PATTERNS:
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
"interventions_per_day": 48,
|
"interventions_per_day": 48,
|
||||||
"god_chat_prefix": "[§6§lGOD§r]",
|
"god_chat_prefix": "[§6§lGOD§r]",
|
||||||
"debug_commands": true,
|
"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.",
|
"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"
|
"memory_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_memory.json"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user