From fe36ae37393bff6615719b5a06242c4ebd649e52 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 16 Mar 2026 19:28:51 -0400 Subject: [PATCH] Add teleport border guard, dimension-aware TP, fully enchanted context, enchantment guidance for god --- LangGraph_Implementation_Idea.md | 180 +++++++++++++++++++++++++++++ mc_aigod.py | 188 ++++++++++++++++++++++++++++++- mc_aigod_shrink.json | 4 + 3 files changed, 367 insertions(+), 5 deletions(-) create mode 100644 LangGraph_Implementation_Idea.md diff --git a/LangGraph_Implementation_Idea.md b/LangGraph_Implementation_Idea.md new file mode 100644 index 0000000..b6092ab --- /dev/null +++ b/LangGraph_Implementation_Idea.md @@ -0,0 +1,180 @@ +# LangGraph Implementation Idea + +## Goal + +Add a session-based agent gateway in front of Ollama so the Minecraft AI system can: +- keep per-player/session memory, +- run multi-step tool calls (MCP/web search), +- return a final `message + commands` payload to the existing plugin. + +This is a future enhancement. Current system is working and remains the source of truth for command execution safety. + +--- + +## Why This Exists + +Current `mc_aigod.py` calls Ollama directly. That is effectively stateless unless full history is re-sent every call. + +A LangGraph sidecar can provide: +- persistent sessions/threads, +- tool loop orchestration, +- model routing, +- better observability of intermediate reasoning steps. + +--- + +## Proposed Architecture + +```text +Minecraft chat -> mc_aigod.py -> LangGraph Gateway API -> Ollama + | \ + | -> MCP tools (web search/wiki/etc) + -> Session store (SQLite/Redis) +``` + +`mc_aigod.py` remains responsible for: +- trigger parsing (`pray`, `bible`, `sudo`), +- RCON command execution, +- whitelist/validation/fixups, +- hard safety rules (e.g. first-login kill limits). + +LangGraph gateway is responsible for: +- session state, +- tool calls, +- composing final JSON output. + +--- + +## API Contract Sketch + +### Start session +`POST /v1/session/start` + +Request: +```json +{ + "player": "slingshooter08", + "mode": "god" +} +``` + +Response: +```json +{ + "session_id": "sess_abc123" +} +``` + +### Send message +`POST /v1/session/{session_id}/message` + +Request: +```json +{ + "role": "user", + "text": "pray I need wood for shelter", + "context": { + "server_state": {}, + "player_state": {}, + "recent_events": [] + }, + "allow_tools": true, + "max_tool_steps": 4 +} +``` + +Response: +```json +{ + "message": "Divine response text", + "commands": [ + "give slingshooter08 minecraft:oak_log 64" + ], + "tool_trace": [ + { + "tool": "web.search", + "input": "minecraft oak log item id", + "ok": true + } + ] +} +``` + +### End session (optional) +`POST /v1/session/{session_id}/close` + +--- + +## LangGraph Flow (Draft) + +1. Load session state by `session_id` +2. Add user message + context +3. Call command model +4. If tool requested: + - execute MCP tool + - append tool result + - loop until final commands or step limit reached +5. Call message model with chosen commands +6. Return final `{message, commands}` +7. Persist updated session state + +--- + +## Tooling Plan + +Primary tools to add first: +- `web.search` (general search) +- `minecraft.wiki_lookup` (targeted page fetch/search) + +Potential later tools: +- local documentation lookup, +- server analytics lookup, +- schematic index lookup. + +--- + +## Modes to Support + +- `god` (prayer flow, tool-enabled) +- `sudo` (translator flow, likely tool-disabled or very limited) +- `god_system` (intervention/first-login internal events) + +--- + +## Safety Model (Keep in mc_aigod.py) + +Even after LangGraph is added, keep hard enforcement in plugin runtime: +- whitelist command families, +- syntax repair + normalization, +- max commands cap, +- per-flow constraints (e.g. first-login benevolence restrictions), +- unauthorized sudo user rejection. + +This ensures model/tool errors cannot directly bypass execution safeguards. + +--- + +## MVP Steps (Later) + +1. Build FastAPI LangGraph gateway with in-memory sessions +2. Add `/session/start` + `/session/{id}/message` +3. Mirror current two-call behavior (no tools yet) +4. Switch `mc_aigod.py` to gateway endpoint +5. Add one MCP search tool and bounded tool loop +6. Add persistence (SQLite/Redis) +7. Add structured logs + tool traces + +--- + +## Open Questions + +- Session lifetime policy (per player, per login, time-based expiry?) +- Whether `sudo` should ever be tool-enabled +- How much tool trace to expose in-game vs log-only +- Which MCP stack to standardize on for web search + +--- + +## Notes + +This document is a planning scratchpad for future implementation. It is intentionally practical and API-first so a coder bot can pick it up and implement directly. diff --git a/mc_aigod.py b/mc_aigod.py index c08dac5..6788329 100644 --- a/mc_aigod.py +++ b/mc_aigod.py @@ -526,6 +526,14 @@ EFFECTS (replace {target} with any online player's username): MOVEMENT: tp {target} 0 64 0 + tp {target} + tp {target} ~ ~10 ~ + execute in minecraft:the_nether run tp {target} + execute in minecraft:the_end run tp {target} 0 64 0 + execute in minecraft:overworld run tp {target} + NOTE: To teleport a player to another dimension ALWAYS use: + execute in minecraft: run tp + NEVER use: tp minecraft:the_nether (this is wrong syntax) WORLD/ENVIRONMENT (affects all players): time set day @@ -668,6 +676,36 @@ def _parse_llm_json(content: str) -> dict: log.warning(f"Repaired JSON: message={len(message)}chars, commands={commands}") return result +ENCHANTMENT_CONTEXT = """ +=== ENCHANTMENT RULES FOR GOD === +When giving weapons, tools, or armor as a divine gift, you should ALMOST ALWAYS enchant them. +Enchanted gifts feel more divine. Unenchanted items are acceptable only as a deliberate +choice (e.g. giving basic materials, a punishment of mediocrity, or items that cannot be enchanted). + +Use 1.21 component syntax: give minecraft:[enchantments={ench1:lvl,ench2:lvl}] 1 + +MAX ENCHANTMENT REFERENCE (use as baseline for "fully enchanted" or "blessed" gifts): + +SWORD: netherite_sword[enchantments={sharpness:5,unbreaking:3,looting:3,fire_aspect:2,mending:1,sweeping_edge:3}] +PICKAXE: netherite_pickaxe[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] +AXE: netherite_axe[enchantments={efficiency:5,unbreaking:3,fortune:3,sharpness:5,mending:1}] +SHOVEL: netherite_shovel[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] +HOE: netherite_hoe[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] +BOW: bow[enchantments={power:5,unbreaking:3,infinity:1,flame:1,punch:2}] +CROSSBOW: crossbow[enchantments={multishot:1,quick_charge:3,unbreaking:3,mending:1}] +TRIDENT: trident[enchantments={channeling:1,loyalty:3,unbreaking:3,mending:1,impaling:5}] +HELMET: netherite_helmet[enchantments={protection:4,unbreaking:3,respiration:3,aqua_affinity:1,thorns:3,mending:1}] +CHEST: netherite_chestplate[enchantments={protection:4,unbreaking:3,thorns:3,mending:1}] +LEGS: netherite_leggings[enchantments={protection:4,unbreaking:3,swift_sneak:3,mending:1}] +BOOTS: netherite_boots[enchantments={protection:4,unbreaking:3,feather_falling:4,depth_strider:3,soul_speed:3,mending:1}] +FISHING: fishing_rod[enchantments={luck_of_the_sea:3,lure:3,unbreaking:3,mending:1}] +ELYTRA: elytra[enchantments={unbreaking:3,mending:1}] +SHIELD: shield[enchantments={unbreaking:3,mending:1}] + +You do NOT need to always give max enchants — a modest reward may have fewer. +But unenchanted weapons/tools/armor from God should be the exception, not the rule. +""" + COMMANDS_SYSTEM_PROMPT = ( "You are a Minecraft server command executor. Given a player's prayer and server context, " "decide what server commands to run (if any) as an act of God.\n\n" @@ -688,6 +726,7 @@ COMMANDS_SYSTEM_PROMPT = ( + COMMAND_PALETTE + "\n=== ITEM LIBRARY ===\n" + ITEM_LIBRARY + + ENCHANTMENT_CONTEXT ) SUDO_COMMANDS_SYSTEM_PROMPT = ( @@ -700,9 +739,68 @@ SUDO_COMMANDS_SYSTEM_PROMPT = ( "- If the request cannot be mapped safely, return commands: [].\n" "- If player says 'me' or 'my', target the requesting player.\n" "- You will receive LAST 10 SUDO ACTIONS. Use them for continuity and corrections when the player says previous output was wrong.\n" - "- For give syntax: give minecraft: \n" - "- Count is last. Namespace minecraft: is required.\n" + "- For give syntax: give minecraft: (count LAST, namespace required)\n" "- Return commands only. No commentary.\n" + "\n" + "=== TELEPORT SYNTAX ===\n" + "Same dimension: tp \n" + "Relative: tp ~ ~10 ~\n" + "To Nether: execute in minecraft:the_nether run tp \n" + "To End: execute in minecraft:the_end run tp 64 \n" + "To Overworld: execute in minecraft:overworld run tp \n" + "WRONG (never do this): tp minecraft:the_nether\n" + "When dimension is unspecified, use a sensible default spawn coord for that dimension.\n" + "\n" + "=== FULLY ENCHANTED (max enchantments per item type, 1.21 syntax) ===\n" + "Use item[enchantments={...}] syntax. Count is always 1 for enchanted items.\n" + "\n" + "SWORD (netherite_sword):\n" + " give

minecraft:netherite_sword[enchantments={sharpness:5,unbreaking:3,looting:3,fire_aspect:2,mending:1,sweeping_edge:3}] 1\n" + "\n" + "PICKAXE (netherite_pickaxe):\n" + " give

minecraft:netherite_pickaxe[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] 1\n" + "\n" + "AXE (netherite_axe):\n" + " give

minecraft:netherite_axe[enchantments={efficiency:5,unbreaking:3,fortune:3,sharpness:5,mending:1}] 1\n" + "\n" + "SHOVEL (netherite_shovel):\n" + " give

minecraft:netherite_shovel[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] 1\n" + "\n" + "HOE (netherite_hoe):\n" + " give

minecraft:netherite_hoe[enchantments={efficiency:5,unbreaking:3,fortune:3,mending:1}] 1\n" + "\n" + "BOW:\n" + " give

minecraft:bow[enchantments={power:5,unbreaking:3,infinity:1,flame:1,punch:2}] 1\n" + "\n" + "CROSSBOW:\n" + " give

minecraft:crossbow[enchantments={multishot:1,quick_charge:3,unbreaking:3,mending:1}] 1\n" + "\n" + "TRIDENT:\n" + " give

minecraft:trident[enchantments={channeling:1,loyalty:3,riptide:3,unbreaking:3,mending:1,impaling:5}] 1\n" + " (note: riptide and loyalty/channeling are mutually exclusive — pick one set)\n" + "\n" + "HELMET (netherite_helmet):\n" + " give

minecraft:netherite_helmet[enchantments={protection:4,unbreaking:3,respiration:3,aqua_affinity:1,thorns:3,mending:1}] 1\n" + "\n" + "CHESTPLATE (netherite_chestplate):\n" + " give

minecraft:netherite_chestplate[enchantments={protection:4,unbreaking:3,thorns:3,mending:1}] 1\n" + "\n" + "LEGGINGS (netherite_leggings):\n" + " give

minecraft:netherite_leggings[enchantments={protection:4,unbreaking:3,swift_sneak:3,mending:1}] 1\n" + "\n" + "BOOTS (netherite_boots):\n" + " give

minecraft:netherite_boots[enchantments={protection:4,unbreaking:3,feather_falling:4,depth_strider:3,soul_speed:3,mending:1}] 1\n" + "\n" + "FISHING ROD:\n" + " give

minecraft:fishing_rod[enchantments={luck_of_the_sea:3,lure:3,unbreaking:3,mending:1}] 1\n" + "\n" + "ELYTRA:\n" + " give

minecraft:elytra[enchantments={unbreaking:3,mending:1}] 1\n" + "\n" + "SHIELD:\n" + " give

minecraft:shield[enchantments={unbreaking:3,mending:1}] 1\n" + "\n" + "When player asks for 'fully enchanted', 'max enchanted', 'best', 'godlike' gear — use the above templates.\n" ) FIRST_LOGIN_BENEVOLENCE_PROMPT = ( @@ -921,6 +1019,83 @@ SAFE_PREFIXES = [ 'execute ', 'kill ', 'summon ', 'tellraw ', 'worldborder ', ] +_border_cache = {"ts": 0.0, "half": None} + + +def _tp_border_guard_enabled(config) -> bool: + return bool(config.get("tp_border_guard_enabled", True)) + + +def _parse_abs_coord(tok: str): + tok = (tok or "").strip() + if not tok or tok.startswith("~") or tok.startswith("^"): + return None + try: + return float(tok) + except Exception: + return None + + +def _extract_tp_abs_xyz(cmd: str): + for m in re.finditer(r'\btp\s+\S+\s+(\S+)\s+(\S+)\s+(\S+)', cmd): + x = _parse_abs_coord(m.group(1)) + y = _parse_abs_coord(m.group(2)) + z = _parse_abs_coord(m.group(3)) + if x is None or y is None or z is None: + continue + return x, y, z + return None + + +def _worldborder_half_extent(config): + now = time.time() + if now - float(_border_cache.get("ts", 0.0)) < 10.0 and _border_cache.get("half") is not None: + return float(_border_cache["half"]) + + try: + raw = rcon("worldborder get", config["rcon_host"], config["rcon_port"], config["rcon_password"]) + nums = re.findall(r'(-?[\d.]+)', raw or "") + if not nums: + return None + width = float(nums[0]) + half = max(0.0, width / 2.0) + _border_cache["ts"] = now + _border_cache["half"] = half + return half + except Exception: + return None + + +_OTHER_DIMENSION_RE = re.compile( + r'\bexecute\s+in\s+minecraft:(the_nether|the_end|nether|end)\b', re.IGNORECASE +) + + +def _tp_inside_worldborder(cmd: str, config) -> bool: + if not _tp_border_guard_enabled(config): + return True + + # Nether/End dimension teleports use different coordinate spaces — skip border check. + if _OTHER_DIMENSION_RE.search(cmd): + return True + + xyz = _extract_tp_abs_xyz(cmd) + if not xyz: + # Relative/dynamic TP can't be statically checked here. + return True + + half = _worldborder_half_extent(config) + if half is None: + return True + + x, _, z = xyz + cx = float(config.get("worldborder_center_x", 0.0)) + cz = float(config.get("worldborder_center_z", 0.0)) + margin = max(0.0, float(config.get("tp_border_margin", 2.0))) + limit = max(0.0, half - margin) + + return abs(x - cx) <= limit and abs(z - cz) <= limit + def fix_give_command(cmd: str) -> str: """ Correct common LLM give command mistakes: @@ -985,7 +1160,7 @@ def fix_effect_command(cmd: str) -> str: return fixed return cmd -def validate_command(cmd, online_players, fallback_player): +def validate_command(cmd, online_players, fallback_player, config=None): """Replace placeholders, auto-fix common give syntax errors, check safe prefix.""" resolved = cmd.replace("{player}", fallback_player).replace("{target}", fallback_player) resolved = fix_give_command(resolved) @@ -993,6 +1168,9 @@ def validate_command(cmd, online_players, fallback_player): if not any(resolved.startswith(p) for p in SAFE_PREFIXES): log.warning(f"Command blocked (unknown prefix): {resolved}") return resolved, False + if config and _extract_tp_abs_xyz(resolved) and not _tp_inside_worldborder(resolved, config): + log.warning(f"Command blocked (tp outside worldborder): {resolved}") + return resolved, False return resolved, True def execute_response(response, context, config, praying_player=None): @@ -1050,7 +1228,7 @@ def execute_response(response, context, config, praying_player=None): ) for cmd in commands[:max_cmds]: - resolved, is_safe = validate_command(cmd, context["online_players"], fallback) + resolved, is_safe = validate_command(cmd, context["online_players"], fallback, config) if not is_safe: continue log.info(f"Executing RCON: {resolved}") @@ -1242,7 +1420,7 @@ def process_sudo(player, prompt, config): executed = [] for cmd in commands: - resolved, is_safe = validate_command(cmd, online, player) + resolved, is_safe = validate_command(cmd, online, player, config) if not is_safe: continue log.info(f"SUDO execute: {resolved}") diff --git a/mc_aigod_shrink.json b/mc_aigod_shrink.json index b40eced..8e8e525 100644 --- a/mc_aigod_shrink.json +++ b/mc_aigod_shrink.json @@ -17,6 +17,10 @@ "sudo_enabled": true, "sudo_user": "slingshooter08", "sudo_max_commands": 3, + "tp_border_guard_enabled": true, + "worldborder_center_x": 0, + "worldborder_center_z": 0, + "tp_border_margin": 2, "first_login_benevolence_enabled": true, "first_login_benevolence_max_commands": 10, "first_login_path": "/opt/mcsmanager/daemon/data/InstanceData/shrinkborder1234567890abcdef12345/aigod_first_login_seen.json",