diff --git a/.env.example b/.env.example index 29092a56..dd6f7141 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,12 @@ # CurseForge API Key (required for automatic modpack download) # Get your API key from: https://console.curseforge.com/?/api-keys CF_API_KEY=$2a$10$... + +# --- Backups (DC-126) --- +# Local placeholder; final destination will be Hetzner Storage Box or B2. +# Bind-mount path on the host where mc-backup writes snapshots. +BACKUP_DEST=./server/backups +# How often to snapshot. itzg/mc-backup parses Go-duration strings. +BACKUP_INTERVAL=24h +# RCON credentials for save flush before snapshot. Defaults match itzg image. +RCON_PASSWORD=daemoncraft-rcon diff --git a/.gitignore b/.gitignore index 8b433b10..ebf6fbfd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Server runtime data server/data/ +server/backups/ server/geyser/*.jar server/geyser/logs/ server/world/ @@ -55,3 +56,9 @@ PROJECT.md EPIC-*.md # docs/ is no longer gitignored — design docs are canonical references # docs/archive/ contains outdated docs kept for historical reference + +# LuckPerms runtime DB (not gitignored by the server/data catch-all since +# LP is installed into data/ which is already ignored — this covers the +# manually tracked copy if someone adds it directly) +server/plugins/luckperms/*.db +server/plugins/luckperms/luckperms-h2* diff --git a/MEMORY.md b/MEMORY.md index 84ca79eb..aeaa8a24 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -557,7 +557,26 @@ NEVER change LLM provider or model configurations without explicit user confirma Done: DC-1 through DC-8, DC-10 through DC-28, DC-68 through DC-76, DC-95 through DC-112, DC-118 through DC-122 Cancelled: DC-78 (Multiverse Pipeline), DC-80 (Lobby Matrix), DC-82 (Showroom), DC-83 (Relocatable blueprints) — discarded in favor of in-world design (2026-04-28) -Backlog: DC-77 (error frequency tracker), DC-79 (blueprint conversion), DC-81 (blueprint compiler), DC-84 (regeneration), DC-85 through DC-91 (in-world blueprint engine), DC-111 (spike: Hermes /voice mode), DC-123 (dashboard/TTS regression after DC-112) +Backlog: DC-77 (error frequency tracker), DC-79 (blueprint conversion), DC-81 (blueprint compiler), DC-84 (regeneration), DC-85 through DC-91 (in-world blueprint engine), DC-111 (spike: Hermes /voice mode), DC-123 (dashboard/TTS regression after DC-112), DC-124 through DC-132 (Server Setup Overhaul epic — see plans/DC-124.md) + +### Epic: DC-124 — Server Setup Overhaul + +**Status: in_planning (2026-05-03)** — branch `overhaul/server-setup`, 5-PR strategy. +Source: Claude Opus 4.7 architectural review, archived in vault at `projects/DaemonCraft/overhaul-plan.md`. +Blocks on: DC-123. + +| Task | Phase | Notes | +|------|-------|-------| +| DC-125 | 0 — stabilize | image SHA pin, rolemaster.yaml model fix, plugin version inventory | +| DC-126 | 1a — hardening | Docker limits, mc-backup sidecar, CoreProtect, LuckPerms | +| DC-127 | 1b — server visual | SkinsRestorer + DecentHolograms + Better Leaves + Clean Glass + TAB | +| DC-128 | 1c — Java client | `daemoncraft.mrpack` (Modrinth App, shaders opt-in) | +| DC-129 | 1d — Bedrock client | `daemoncraft.mcpack` via Geyser/packs/ | +| DC-130 | 2 — docs | SOUL-rolemaster stage-tools cheatsheet | +| DC-131 | safety | whitelist + chat moderation | +| DC-132 | observability | Plan plugin + agent metrics JSONL | + +Deferred per plan: multi-server mesh, Velocity proxy, Terraform, pre-built worlds. ### Epic: DC-105 — Unified Social Routing diff --git a/agents/SOUL-rolemaster.md b/agents/SOUL-rolemaster.md index 1719d3bd..58d86942 100644 --- a/agents/SOUL-rolemaster.md +++ b/agents/SOUL-rolemaster.md @@ -467,6 +467,223 @@ mc_story(action="log_event", event="Transitioned to el_nacimiento: Pixelito appe --- +## Stage Tools — Quick Reference + +Pamplinas has **operator-level world access** via `mc_command`. This section is the cheatsheet for common scene-staging operations. No new tools are needed — everything here uses `mc_command` and the tools already listed above. + +> **DC-127 dependency**: DecentHolograms and SkinsRestorer activate when DC-127 lands. Until then, `/dh` and `/skin` commands will be rejected by the server. + +--- + +### Holograms (DecentHolograms) + +Holograms are floating text labels. Great for location names, story fragments, ambient flavour. + +``` +# Create a hologram at your current position +mc_command(command="/dh create ") + +# Add a line to an existing hologram +mc_command(command="/dh addline ") + +# Edit an existing line (lines are 1-indexed) +mc_command(command="/dh setline ") + +# Teleport a hologram to exact coordinates +mc_command(command="/dh teleport ") + +# Delete a hologram +mc_command(command="/dh delete ") +``` + +**Text formatting** uses `§` colour codes or MiniMessage tags (``, ``, ``). + +**Stage pattern — named location marker:** +``` +mc_command(command="/dh create entrada_templo §6§l✦ El Templo Olvidado §6§l✦") +mc_command(command="/dh addline entrada_templo §7Los dioses no responden aquí.") +mc_command(command="/dh teleport entrada_templo 120 75 340") +``` + +**Cleanup on quest end** — always delete holograms you created: +``` +mc_command(command="/dh delete entrada_templo") +``` +Or log their names with `mc_story(action="log_event", event="Hologram: entrada_templo at 120,75,340")` so you can clean up later. + +--- + +### Skin Changes (SkinsRestorer) + +Change a player's visual appearance for a scene. Useful for disguise mechanics, role assignment, or dramatic reveals. + +``` +# Set a player's skin by Minecraft username (pulls the real Mojang skin) +mc_command(command="/skin set ") + +# Set a skin by URL (custom texture) +mc_command(command="/skin url ") + +# Clear a player's skin (restore their original) +mc_command(command="/skin clear ") +``` + +**Stage pattern — disguise mechanic:** +``` +# Pamplinas gives a player an NPC disguise for the scene +mc_command(command="/skin set Fede Notch") +mc_chat(action="chat_to", player="Fede", message="You wear the face of the Builder tonight. Do not let them recognise you.") + +# On scene end, restore +mc_command(command="/skin clear Fede") +``` + +--- + +### Time and Weather + +``` +mc_command(command="/time set day") # bright, safe feeling +mc_command(command="/time set noon") # high sun, clear shadows +mc_command(command="/time set night") # darkness, tension +mc_command(command="/time set midnight") # deepest dark +mc_command(command="/time set 13000") # just-turned-night (exact ticks) + +mc_command(command="/weather clear") +mc_command(command="/weather rain") +mc_command(command="/weather thunder") +mc_command(command="/weather clear 99999") # lock clear for ~5 game days +``` + +**Scene transitions — combine time and weather:** +``` +# Ritual begins +mc_command(command="/time set midnight") +mc_command(command="/weather thunder") +mc_command(command="/effect give @a minecraft:darkness 10 1 true") + +# Dawn after resolution +mc_command(command="/time set 23000") +mc_command(command="/weather clear") +mc_command(command="/effect give @a minecraft:regeneration 30 0 true") +``` + +--- + +### Titles and Subtitles + +Titles appear as large on-screen text — the closest thing to a cinematic cut. Use them for phase transitions, reveals, and dramatic moments. + +``` +# Full title + subtitle combo +mc_command(command="/title @a title {\"text\":\"Capítulo II\",\"color\":\"dark_red\",\"bold\":true}") +mc_command(command="/title @a subtitle {\"text\":\"El Despertar\",\"color\":\"gray\",\"italic\":true}") + +# Timing: fadein ticks, stay ticks, fadeout ticks (all in game ticks, 20/sec) +mc_command(command="/title @a times 20 80 30") + +# Clear immediately +mc_command(command="/title @a clear") + +# Action bar (smaller, bottom of screen, less intrusive) +mc_command(command="/title @a actionbar {\"text\":\"⚠ Algo se acerca...\",\"color\":\"yellow\"}") +``` + +--- + +### Sounds + +``` +# Ambient sound at a player's position +mc_command(command="/playsound minecraft:ambient.cave ambient @a ~ ~ ~ 0.8 1.0") + +# Jump-scare or trigger +mc_command(command="/playsound minecraft:entity.warden.heartbeat master @a ~ ~ ~ 1.0 0.8") + +# Music disc style (looping ambient) +mc_command(command="/playsound minecraft:music_disc.13 record @a ~ ~ ~ 2.0 1.0") + +# Stop all sounds +mc_command(command="/stopsound @a") +``` + +**Sound categories**: `master`, `music`, `record`, `weather`, `block`, `hostile`, `neutral`, `player`, `ambient`, `voice`. Use `ambient` for environmental; `master` for dramatic stings. + +**Useful sounds for rolemaster:** +| Sound | Use | +|---|---| +| `minecraft:ambient.cave` | Mystery, unease | +| `minecraft:entity.warden.heartbeat` | Dread, approaching threat | +| `minecraft:block.bell.use` | Announcement, scene start | +| `minecraft:ui.toast.challenge_complete` | Victory sting | +| `minecraft:entity.elder_guardian.curse` | Boss reveal | +| `minecraft:music_disc.13` | Unsettling ambient | +| `minecraft:music_disc.11` | Horror ambient | +| `minecraft:block.note_block.harp` + varying pitch | Custom melodies | + +--- + +### Particles + +Particles add atmosphere without spawning entities. They are local and temporary. + +``` +# Particle burst at specific coords +mc_command(command="/particle minecraft:flame 120 75 340 0.5 0.5 0.5 0.05 50") +# ^x ^y ^z ^dx^dy^dz ^speed ^count + +# On a player (uses ~ ~ ~ for relative) +mc_command(command="/execute at Fede run particle minecraft:witch ~ ~1 ~ 0.3 0.5 0.3 0.05 20") + +# Floating dust (custom colour, needs hex via dust particle) +mc_command(command="/particle minecraft:dust{color:[1.0,0.0,0.0],scale:1.5} 120 75 340 0.3 0.3 0.3 0 30") +``` + +**Common stage particles:** +| Particle | Effect | +|---|---| +| `minecraft:flame` | Fire, ritual | +| `minecraft:soul_fire_flame` | Supernatural fire | +| `minecraft:enchant` | Magic, spellcasting | +| `minecraft:end_rod` | Magical shimmer | +| `minecraft:portal` | Dimensional energy | +| `minecraft:witch` | Curse, potion effect | +| `minecraft:explosion` | Impact, destruction | +| `minecraft:cloud` | Smoke, obscurement | + +--- + +### Targeting players + +``` +@a — all players +@a[r=30] — all players within 30 blocks of command origin +@a[name=Fede] — specific player by name +@p — nearest player +``` + +**Good habit — use `/execute as ... at @s` to run relative to a player:** +``` +# Spawn flame particles above a specific player wherever they are +mc_command(command="/execute as Fede at @s run particle minecraft:flame ~ ~2 ~ 0.3 0.3 0.3 0.05 20") +``` + +--- + +### Anti-patterns (do NOT do these) + +| Anti-pattern | Why | Instead | +|---|---|---| +| `/op ` | Grants full server control | Use `lp user parent add pamplina-team` | +| `/stop` | Kills the server | Never. If you need a restart, alert the human admin. | +| `/whitelist remove ` | Bans a kid mid-session | Alert the human admin. | +| `/fill air` | Can destroy player builds permanently | Always verify with `mc_perceive(type="scene")` first; fill only regions you placed | +| Creating holograms without logging them | They become untrackable ghosts | Always `mc_story(action="log_event", ...)` with the hologram name and coordinates | +| Changing a player's skin without restoring it | Player is stuck in a costume after the scene | Always `mc_story(action="log_event", event="Skin changed: Fede -> Notch")` and restore in cleanup phase | +| Playing sounds on a loop without a stop | Permanent audio | Always pair with a cleanup `stopsound @a` in the scene's resolution phase | + +--- + ## Memory You MUST remember across sessions: diff --git a/agents/agent_loop.py b/agents/agent_loop.py index 61c844b3..ddf5c50c 100644 --- a/agents/agent_loop.py +++ b/agents/agent_loop.py @@ -28,6 +28,48 @@ MC_API_URL = os.getenv("MC_API_URL", "http://localhost:3001") BOT_USERNAME = os.getenv("MC_USERNAME", "Steve").lower() +# DC-132 metrics — append-only JSONL per cast per UTC day. The gateway +# adapter writes turn/tool events; this loop writes heartbeats. Schema +# is documented in scripts/agent-metrics-report.py. +_METRICS_DIR_DEFAULT = Path.home() / ".hermes" / "metrics" +METRICS_CAST = os.getenv("MC_METRICS_CAST", "") # set by daemoncraft.py launcher +METRICS_DIR = Path(os.getenv("MC_METRICS_DIR", str(_METRICS_DIR_DEFAULT))) + + +def _emit_metric(kind: str, **fields) -> None: + """Append a JSON line to ~/.hermes/metrics//.jsonl. Best-effort. + + Uses a single os.write() with O_APPEND so writes shorter than PIPE_BUF + (typically 4 KB on Linux) are POSIX-atomic — even with concurrent writers + or a process kill mid-write, you can't get a half-written line. The + report script tolerates truncated lines anyway, but this prevents them + in the first place. + """ + if not METRICS_CAST: + return + try: + import datetime as _dt + now = _dt.datetime.utcnow() + cast_dir = METRICS_DIR / METRICS_CAST + cast_dir.mkdir(parents=True, exist_ok=True) + path = cast_dir / f"{now.date().isoformat()}.jsonl" + record = { + "ts": now.isoformat(timespec="seconds") + "Z", + "cast": METRICS_CAST, + "agent": BOT_USERNAME.capitalize(), + "kind": kind, + **fields, + } + line = (json.dumps(record, separators=(",", ":")) + "\n").encode("utf-8") + fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644) + try: + os.write(fd, line) + finally: + os.close(fd) + except Exception: + # Metrics must never break the heartbeat loop. + pass + # ═════════════════════════════════════════════════════════════════════════════════════════════════════════ # HTTP helpers @@ -587,6 +629,7 @@ def run_agent_loop(profile_name: str, initial_prompt: str, interval: int = 30): ok = send_heartbeat_context(status, nearby, inventory, plan, events) if ok: print(f"[loop] Heartbeat sent (status={bool(status)}, nearby={bool(nearby)}, plan={bool(plan)})", flush=True) + _emit_metric("heartbeat", triggered=bool(triggered)) else: print("[loop] Heartbeat send failed", flush=True) diff --git a/agents/bot/package.json b/agents/bot/package.json index 14105cba..8e4aa398 100644 --- a/agents/bot/package.json +++ b/agents/bot/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "canvas": "^3.2.3", - "mine-photo": "github:MakkusuOtaku/mine-photo", "minecraft-data": "^3.69.0", "mineflayer": "^4.23.0", "mineflayer-armor-manager": "^2.0.1", diff --git a/agents/bot/server.js b/agents/bot/server.js index 5ba30efe..f88cec22 100644 --- a/agents/bot/server.js +++ b/agents/bot/server.js @@ -84,7 +84,11 @@ import { recipeDiagnostics, recipeIngredientCounts, } from './lib/action_feedback.js'; -import { Camera } from 'mine-photo'; +// mine-photo is dead code — prismarine-viewer + puppeteer replaced it (see line 253). +// The package is broken on Node 22 (fs.globSync at module load) so we stub Camera here. +class Camera { + constructor() { throw new Error('mine-photo Camera disabled — use prismarine-viewer screenshot path'); } +} import { mineflayer as mineflayerViewer } from 'prismarine-viewer'; import puppeteer from 'puppeteer'; diff --git a/agents/casts/rolemaster.yaml b/agents/casts/rolemaster.yaml index d79bd64d..d82dcbfc 100644 --- a/agents/casts/rolemaster.yaml +++ b/agents/casts/rolemaster.yaml @@ -20,9 +20,9 @@ agents: - name: Pamplinas template: rolemaster/pamplinas port: 3002 - model: kimi-k2.6 - provider: kimi-coding - base_url: https://api.kimi.com/coding/v1 + model: MiniMax-M2.7 + provider: minimax + base_url: https://api.minimax.io/anthropic extra_toolsets: - vision gamemode: creative diff --git a/agents/daemoncraft.py b/agents/daemoncraft.py index ebfb039a..3f9b7d97 100755 --- a/agents/daemoncraft.py +++ b/agents/daemoncraft.py @@ -431,6 +431,8 @@ def start_agent( "MC_KNOWN_BOTS": _get_all_known_bots(), # Enable send_message tool by telling Hermes we're on a messaging platform. "HERMES_SESSION_PLATFORM": "telegram", + # DC-132 — activates the JSONL metrics emitter in agent_loop.py. + "MC_METRICS_CAST": cast_name, } if max_chat_chars: env["MC_MAX_CHAT_CHARS"] = str(max_chat_chars) diff --git a/docker-compose.yml b/docker-compose.yml index c1ad2907..27fe3612 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,10 @@ version: "3.8" services: minecraft: - image: itzg/minecraft-server:latest + # Pinned to a digest so restarts are deterministic. Bump deliberately + # after a smoke test; do NOT switch back to :latest. See + # docs/server-overhaul.md for the upgrade procedure (DC-125). + image: itzg/minecraft-server@sha256:629762aaf864e109a35e00b11d701cfc6b2bddca4331944aefde0a352ebb9fd4 container_name: daemoncraft-minecraft restart: unless-stopped network_mode: host @@ -21,12 +24,21 @@ services: # --- Gameplay / Dev Settings --- ONLINE_MODE: "false" - DIFFICULTY: "peaceful" + # easy permits hostile mob spawns (rolemaster narrative tension) while + # keeping kid-friendly damage scaling. Per-cast overrides via in-game + # /difficulty are still allowed (Pamplinas can switch to peaceful for + # specific scenes via mc_command). + DIFFICULTY: "easy" CREATE_CONSOLE_IN_PIPE: "true" ALLOW_FLIGHT: "true" ENABLE_COMMAND_BLOCK: "true" SPAWN_PROTECTION: "0" MAX_PLAYERS: "20" + # Identity gating (DC-131). With ONLINE_MODE=false anyone who knows a + # username can connect AS that name; the whitelist is the floor. + # Manage with `whitelist add ` / `whitelist remove ` via rcon + # or by editing server/data/whitelist.json directly. + ENFORCE_WHITELIST: "true" VIEW_DISTANCE: "10" SIMULATION_DISTANCE: "10" GAME_MODE: "survival" @@ -45,6 +57,19 @@ services: # MODPACK: "/modpacks/phi-craft.zip" # MODPACK_PLATFORM: "" + # --- Plugins (auto-installed by itzg from Modrinth, DC-126) --- + # Pinned version IDs so restarts are deterministic. Bumps land in + # follow-up PRs after smoke-test. See docs/server-overhaul.md. + # NOTE: when feat/bedrock-geyser-support merges upstream, this list + # gains "geyser:R7DKgZlt" — combine the two lists at merge time. + MODRINTH_PROJECTS: "luckperms:OrIs0S6b,coreprotect:HD2IvrxS,skinsrestorer:2PjHGlwd,decentholograms:t9gURTWO,tab-was-taken:FewsxQmS,chatfilter-zepsizola:UlCRrLx8,plan:egk2fxRL" + MODRINTH_ALLOWED_VERSION_TYPE: "release" + + # RCON — enabled by default in Purpur; password is pinned here so the + # mc-backup sidecar can reliably authenticate. Change in .env only. + RCON_PASSWORD: ${RCON_PASSWORD:-daemoncraft-rcon} + ENABLE_RCON: "true" + # --- Logging --- TZ: "UTC" volumes: @@ -55,12 +80,49 @@ services: - ./server/modpacks:/modpacks:ro tty: true stdin_open: true + # Bound resource use so a runaway plugin or hot loop cannot OOM the host + # (which would also take down redis, bridge, lan-broadcast). Tune to host + # capacity; current target is a 16 GB / 8-core box with headroom. + mem_limit: 14g + cpus: 6.0 healthcheck: test: mc-health start_period: 1m interval: 5s retries: 20 + # --------------------------------------------------------------------------- + # Backup Sidecar (DC-126) — periodic world snapshots with RCON save flush + # --------------------------------------------------------------------------- + # Destination is a placeholder bind-mount for now. Final destination will + # move to Hetzner Storage Box or Backblaze B2 in a follow-up (decision in + # plans/DC-126.md "Open Questions"). The sidecar pattern stays the same; + # only the destination volume mapping changes. + mc-backup: + image: itzg/mc-backup@sha256:7ffba80d2c6752df8d1669451de928f9e7b2d94866cd84951af6e7bc5bed1496 + container_name: daemoncraft-mc-backup + restart: unless-stopped + network_mode: host + environment: + BACKUP_INTERVAL: ${BACKUP_INTERVAL:-24h} + INITIAL_DELAY: "5m" + PRUNE_BACKUPS_DAYS: "30" + RCON_HOST: localhost + RCON_PORT: "25575" + RCON_PASSWORD: ${RCON_PASSWORD:-minecraft} + SRC_DIR: /data + DEST_DIR: /backups + BACKUP_NAME: daemoncraft + TZ: UTC + volumes: + # Read the world from the live server. Read-only would be ideal but + # mc-backup needs to call save-off / save-on via RCON, which is fine. + - ./server/data:/data:ro + # Local placeholder destination; override BACKUP_DEST in .env. + - ${BACKUP_DEST:-./server/backups}:/backups + depends_on: + - minecraft + # --------------------------------------------------------------------------- # LAN Discovery Broadcaster — makes dedicated server visible in LAN list # --------------------------------------------------------------------------- diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 00000000..96b4f190 --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,72 @@ +# Privacy & Data Handling — DaemonCraft + +Light-touch policy for the DaemonCraft server. The audience is small and +includes minors, so we keep the data footprint narrow and the deletion path +short. Companion to `plans/DC-131.md`. + +--- + +## What we log + +| Source | What | Where | Retention | +|---|---|---|---| +| Minecraft server | All player chat, joins, leaves, world edits | `server/data/logs/.log.gz` (rotated daily by Purpur) | 30 days | +| CoreProtect | Block place/break, container interactions, command use | `server/data/plugins/CoreProtect/database.db` | 30 days (`co purge t:30d`) | +| Backups | World snapshots (which include chat/log fragments) | `${BACKUP_DEST}` (see `.env`) | 30 days (`PRUNE_BACKUPS_DAYS`) | +| Agent traces | Each AI agent turn (prompt → response → tool calls) | `~/.hermes/profiles//sessions/*.json` | Manual purge — no automatic rotation yet | +| Bot Mind dashboard | In-memory ring buffer of last 50 agent turns | RAM only; cleared on bot restart | Until restart | + +Player UUIDs and usernames are stored in `server/data/usercache.json` and +`server/data/whitelist.json`. UUIDs are stable; usernames may change. + +## What we do NOT log + +- Voice / audio. TTS output is generated, played, and the temp file deleted. +- IP addresses beyond the server's own log line at connect time (Purpur default + log format includes IP — we don't separately persist or aggregate it). +- Anything outside Minecraft (no profiling, no telemetry to third parties). + +## Identity & access + +- `ONLINE_MODE: false` — the server does not authenticate against Mojang. +- `ENFORCE_WHITELIST: true` — connection requires an explicit whitelist entry. +- LuckPerms gates commands by group (`default` / `pamplina-team` / `op`). +- Anyone with `op` can read all logs and run any command. Keep that group small. + +## Parent / data-subject deletion request + +To remove a player's data: + +``` +# Remove from whitelist (prevents reconnection) +docker exec -u 1000 daemoncraft-minecraft mc-send-to-console "whitelist remove " + +# Roll back their builds (optional — also removes evidence of incidents) +docker exec -u 1000 daemoncraft-minecraft mc-send-to-console "co rollback u: t:30d" + +# Drop their LuckPerms record +docker exec -u 1000 daemoncraft-minecraft mc-send-to-console "lp user clear" + +# Drop their entry from usercache and playerdata +# (next restart picks this up; do this after the player is offline) +jq 'map(select(.name != ""))' server/data/usercache.json > /tmp/uc.json && mv /tmp/uc.json server/data/usercache.json +rm server/data/world/playerdata/.dat 2>/dev/null +``` + +For a complete wipe, also remove the player's chat lines from rotated log +files (`server/data/logs/*.log.gz`) — `zgrep -v ""` then re-gzip. + +## Incident review + +If something needs to be reviewed (griefing, inappropriate chat): + +1. Chat: `zgrep "" server/data/logs/*.log.gz | head` +2. Block actions: `co lookup u: t:7d` +3. Agent context (if an agent was involved): inspect the relevant session JSON + under `~/.hermes/profiles//sessions/`. + +## Changes to this policy + +This file is the source of truth. If retention or scope changes, update here +first, then notify all `op` group members. Material changes (new categories, +shorter retention, third-party export) require admin sign-off. diff --git a/docs/server-overhaul.md b/docs/server-overhaul.md new file mode 100644 index 00000000..c8894e5a --- /dev/null +++ b/docs/server-overhaul.md @@ -0,0 +1,149 @@ +# Server Overhaul — Operator Runbook + +Operational reference for the DaemonCraft server overhaul (epic DC-124). Companion to `plans/DC-124.md` and the verbatim plan in `~/REPOS/vault/projects/DaemonCraft/overhaul-plan.md`. + +This file is updated as each phase lands. Treat it as the single source of truth for "what versions are pinned, how do I upgrade them, how do I restore from backup". + +--- + +## Pinned versions + +### Server image (DC-125) + +``` +itzg/minecraft-server@sha256:629762aaf864e109a35e00b11d701cfc6b2bddca4331944aefde0a352ebb9fd4 +``` + +Captured 2026-05-03 from `docker image inspect itzg/minecraft-server:latest --format '{{index .RepoDigests 0}}'` on a known-good running container. + +**Upgrade procedure:** + +1. On a scratch host (or laptop), pull the new tag and start a clean container with the same env. Smoke test: server boots healthy, Geyser loads, agent bot connects. +2. Capture the new digest: `docker image inspect itzg/minecraft-server:latest --format '{{index .RepoDigests 0}}'` +3. Open a PR that updates only the digest line in `docker-compose.yml`. PR description: what version changed, what was smoke-tested. +4. After merge, `docker compose pull && docker compose up -d` on production. Watch logs for 5 min. +5. **Never switch back to `:latest`** — that defeats the pin and makes incidents un-bisectable. + +### Plugins (auto-installed via itzg) + +| Plugin | Source | Version pin | Owner ticket | +|---|---|---|---| +| Geyser-Spigot | Modrinth `geyser` | `2.9.5-b1130` (`R7DKgZlt`) — pin once `feat/bedrock-geyser-support` merges upstream | DC-125 follow-up | + +The Geyser pin lands as a follow-up because the `MODRINTH_PROJECTS: "geyser"` line is on the `feat/bedrock-geyser-support` branch (PR #2 to nicoechaniz). Once that merges, a small follow-up PR replaces `"geyser"` with `"geyser:R7DKgZlt"`. + +### Plugins (manually installed) + +Currently in `server/data/plugins/` (not auto-installed; survive the gitignored `server/data/` boundary because the plugin jars and configs are managed manually): + +| Plugin | Loaded version | Source | +|---|---|---| +| Denizen | _capture from running server, see below_ | manual | +| spark | _capture from running server, see below_ | manual | + +To capture loaded versions: +``` +docker exec daemoncraft-minecraft mc-send-to-console "version Denizen" +docker exec daemoncraft-minecraft mc-send-to-console "version spark" +``` + +(These versions land in this table as part of DC-126 when LuckPerms / CoreProtect / SkinsRestorer / DecentHolograms / TAB are added — they all get one inventory pass.) + +--- + +## Cast model configuration (DC-125) + +All casts use **MiniMax-M2.7** via `provider: minimax` and `base_url: https://api.minimax.io/anthropic`. The `agents/casts/rolemaster.yaml` file was previously misconfigured with `kimi-k2.6` / `kimi-coding`; corrected in DC-125 to match the runtime. + +If you add a new cast, copy the model/provider/base_url block from `companion.yaml` as the canonical reference. + +--- + +## Difficulty (DC-125) + +Server-wide default: **easy**. Permits hostile mob spawns (needed for rolemaster narrative tension) while keeping kid-friendly damage scaling. + +Pamplinas (rolemaster cast) can issue `/difficulty peaceful` for specific scenes via `mc_command`; the server-wide default reverts on next restart. + +**Gotcha**: existing worlds store difficulty in `level.dat` (NBT) which overrides `server.properties`. Changing the env var alone does nothing for an already-generated world. Apply once via console: +``` +docker exec daemoncraft-minecraft rcon-cli difficulty easy +``` +This persists into `level.dat`. New worlds pick up the env value at generation. + +--- + +## Backup + restore (DC-126) + +### Sidecar: `itzg/mc-backup` + +Pinned digest: `itzg/mc-backup@sha256:7ffba80d2c6752df8d1669451de928f9e7b2d94866cd84951af6e7bc5bed1496` + +Schedule: daily (`BACKUP_INTERVAL=24h`, 5-minute initial delay). +Destination: `${BACKUP_DEST}` from `.env` (default: `./server/backups/` local placeholder). + +**Open item — final destination**: move `BACKUP_DEST` to Hetzner Storage Box or Backblaze B2 once the project decides; only the volume mapping in `.env` and `docker-compose.yml` changes, sidecar logic stays. + +**How it works**: on each scheduled run, mc-backup connects via RCON (`RCON_PASSWORD` from `.env`) and issues `save-off → save-all flush → snapshot → save-on`. The snapshot is a `.tar.gz` of `/data/`. + +**Test a restore**: +```bash +mkdir /tmp/restore-test +cd /tmp/restore-test +tar -xzf ~/REPOS/daemoncraft/server/backups/daemoncraft-YYYYMMDD-HHMMSS.tar.gz +# Mount into a scratch itzg container and verify world loads +docker run --rm -v /tmp/restore-test:/data \ + -e EULA=true -e TYPE=PURPUR -e VERSION=1.21.11 \ + itzg/minecraft-server@sha256:629762... +``` + +**Retention**: 30 days (controlled by `PRUNE_BACKUPS_DAYS=30`). + +--- + +## CoreProtect (DC-126) + +Installed: v23.1 (Modrinth `HD2IvrxS`). Note: 1.21.11 is not yet listed in Modrinth's game-version metadata but 23.1 loads and enables cleanly on Purpur 1.21.11 (verified 2026-05-03). Backend: SQLite. + +**Test rollback**: +```bash +# In-game, deliberately place/break a test block as a non-admin user +docker exec daemoncraft-minecraft rcon-cli "co rollback u: t:1h r:10" +``` + +**Alert on bulk griefing**: CoreProtect logs everything; for active monitoring wire a log watcher to `server/data/plugins/CoreProtect/` (future DC-132 scope). + +--- + +## LuckPerms group definitions (DC-126) + +Installed: v5.5.17-bukkit (Modrinth `OrIs0S6b`). Storage: H2. + +Group hierarchy and `groups.json` import procedure: see `server/plugins/luckperms/README.md`. + +| Group | Scope | +|---|---| +| `default` | All players — `/help`, `/msg`, `/me`, `/reply` | +| `pamplina-team` | Narrative operator — time, weather, gamemode, tp, give, effect, difficulty, say, title, summon, kill | +| `op` | Full wildcard (`*`) — human admin only | + +**Emptying `op.json`**: once LuckPerms is the authority, `op.json` should be empty or contain only the admin UUID. Run: +```bash +docker exec daemoncraft-minecraft rcon-cli "deop " +``` +then add them to the `op` group: +```bash +docker exec daemoncraft-minecraft rcon-cli "lp user parent add op" +``` + +--- + +## Whitelist / invite-code procedure (DC-131 — pending) + +Onboarding runbook lands here. + +--- + +## Daily metrics report (DC-132 — pending) + +Aggregation script and read-the-output guide land here. diff --git a/plans/DC-124.md b/plans/DC-124.md new file mode 100644 index 00000000..4c957230 --- /dev/null +++ b/plans/DC-124.md @@ -0,0 +1,85 @@ +# DC-124: Server Setup Overhaul (EPIC) + +**Status:** in_planning +**Priority:** high +**Type:** epic +**Blocks on:** DC-123 (dashboard/TTS regression must close before visual work) + +## Vision + +Evolve DaemonCraft from a developer-grade dev server into a professional, beautiful, performant setup for kids playing alongside AI agents (Pamplinas, rolemaster cast). Server-side visual upgrades, client-side auto-setup (Modrinth `.mrpack` + Bedrock `.mcpack`), agent integration, and operational hardening. + +## Source + +Architectural review by Claude Opus 4.7 (max effort), pasted into the working session on 2026-05-03. Verbatim plan archived at: +- `~/REPOS/vault/projects/DaemonCraft/overhaul-plan.md` + +## Critical issues identified by the review + +1. **Server-side resource pack limits** — Fresh Animations needs client-side ETF/EMF or OptiFine; pushed server-side it gives stock models to vanilla clients. Belongs in the client modpack only. Connected-texture Clear Glass needs Continuity client-side; a vanilla-compatible Clean Glass is the right swap. +2. **Bedrock visual exclusion** — Geyser does NOT convert Java RPs to Bedrock. Tablets see stock textures unless we author a separate `.mcpack` placed under `Geyser/packs/`. Accept parity gap; prioritize cross-play. +3. **Operational regressions** — `itzg/minecraft-server:latest` is unpinned (silent forward-rolls can break Geyser). `DIFFICULTY: peaceful` contradicts rolemaster narrative goals. `agents/casts/rolemaster.yaml:23` still declares `model: kimi-k2.6` while runtime uses MiniMax-M2.7. DC-123 (dashboard/TTS regression) is open and blocks visual sequencing. +4. **Phase 2 from original proposal is mostly trivial** — `mc_command` already gives Pamplinas operator-level access; reduces to a docs cheatsheet in `SOUL-rolemaster.md`. + +## Component decisions (revised vs original proposal) + +| Component | Decision | Rationale | +|---|---|---| +| DecentHolograms | Keep | Best-in-class for 1.21.x, scriptable via commands | +| SkinsRestorer 15.12.0 | Keep | Fetches real Mojang skins by username in offline mode | +| TAB | Use TAB alone (drop NameTagEdit) | Modern TAB has built-in nametag features | +| Server-side Fresh Animations | DROP from Phase 1 | Broken on vanilla clients; ship via `.mrpack` only | +| Better Leaves | Keep | Cross-shape leaves work server-side | +| Clean Glass (vanilla-compatible) | Replace original Clear Glass (CTM) | Avoids Continuity dependency | +| Client launcher (Java) | Modrinth App + `.mrpack` | Two-click install, server IP pinned | +| CI/CD | Watchtower + signed image tags for container; GHA only for plugins/datapacks | Live MC servers don't redeploy like webapps | +| Monitoring | Spark (already loaded) + Plan plugin | Spark = JVM/CPU; Plan = player analytics | +| Multi-server mesh | DEFER ENTIRELY | Single Purpur 12 GB handles current load | + +## Work Breakdown + +| Ticket | Title | Status | Blocks on | +|--------|-------|--------|-----------| +| DC-125 | Phase 0 Stabilize: pin server image SHA, fix `rolemaster.yaml` model field, version-pin all plugins, reconsider DIFFICULTY | backlog | — | +| DC-126 | Phase 1a Hardening: Docker mem/cpu limits, mc-backup sidecar, CoreProtect, LuckPerms | backlog | DC-125 | +| DC-127 | Phase 1b Visual: SkinsRestorer + DecentHolograms + Better Leaves + Clean Glass + TAB | backlog | DC-125, DC-123 | +| DC-128 | Phase 1c Java client `.mrpack` (Modrinth App, server IP pinned, shaders opt-in) | backlog | DC-127 | +| DC-129 | Phase 1d Bedrock `.mcpack` (basic textures via `Geyser/packs/`) | backlog | DC-127 | +| DC-130 | Phase 2 SOUL-rolemaster stage-tools cheatsheet (docs only) | backlog | — | +| DC-131 | Safety: whitelist or invite codes + chat moderation (kids on offline mode) | backlog | DC-126 | +| DC-132 | Observability: agent metrics (tokens/day, tool-call dist, latency, heartbeat rate) + Plan plugin | backlog | DC-126 | + +Deferred per plan: multi-server mesh, Velocity proxy, Terraform IaC, pre-built world purchases. + +## Branch + PR strategy + +Tracking branch: `overhaul/server-setup` (off `main`). Five stacked PRs, each independently revertable: + +1. **PR-A** — DC-125 stabilization (small, lands fast, unblocks everything) +2. **PR-B** — DC-126 hardening (backups + limits + CoreProtect + LuckPerms) +3. **PR-C** — DC-127 visual (plugin set + RP) +4. **PR-D** — DC-128 + DC-129 client packs (`.mrpack` + `.mcpack`) +5. **PR-E** — DC-131 safety + DC-132 observability + +## Acceptance Criteria + +1. Server image is pinned to a SHA digest; restarts are deterministic. +2. Docker has explicit `mem_limit` and `cpus`; container cannot OOM the host. +3. Backups run on a schedule and a restore has been tested end-to-end at least once. +4. CoreProtect rollback works for a deliberately introduced griefing event. +5. LuckPerms groups defined: `default`, `pamplina-team`, `op`. No raw `op.json` bypasses. +6. Java player on Modrinth App imports `daemoncraft.mrpack` and is in-game in ≤2 clicks. +7. Bedrock player on tablet sees the custom `.mcpack` apply on join. +8. Whitelist or invite-code mechanism in place; offline-mode spoofing mitigated. +9. Daily aggregate metrics for at least one Pamplina cast: tokens/day, tool-call distribution, average turn latency. + +## Files (forward-looking) + +- `docker-compose.yml` (image pin, resource limits, mc-backup sidecar) +- `agents/casts/rolemaster.yaml` (model field) +- `server/data/plugins/CoreProtect/`, `server/data/plugins/LuckPerms/` +- `server/data/plugins/SkinsRestorer/`, `server/data/plugins/DecentHolograms/`, `server/data/plugins/TAB/` +- `client/daemoncraft.mrpack` (new) +- `server/geyser/packs/daemoncraft.mcpack` (new) +- `agents/SOUL-rolemaster.md` (stage-tools cheatsheet) +- `docs/server-overhaul.md` (operator runbook) diff --git a/plans/DC-125.md b/plans/DC-125.md new file mode 100644 index 00000000..95e1f122 --- /dev/null +++ b/plans/DC-125.md @@ -0,0 +1,30 @@ +# DC-125: Phase 0 — Stabilize + +**Status:** backlog +**Priority:** high +**Type:** task +**Parent:** DC-124 + +## Goal + +Lock down the moving pieces so subsequent overhaul phases land on a deterministic base. No new features. + +## Tasks + +1. **Pin server image** — replace `image: itzg/minecraft-server:latest` with `image: itzg/minecraft-server@sha256:`. Capture the digest of the currently-running known-good image. Document the upgrade procedure (manual digest bump after smoke test). +2. **Fix `agents/casts/rolemaster.yaml:23`** — `model: kimi-k2.6` → the correct MiniMax-M2.7 identifier (verify against runtime config). Cross-check `provider`, `base_url`. +3. **Pin Geyser version** — DEFERRED to a follow-up PR. The `MODRINTH_PROJECTS: "geyser"` line lives on `feat/bedrock-geyser-support` (upstream PR #2), not on `main`. After that PR merges, a small follow-up replaces `"geyser"` with `"geyser:R7DKgZlt"` (Geyser-Spigot 2.9.5-b1130, captured 2026-05-03). Tracked in `docs/server-overhaul.md` "Pinned versions" table. +4. **Reconsider DIFFICULTY** — DONE: set to `easy` (permits hostile mob spawns for narrative tension, kid-friendly damage scaling). Pamplinas can override per-scene via `/difficulty peaceful` through `mc_command`; server-wide default reverts on restart. +5. **Plugin version inventory** — for every plugin currently in `server/data/plugins/`, record the JAR version in `docs/server-overhaul.md`. Future updates require a deliberate bump. + +## Acceptance + +- `docker compose pull && docker compose up -d` reproduces the exact running image. +- Restart 3× in a row: server comes up healthy each time, no plugin auto-updates. +- `agents/casts/rolemaster.yaml` matches runtime model config (verified by inspecting an actual cast launch). + +## Files + +- `docker-compose.yml` +- `agents/casts/rolemaster.yaml` +- `docs/server-overhaul.md` (new — operator runbook + pinned versions table) diff --git a/plans/DC-126.md b/plans/DC-126.md new file mode 100644 index 00000000..ef8fa88d --- /dev/null +++ b/plans/DC-126.md @@ -0,0 +1,49 @@ +# DC-126: Phase 1a — Hardening + +**Status:** in_progress +**Priority:** high +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-125 + +## Goal + +Bring the server to operational table-stakes: bounded resource use, tested backups, anti-grief, and granular permissions. + +## Tasks + +1. **Docker resource limits** — add `mem_limit: 14g`, `cpus: 6.0` (host has headroom for redis/bridge/lan-broadcast) to the `minecraft` service. Validate with a load test (10 simulated bots). +2. **Backup sidecar** — add `itzg/mc-backup` as a sibling service: + - Daily backup at 04:00 UTC (low-traffic window). + - Destination: Hetzner Storage Box (preferred) or Backblaze B2. + - Retention: 7 daily, 4 weekly, 3 monthly. + - Use `RCON_HOST: localhost` + `RCON_PORT: 25575` (host net) to flush before snapshot. +3. **CoreProtect** — install plugin, configure: + - Default rollback window: 30 days. + - DB backend: SQLite (single-server scale). + - Alert on bulk-break events (>20 blocks/min by single player). +4. **LuckPerms** — install plugin, define groups: + - `default` — basic gameplay, no commands beyond `/spawn`, `/help`. + - `pamplina-team` — agent operator scope (mirrors current `mc_command` capability). + - `op` — full server ops (only the human admin). + - Migrate any current `op.json` entries into LuckPerms `op` group; empty `op.json`. + +## Acceptance + +- `docker stats daemoncraft-minecraft` shows mem capped at 14 GB. +- Backup runs nightly; restore from backup tested at least once into a scratch dir. +- CoreProtect: deliberately griefed test region rolls back via `/co rollback`. +- LuckPerms: a `default` user cannot issue `/op`, `/give`, `/gamemode`; a `pamplina-team` user can issue commands Pamplinas needs (verified by listing `mc_command` usage in agent logs). + +## Files + +- `docker-compose.yml` (mem_limit, cpus, mc-backup service) +- `server/data/plugins/CoreProtect/` (config snapshot, no jar in git) +- `server/data/plugins/LuckPerms/` (groups.yml, no jar in git) +- `docs/server-overhaul.md` (backup runbook, restore procedure) +- `.gitignore` (plugin jars, backup destination secrets) + +## Open Questions + +- Backup destination: Hetzner Storage Box vs B2 — what does the project already pay for? +- `pamplina-team` group: enumerate the exact command set used by `mc_command` in current agent traces before defining permissions. diff --git a/plans/DC-127.md b/plans/DC-127.md new file mode 100644 index 00000000..e9284131 --- /dev/null +++ b/plans/DC-127.md @@ -0,0 +1,58 @@ +# DC-127: Phase 1b — Visual Upgrade (Server-Side) + +**Status:** in_progress +**Priority:** medium +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-125, DC-123 + +## Goal + +Server-side visual polish that works on **vanilla** Java clients (no client mods required). Bedrock and modded-client visual parity is handled in DC-128 / DC-129. + +## Tasks + +1. **SkinsRestorer** (15.12.0) — install via itzg `MODRINTH_PROJECTS`. Configure: + - Mojang skin lookup by username (offline-mode-compatible). + - Cache dir under `server/data/plugins/SkinsRestorer/`. +2. **DecentHolograms** — install. Author starter holograms: + - Spawn welcome banner. + - Pamplinas stage labels (referenced by DC-130 cheatsheet). +3. **Better Leaves resource pack** — server-pushed RP. Pure RP, no client mods needed. +4. **Clean Glass resource pack** — vanilla-compatible (NOT the CTM Continuity-dependent one). Replaces the original "Clear Glass" item from the proposal. +5. **TAB plugin** — install. Configure: + - Header/footer with server MOTD and player count. + - Nametag colorization by LuckPerms group (`pamplina-team` = light_purple, `op` = red, `default` = white). + - **Drop NameTagEdit** — TAB owns nametags now. +6. **Resource pack delivery** — combine Better Leaves + Clean Glass + UI tweaks into a single `daemoncraft-server.zip`, host on a stable URL (e.g., `inference01.altermundi.net/rp/daemoncraft-server.zip`), set `resource-pack` + `resource-pack-sha1` in `server.properties`. + +## Explicitly NOT in scope + +- Fresh Animations (broken on vanilla — moved to DC-128 client mrpack). +- Connected-textures Clear Glass via Continuity (moved to DC-128). +- Iris/Complementary shaders (opt-in via DC-128, off by default). + +## Acceptance + +- Vanilla Java client connecting to `localhost:25565` sees: real Mojang skins, stage holograms, leafy trees, clean glass, color-coded nametags. +- DC-123 dashboard/TTS regression remains closed (no regressions introduced). + +## Progress + +- **2026-05-03** Plugins installed via Modrinth pins: `skinsrestorer:2PjHGlwd` (15.12.0), `decentholograms:t9gURTWO` (2.9.10), `tab-was-taken:FewsxQmS` (6.0.2). All five plugins (these + LuckPerms + CoreProtect from DC-126) load cleanly on Purpur 1.21.11. +- **2026-05-03** LuckPerms group prefixes set (default `&f`, pamplina-team `&d[★]`, op `&c[OP]`). TAB picks them up via `%luckperms-prefix%` placeholder — no TAB groups.yml override needed. Persisted to `server/plugins/luckperms/groups.json`. +- **2026-05-03** Welcome hologram authored (`server/data/plugins/DecentHolograms/holograms/welcome.yml`). Uses canonical `world:x:y:z` location format and the `pages → lines → content` structure that DH 2.9.x writes itself; hand-written shorthand triggers a NPE in `loadHolograms`. +- **2026-05-03** TAB header/footer simplified to DaemonCraft branding (`config.yml`). + +## Open + +- Resource pack delivery (Better Leaves + Clean Glass + UI tweaks bundled into `daemoncraft-server.zip`) — needs a stable hosting URL. Defer to follow-up; rest of DC-127 lands without it. + +## Files + +- `docker-compose.yml` (extend `MODRINTH_PROJECTS`) +- `server/data/plugins/SkinsRestorer/config.yml` +- `server/data/plugins/DecentHolograms/holograms.yml` +- `server/data/plugins/TAB/config.yml` +- `server/data/server.properties` (resource-pack URL + SHA1) +- `client/server-rp/` (build script + source assets for `daemoncraft-server.zip`) diff --git a/plans/DC-128.md b/plans/DC-128.md new file mode 100644 index 00000000..9a71ca81 --- /dev/null +++ b/plans/DC-128.md @@ -0,0 +1,56 @@ +# DC-128: Phase 1c — Java Client `.mrpack` + +**Status:** backlog +**Priority:** medium +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-127 + +## Goal + +Two-click client install for parents: download Modrinth App → import `daemoncraft.mrpack` → "Play" connects to server IP. Carries Fresh Animations + ETF/EMF + Continuity etc that cannot be delivered server-side. + +## Tasks + +1. **Bootstrap pack repo layout** — `client/mrpack/` with `modrinth.index.json`, `overrides/`. +2. **Required mods** (Fabric, version-pinned): + - Performance: Sodium, Lithium, FerriteCore, ImmediatelyFast + - Visuals: Entity Texture Features (ETF), Entity Model Features (EMF), Continuity + - QoL: Fabric API, Mod Menu +3. **Resource packs (auto-enabled)**: + - Fresh Animations + - Better Leaves (matches server) + - Clean Glass (matches server) + - Custom UI pack (project branding) +4. **Toggleable (off by default)**: + - Iris + Complementary Reimagined shaders. Document the toggle in pack README. +5. **Vanity (low overhead, on by default)**: + - LambDynamicLights, Falling Leaves, Particular +6. **Server entry** — embed server IP/port in `servers.dat` override. +7. **Build pipeline** — script (`scripts/build-mrpack.sh`) that assembles the `.mrpack`, validates with `mrpack-install` or equivalent, outputs to `dist/daemoncraft-.mrpack`. +8. **Distribution** — host on Modrinth (public) or self-host (project URL). Modrinth is preferred for auto-updates. + +## Policy + +**Do NOT ship shaders enabled by default.** School laptops will choke at 12 fps. Document the toggle conspicuously in the pack README. + +## Acceptance + +- A fresh Modrinth App install on a Windows/macOS/Linux machine imports the pack and launches into the server in ≤2 minutes (modulo download time). +- Default frame rate on a 2022 mid-range laptop ≥60 fps (no shaders). +- All resource packs auto-enable; player sees Fresh Animations + Better Leaves + Clean Glass + UI pack on join. +- Toggling shaders on works without re-importing the pack. + +## Files + +- `client/mrpack/modrinth.index.json` +- `client/mrpack/overrides/config/` +- `client/mrpack/overrides/servers.dat` +- `client/mrpack/README.md` +- `scripts/build-mrpack.sh` +- `dist/daemoncraft-.mrpack` (gitignored — built artifact) + +## Open Questions + +- Distribution: publish on Modrinth (requires public project + maintainer account) or self-host on AlterMundi infra? +- Shader pack license: Complementary Reimagined is BTSL — confirm distribution rights. diff --git a/plans/DC-129.md b/plans/DC-129.md new file mode 100644 index 00000000..21690850 --- /dev/null +++ b/plans/DC-129.md @@ -0,0 +1,46 @@ +# DC-129: Phase 1d — Bedrock `.mcpack` + +**Status:** backlog +**Priority:** medium +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-127 + +## Goal + +Minimum-viable visual layer for Bedrock (tablet/console) players via Geyser pack delivery. Accept parity gap with Java; tablet kids prioritize playing with friends over pixel-perfect parity. + +## Background + +Geyser does NOT auto-convert Java resource packs to Bedrock format. We must author a separate `.mcpack` (Bedrock pack format) and place it in `Geyser/packs/` for automatic delivery to Bedrock clients on join. + +## Tasks + +1. **Author baseline `.mcpack`**: + - Bedrock-compatible block textures matching Better Leaves visuals where the format permits. + - Glass texture remap to mirror Clean Glass. + - Project branding (loading screen, MOTD overlay). +2. **Place under `server/geyser/packs/daemoncraft.mcpack`** — Geyser auto-serves to connecting Bedrock clients. +3. **Verify on real device** — at minimum: Android tablet + Windows 10 Bedrock, since those are the most common kid devices. + +## Explicitly out of scope + +- Fresh-Animations equivalent on Bedrock (no good cross-engine port). +- Shaders on Bedrock (different shader language; defer indefinitely). + +## Acceptance + +- Bedrock client connecting to `localhost:19132` receives the `.mcpack` automatically and applies it without user intervention. +- Visuals visibly differentiated from stock Bedrock (leafy trees, branded loading). +- No connection failures or pack-rejection prompts on the two test devices. + +## Files + +- `client/mcpack/` (source assets and `manifest.json`) +- `scripts/build-mcpack.sh` (zip + rename to `.mcpack`) +- `server/geyser/packs/daemoncraft.mcpack` (built artifact, gitignored or LFS) + +## Open Questions + +- Bedrock pack format version: confirm against Geyser's tested compatibility matrix. +- Distribution to non-LAN Bedrock players: Geyser delivers on join, but pack size matters — keep ≤10 MB to avoid join timeouts on slow connections. diff --git a/plans/DC-130.md b/plans/DC-130.md new file mode 100644 index 00000000..6254b6c4 --- /dev/null +++ b/plans/DC-130.md @@ -0,0 +1,31 @@ +# DC-130: Phase 2 — SOUL-rolemaster Stage Tools Cheatsheet + +**Status:** done +**Priority:** low +**Type:** docs +**Parent:** DC-124 + +## Goal + +The original Phase 2 proposal called for "stage tool primitives" for Pamplinas. Inspection shows `mc_command` already gives operator-level access to holograms, weather, time, and skin commands. So Phase 2 reduces to **documentation**. + +## Tasks + +1. **Add a "Stage Tools" section to `agents/SOUL-rolemaster.md`** — cheatsheet of common stage commands the rolemaster cast can invoke via `mc_command`: + - `/dh create ` (DecentHolograms — DC-127) + - `/dh edit ...` + - `/time set day|night` + - `/weather clear|rain|thunder` + - `/skin set ` (SkinsRestorer — DC-127) + - Common particle and sound effects. +2. **Add command-syntax guidance** — when to use `/execute as ...`, how to target nearby players, how to chain effects. +3. **Add anti-pattern warnings** — commands Pamplinas should NOT issue (anything destructive, anything that affects out-of-scene players). + +## Acceptance + +- A new rolemaster cast session can execute a "scene change" (time + weather + hologram update) using only the cheatsheet as reference. +- Pamplinas does not issue destructive commands across 10 test scenes (verified via agent log review). + +## Files + +- `agents/SOUL-rolemaster.md` (new "Stage Tools" section) diff --git a/plans/DC-131.md b/plans/DC-131.md new file mode 100644 index 00000000..6ef1bebd --- /dev/null +++ b/plans/DC-131.md @@ -0,0 +1,61 @@ +# DC-131: Safety — Whitelist/Invite Codes + Chat Moderation + +**Status:** in_progress +**Priority:** high +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-126 + +## Goal + +Mitigate the safety risks inherent to an offline-mode public-VPN-reachable server frequented by minors. + +## Background + +`ONLINE_MODE: false` + reachable via VPN means anyone who knows a username can connect AS that username. With kids on the server, this is unacceptable for a non-trivial deployment. + +## Tasks + +1. **Identity gating** — choose one of: + - **Whitelist** — Java has built-in `whitelist.json`; LuckPerms manages additions. Simplest. Geyser supports whitelist via Floodgate-prefixed names; without Floodgate, document the workaround (manual entry of expected Bedrock usernames). + - **Invite codes** — plugin-driven (e.g., AdvancedJoinKick or custom Denizen script). One-shot codes a parent uses on first join, then converts to whitelist entry. + + Recommendation: start with whitelist (zero new code), revisit invite codes if onboarding friction shows up. +2. **Chat moderation** — install a profanity-filter plugin (e.g., AdvancedAntiSwear, ChatGuard). Configure: + - Filter ES + EN word list (project audience). + - Soft-block (replace) for first offense, hard-block (cancel) for repeats. +3. **Chat history retention** — log all chat to `server/data/logs/chat-YYYY-MM.log` with rotation. Useful for incident review and parent inquiry. +4. **GDPR/COPPA posture (light)** — document data-handling policy in `docs/privacy.md`: + - What is logged (chat, joins, agent traces). + - Retention period. + - Parent-request deletion procedure. + +## Acceptance + +- Connecting as an un-whitelisted user is rejected with a clear message. +- Profanity test message is filtered or blocked. +- Chat log file is created and rotated. +- Privacy doc exists and is linked from project README. + +## Files + +- `server/data/whitelist.json` (managed by LuckPerms or admin) +- `server/data/plugins//config.yml` +- `docs/privacy.md` (new) + +## Progress + +- **2026-05-03** Whitelist enabled via `ENFORCE_WHITELIST: "true"` in compose. Pamplinas seeded; verified by watching a non-whitelisted bot get rejected with the standard message ("You are not whitelisted on this server!"). Add/remove via `whitelist add|remove` over rcon. +- **2026-05-03** ChatFilter installed via Modrinth pin (`chatfilter-zepsizola:UlCRrLx8`). Loads cleanly on Purpur 1.21.11 even though Modrinth metadata only lists up to 1.21.8 — same forward-compat pattern as CoreProtect in DC-126. +- **2026-05-03** `docs/privacy.md` written: data inventory, retention table, parent-deletion runbook, incident-review commands. + +## Open + +- ChatFilter ES wordlist: ships with EN defaults; we still need to drop a Spanish wordlist into `server/data/plugins/ChatFilter/blacklist.txt` (or whatever path the plugin uses on first run — TODO once the plugin generates its config). +- Chat history rotation: Purpur logs already rotate daily (`server/data/logs/.log.gz`), so the plan task #3 is a no-op. Documented in `docs/privacy.md`. +- README link to `docs/privacy.md` — the project's main README hasn't been touched in this branch; defer the link to whoever owns the README structure. + +## Open Questions + +- Floodgate decision (DC-124 currently skips it); revisit if Bedrock identity matters. +- DDoS mitigation deferred for now (TCPShield free tier is the eventual answer when public exposure broadens). diff --git a/plans/DC-132.md b/plans/DC-132.md new file mode 100644 index 00000000..f33dec73 --- /dev/null +++ b/plans/DC-132.md @@ -0,0 +1,56 @@ +# DC-132: Observability — Agent Metrics + Plan Plugin + +**Status:** in_progress +**Priority:** medium +**Type:** task +**Parent:** DC-124 +**Blocks on:** DC-126 + +## Goal + +Cover the two visibility gaps the overhaul plan identified: **player analytics** (covered by Plan plugin) and **agent operational metrics** (no current solution). + +## Tasks + +1. **Plan plugin** — install. Configure: + - SQLite backend (single-server scale). + - Web dashboard at `localhost:8804` (or whichever Plan default — confirm). + - Bind to `127.0.0.1` only (don't expose publicly). +2. **Agent metrics collection** — add to `agent_loop.py` and `gateway/`: + - `tokens_in`, `tokens_out` per turn (already in Hermes session DB — surface as daily aggregate). + - Tool-call distribution: count per tool name per cast per day. + - Average turn latency (gateway → final response). + - Heartbeat wake-up rate (per cast, per minute). +3. **Storage** — write to `~/.hermes/metrics//YYYY-MM-DD.jsonl` (append-only, line-delimited JSON). Keep it boring; defer Prometheus until there's a reason. +4. **Daily report** — a small script (`scripts/agent-metrics-report.py`) that aggregates the day's JSONL and prints a one-page summary. Optionally schedulable as a recurring routine. + +## Explicitly out of scope + +- Prometheus / Grafana stack (overkill for current scale). +- Real-time alerting (defer; polling is fine). + +## Acceptance + +- Plan dashboard is reachable on `localhost:8804` and shows player join/leave history. +- One day of agent traffic produces a complete JSONL file with the four metric families above. +- `agent-metrics-report.py` prints a readable daily summary. + +## Progress + +- **2026-05-03** Plan plugin installed via Modrinth pin (`plan:egk2fxRL`, version 5.7+build.3306). Loads cleanly on Purpur 1.21.11; webserver bound to `127.0.0.1:8804` (private). LuckPerms extension auto-registers. GeoLite2 download deliberately not enabled (requires EULA acceptance and an external API call we don't need for single-server scale). +- **2026-05-03** Daily-report script `scripts/agent-metrics-report.py` written. Reads `~/.hermes/metrics//.jsonl`, aggregates four metric families (turns/tokens, tool distribution, heartbeats, failures) and prints a one-page summary. JSONL schema documented in the script docstring. Smoke-tested with a synthetic feed; gracefully reports "no events" when the metrics dir is empty. +- **2026-05-03** Heartbeat emitter wired into `agents/agent_loop.py` (gated on `MC_METRICS_CAST` env var, set by the launcher in `daemoncraft.py`). Best-effort writes to `~/.hermes/metrics//.jsonl`; emitter is wrapped in a bare `except` so a failed write can never break the heartbeat loop. Smoke-tested with a synthetic call. +- **2026-05-03** ⚠️ Plan defaults to `Internal_IP: 0.0.0.0` and exposes the dashboard publicly under host-network mode. Apply procedure for the `127.0.0.1` bind documented at `server/plugins/plan/README.md` — must run after the plugin generates its config on first start. Tracked the snippet rather than the whole 100-line `config.yml` so we don't churn on every Plan version bump. + +## Open + +- **Gateway-side metric emission** (`turn` / `tool` events) belongs in `gateway/platforms/daemoncraft.py` (hermes-agent repo), parallel to the heartbeat emitter here. Schema is shared via `scripts/agent-metrics-report.py`'s docstring. Landing it makes the report fully populated. +- GeoLite2 — defer until there's a reason (city-level player geolocation isn't useful at current scale and adds an EULA + external dependency). + +## Files + +- `docker-compose.yml` (Plan plugin via `MODRINTH_PROJECTS`) +- `agents/agent_loop.py` (metric emission hooks) +- `gateway/platforms/daemoncraft.py` (metric emission hooks) +- `scripts/agent-metrics-report.py` (new) +- `~/.hermes/metrics/` (gitignored runtime data) diff --git a/scripts/agent-metrics-report.py b/scripts/agent-metrics-report.py new file mode 100755 index 00000000..b0d93a23 --- /dev/null +++ b/scripts/agent-metrics-report.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Aggregate one day of agent JSONL metrics into a one-page summary. + +Reads ~/.hermes/metrics//.jsonl (append-only, line-delimited +JSON), aggregates four metric families, and prints a readable report. + +JSONL schema (one event per line): + + { + "ts": "2026-05-03T08:21:00Z", # ISO8601 UTC + "cast": "rolemaster", # agents/casts/.yaml + "agent": "Pamplinas", + "kind": "turn" | "tool" | "heartbeat", + # turn: tokens_in, tokens_out, latency_ms + # tool: tool, ok (bool) + # heartbeat: (no extra fields — count alone) + } + +Emitters: agent_loop.py (heartbeats, latency) and gateway/platforms/daemoncraft.py +(turns + tool calls). Wiring those is tracked as the open item in plans/DC-132.md. + +Usage: + scripts/agent-metrics-report.py # today, all casts + scripts/agent-metrics-report.py 2026-05-02 + scripts/agent-metrics-report.py 2026-05-02 --cast rolemaster +""" +from __future__ import annotations + +import argparse +import datetime as dt +import json +import os +import sys +from collections import Counter, defaultdict +from pathlib import Path + +METRICS_ROOT = Path.home() / ".hermes" / "metrics" + + +def _read_jsonl(path: Path): + if not path.exists(): + return + with path.open() as f: + for line in f: + line = line.strip() + if not line: + continue + try: + yield json.loads(line) + except json.JSONDecodeError: + continue + + +def _aggregate(events): + by_agent_turns = defaultdict(lambda: {"count": 0, "tokens_in": 0, "tokens_out": 0, "latency_ms": []}) + by_agent_tools = defaultdict(Counter) + by_agent_heartbeats = Counter() + tool_failures = Counter() + for e in events: + agent = e.get("agent", "?") + kind = e.get("kind") + if kind == "turn": + t = by_agent_turns[agent] + t["count"] += 1 + t["tokens_in"] += int(e.get("tokens_in", 0) or 0) + t["tokens_out"] += int(e.get("tokens_out", 0) or 0) + lat = e.get("latency_ms") + if isinstance(lat, (int, float)): + t["latency_ms"].append(lat) + elif kind == "tool": + tool = e.get("tool", "?") + by_agent_tools[agent][tool] += 1 + if e.get("ok") is False: + tool_failures[(agent, tool)] += 1 + elif kind == "heartbeat": + by_agent_heartbeats[agent] += 1 + return by_agent_turns, by_agent_tools, by_agent_heartbeats, tool_failures + + +def _fmt_latency(samples): + if not samples: + return "—" + s = sorted(samples) + n = len(s) + p50 = s[n // 2] + p95 = s[min(n - 1, int(n * 0.95))] + return f"p50={p50:.0f}ms p95={p95:.0f}ms (n={n})" + + +def report(date: dt.date, cast_filter: str | None) -> int: + if not METRICS_ROOT.exists(): + print(f"No metrics dir at {METRICS_ROOT} — nothing to report.") + print("Emission hooks (agent_loop.py + gateway/platforms/daemoncraft.py) are pending.") + return 1 + + cast_dirs = [d for d in METRICS_ROOT.iterdir() if d.is_dir()] + if cast_filter: + cast_dirs = [d for d in cast_dirs if d.name == cast_filter] + if not cast_dirs: + print(f"No cast metrics for filter={cast_filter!r} under {METRICS_ROOT}") + return 1 + + print(f"=== Agent metrics — {date.isoformat()} ===\n") + total_files = 0 + for cast_dir in sorted(cast_dirs): + path = cast_dir / f"{date.isoformat()}.jsonl" + events = list(_read_jsonl(path)) + if not events: + print(f"[{cast_dir.name}] no events ({path})\n") + continue + total_files += 1 + turns, tools, hbs, failures = _aggregate(events) + print(f"[{cast_dir.name}] {len(events)} events from {path.name}") + + for agent in sorted(set(list(turns.keys()) + list(tools.keys()) + list(hbs.keys()))): + t = turns.get(agent, {}) + print(f" {agent}:") + if t.get("count"): + print(f" turns: {t['count']} tokens_in={t['tokens_in']:,} tokens_out={t['tokens_out']:,}") + print(f" latency: {_fmt_latency(t['latency_ms'])}") + if hbs.get(agent): + # rate per minute over a 24h day + print(f" heartbeats: {hbs[agent]} (avg {hbs[agent] / 1440:.2f}/min)") + agent_tools = tools.get(agent) + if agent_tools: + top = ", ".join(f"{name}={n}" for name, n in agent_tools.most_common(5)) + print(f" tools: {top}") + agent_failures = {tn: c for (a, tn), c in failures.items() if a == agent} + if agent_failures: + fails = ", ".join(f"{tn}={c}" for tn, c in sorted(agent_failures.items(), key=lambda x: -x[1])) + print(f" failures: {fails}") + print() + return 0 if total_files else 1 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("date", nargs="?", help="YYYY-MM-DD, default today (UTC)") + parser.add_argument("--cast", help="filter to one cast (default: all)") + args = parser.parse_args() + + if args.date: + try: + date = dt.date.fromisoformat(args.date) + except ValueError: + print(f"Bad date: {args.date!r} (want YYYY-MM-DD)", file=sys.stderr) + sys.exit(2) + else: + date = dt.datetime.utcnow().date() + + sys.exit(report(date, args.cast)) + + +if __name__ == "__main__": + main() diff --git a/server/plugins/luckperms/README.md b/server/plugins/luckperms/README.md new file mode 100644 index 00000000..ed8beb41 --- /dev/null +++ b/server/plugins/luckperms/README.md @@ -0,0 +1,37 @@ +# LuckPerms group definitions + +`groups.json` is the authoritative group config exported from the live server via `lp export`. + +## Groups + +| Group | Inherits | Purpose | +|---|---|---| +| `default` | — | All players; basic chat commands only | +| `pamplina-team` | `default` | Narrative operator scope (mirrors `mc_command` capability) | +| `op` | `pamplina-team` | Full server ops; human admin only | + +## Applying to a fresh server + +```bash +# 1. Copy groups.json into the LuckPerms plugin data dir +cp server/plugins/luckperms/groups.json \ + server/data/plugins/LuckPerms/groups-import.json +# 2. Import via rcon +docker exec daemoncraft-minecraft rcon-cli "lp import groups-import.json" +``` + +## Adding a new player to a group + +```bash +docker exec daemoncraft-minecraft rcon-cli "lp user parent add pamplina-team" +``` + +## Updating group definitions + +Make changes in-game or via rcon, then re-export: +```bash +docker exec daemoncraft-minecraft rcon-cli "lp export groups-export.yml" +# decode the gzip+json, overwrite groups.json, commit +python3 -c "import gzip,json; open('server/plugins/luckperms/groups.json','w').write( + json.dumps(json.loads(gzip.open('server/data/plugins/LuckPerms/groups-export.yml.json.gz').read()),indent=2))" +``` diff --git a/server/plugins/luckperms/groups.json b/server/plugins/luckperms/groups.json new file mode 100644 index 00000000..1c2d6e05 --- /dev/null +++ b/server/plugins/luckperms/groups.json @@ -0,0 +1 @@ +{"metadata":{"generatedBy":"Console","generatedAt":"2026-05-03 08:01:55 UTC"},"groups":{"default":{"nodes":[{"type":"permission","key":"minecraft.command.help","value":true},{"type":"permission","key":"minecraft.command.me","value":true},{"type":"permission","key":"minecraft.command.msg","value":true},{"type":"permission","key":"minecraft.command.reply","value":true},{"type":"prefix","key":"prefix.0.&f","value":true}]},"op":{"nodes":[{"type":"permission","key":"'*'","value":true},{"type":"inheritance","key":"group.pamplina-team","value":true},{"type":"prefix","key":"prefix.100.&c[OP]","value":true}]},"pamplina-team":{"nodes":[{"type":"inheritance","key":"group.default","value":true},{"type":"permission","key":"minecraft.command.difficulty","value":true},{"type":"permission","key":"minecraft.command.effect","value":true},{"type":"permission","key":"minecraft.command.gamemode","value":true},{"type":"permission","key":"minecraft.command.give","value":true},{"type":"permission","key":"minecraft.command.kill","value":true},{"type":"permission","key":"minecraft.command.say","value":true},{"type":"permission","key":"minecraft.command.summon","value":true},{"type":"permission","key":"minecraft.command.time","value":true},{"type":"permission","key":"minecraft.command.title","value":true},{"type":"permission","key":"minecraft.command.tp","value":true},{"type":"permission","key":"minecraft.command.weather","value":true},{"type":"prefix","key":"prefix.50.&d[★]","value":true}]}},"tracks":{},"users":{"19dc6d96-3c66-3cad-b88c-3a2fc4dd506f":{"username":"pamplinas","nodes":[{"type":"permission","key":"dh.admin","value":true},{"type":"inheritance","key":"group.default","value":true}]}}} \ No newline at end of file diff --git a/server/plugins/plan/README.md b/server/plugins/plan/README.md new file mode 100644 index 00000000..1745b531 --- /dev/null +++ b/server/plugins/plan/README.md @@ -0,0 +1,47 @@ +# Plan plugin — local-only webserver bind + +Plan (Player Analytics) ships with `Webserver.Internal_IP: 0.0.0.0` by default. +On a host-network container that exposes the dashboard publicly on +`:8804` — including any auth bypass it has at the time. We bind to +`127.0.0.1` so the dashboard is reachable only from the host (or via SSH +tunnel for remote admin). + +## Apply on a fresh server + +The full `config.yml` lives under the gitignored `server/data/` tree. After +the plugin generates its config on first boot, apply the bind override: + +```bash +docker exec daemoncraft-minecraft sed -i \ + 's|Internal_IP: 0.0.0.0|Internal_IP: 127.0.0.1|' \ + /data/plugins/Plan/config.yml +docker exec -u 1000 daemoncraft-minecraft mc-send-to-console "plan reload" +``` + +Verify: + +```bash +# Should respond (302 → /server) +curl -sI http://127.0.0.1:8804/ | head -1 +# Should be unreachable from any other interface +curl -sI --max-time 3 http://:8804/ | head -1 # expect timeout / no route +``` + +## Why not ship the whole config.yml? + +Plan's `config.yml` is ~3 KB of timezone, theme, extension, retention, and +proxy knobs that have nothing to do with security and change between Plan +versions. Tracking the whole file means rebasing it every Plan bump. +Tracking just the one critical setting (this README) keeps the security +contract stable across Plan versions. + +If the Plan team ever changes the default to `127.0.0.1`, this whole step +becomes a no-op — the override still works, and the README documents why +the line is there. + +## Future + +If we ever need more Plan settings tracked (theme branding, server name, +retention), prefer a thin overlay file pattern (small YAML with just the +overrides, applied via a script at boot) over committing the whole +generated config.