fangorn/huorn-minecraft
public
Huorn Development Journal
2026-03-20 — Phase 2: Distribution, Admin & Security
What was built
Everything needed to make Huorn installable by real users and manageable by server admins. Renamed the project from alacrittymc to huorn/huorn-minecraft, added admin infrastructure, and built the foundation for sandboxed terminal backends.
Rename
Full rename across the codebase. Naming rule: things inside Minecraft (mod ID, package, commands, permissions) use huorn. Things outside Minecraft (native library, Rust crate, repo) use huorn-minecraft. The upstream alacritty_terminal crate dependency name is preserved.
CI/Release Pipeline
- CalVer versioning:
YYYY.MM.BUILD(e.g.,2026.03.1) ci/release.shcomputes next version from latest Anvil release tag.anvil-ci.ymlpipeline: compute version → build natives → build JARs → test → create Anvil release → tag commitgradle.propertiesdefaults to0.0.0-devfor local builds, CI overrides with-Pmod_version=
Permission System
- 6 permission nodes:
huorn.use,huorn.use.docker,huorn.admin.{reload,list,kill,audit} - Fabric:
fabric-permissions-api(LuckPerms-compatible, falls back to op level) - Forge: vanilla op level fallback (full PermissionNode registration deferred)
@ExpectPlatformabstraction bridges Fabric and Forge implementations- All permission checks are server-side.
TerminalBlock.use()checksenableOnServersconfig andHuornPermissions.hasPermission()before allowing interaction.
Config
Restructured from flat fields to nested JSON:
server: enableOnServers, maxTerminalsPerPlayer, maxTerminalsTotal, idleTimeoutMinutes, defaultBackend, defaultOpLevel
backends: plain {enabled, allowedShells}, docker {enabled, image, memoryLimit, cpuLimit, networkEnabled, mountPaths}
security: commandBlocklist, auditLog {enabled, logFile, logCommands, logConnections}
display: fontSize, craftable
Old canUse() permission method removed. Old allowedPlayers/opsAlwaysAllowed fields removed. Config migration: logs warning if old config/alacrittymc.json exists.
Terminal Management
TerminalManager singleton with ConcurrentHashMap<UUID, AtomicInteger> per-player counts + global AtomicInteger. Checks both maxTerminalsPerPlayer and maxTerminalsTotal before allowing terminal creation. Tracks active sessions with metadata (player, backend, location, start time).
Admin Commands
Brigadier command tree registered via Architectury’s CommandRegistrationEvent:
/huorn reload — reload config from disk
/huorn list — show active terminals (player, location, backend, uptime)
/huorn kill <player|all> — force-kill terminals
/huorn status — server-wide stats
/huorn audit [player] — tail last 20 audit log entries, color-coded
Pluggable Sandbox Architecture
Rust trait system for terminal backends:
trait TerminalBackend: Send + Sync {
fn spawn(&self, config: &BackendConfig) -> Result<Box<dyn TerminalSession>>;
fn name(&self) -> &'static str;
fn is_available(&self) -> Result<bool>;
}
trait TerminalSession: Send {
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
fn write(&mut self, data: &[u8]) -> Result<usize>;
fn resize(&mut self, cols: u16, rows: u16) -> Result<()>;
fn kill(&mut self) -> Result<()>;
fn is_alive(&self) -> bool;
}
PlainShellBackend: extracted fromterminal.rs, wrapsalacritty_terminal::tty::PtyDockerBackend: stub (checks/var/run/docker.sock, spawn returns error — full implementation is next phase)BackendRegistrymaps backend names to implementationsTerminalStatenow holdsBox<dyn TerminalSession>instead of directtty::Pty- JNI
native_createacceptsbackendparameter, looks up in registry - Backend selection: respects
defaultBackendconfig +huorn.use.dockerpermission
Audit Logging
AuditLogger: JSONL writer tologs/huorn-audit.log, thread-safe viaMutex<File>- Events: CONNECT, DISCONNECT, COMMAND, TIMEOUT, ADMIN_KILL, BLOCKED, BACKEND_ERROR
- Single writer architecture: all writes go through Rust. Java sends lifecycle events via JNI (
nativeAuditEvent,nativeAuditEventGlobal) InputFilter: line-buffered command blocklist with substring matchingIdleTracker: per-session last-activity timestamp tracking (background reaper thread deferred)
Tests
65 Rust tests pass: 52 unit (rendering, colors, keys, terminal, backends) + 6 audit + 6 security + 1 integration.
Files
12 commits on feature/huorn-distribution-admin:
- Rust crate + native lib rename
- Java packages + class rename
- Resources + metadata rename
- Documentation cleanup
- Config restructure
- CI pipeline (CalVer + Anvil CI)
- Permission system (@ExpectPlatform)
- TerminalManager + admin commands
- Sandbox architecture (backend traits)
- Audit logging + security
- Integration (backend selection + audit command)
Bugs found and fixed during real execution testing
- Native library not rebuilt after rename — The
.dylibinnatives/still had old JNI class pathio/fangorn/alacrittymc/nativelib/NativeTerminal.cargo testand./gradlew buildboth pass — only GameTest (real server boot) catches this. Fix: rebuilt native lib. - NativeTerminal class loading on server init —
nativeInitAudit()call inHuornMod.init()triggeredNativeTerminal.<clinit>which callsSystem.load(). Crashes dedicated servers. Fix: defer audit init to first terminal creation. - fabric-permissions-api upgrades Fabric Loader —
0.3.1transitively pullsfabric-loader:0.15.10, which brings a Mixin version that crashes withNoSuchFieldError: JAVA_22. Fix: exclude transitive loader dep.
Real execution test results
- 65 Rust tests pass (52 unit + 6 audit + 6 security + 1 integration)
- 46 Minecraft GameTests pass — block placement, entity lifecycle, multi-block groups, config defaults (nested structure), NBT serialization, interaction, native JNI bridge (with
"plain"backend parameter), terminal resize - Gradle BUILD SUCCESSFUL for both Fabric and Forge JARs
Deferred
- Docker backend full implementation (hyper/hyperlocal/tokio — separate ticket)
- MicroVM / Apple Hypervisor / Hyper-V backends
- Custom sandbox image on
registry.fangorn.io - Modrinth / CurseForge listing
- Background idle timeout reaper thread
- Full Forge PermissionNode registration
2026-03-19/20 — Phase 1: Full Implementation & Working In-Game Terminal
What was built
A fully functional Alacritty terminal emulator embedded as an interactive block in Minecraft Java Edition 1.20.1, supporting both Fabric and Forge via Architectury.
Architecture
Player right-clicks block
→ TerminalBlockEntity.onPlayerInteract()
→ NativeTerminal (JNI) creates Rust TerminalState
→ PTY spawns shell process (zsh/bash/powershell)
→ VTE parser processes ANSI escape sequences
→ fontdue rasterizes glyphs to pixel buffer
→ JNI copies pixels to Java ByteBuffer
→ NativeImage → DynamicTexture → OpenGL
→ BlockEntityRenderer draws textured quad on block face
→ TerminalFocusScreen captures all keyboard input
→ Input forwarded to PTY via JNI
Files
Rust native library (rust/src/, 4 files):
lib.rs— JNI_OnLoad with RegisterNatives (10 methods)terminal.rs— TerminalState, PTY, VTE, GLFW key translationrenderer.rs— Pixel buffer renderer, ANSI 256-color paletteglyph_cache.rs— fontdue + LRU cache (4096 glyphs)
Java mod (common/src/, 15 files):
- Block, BlockEntity, ScreenGroup
- NativeTerminal (JNI wrapper), NativeLoader (platform extraction)
- TerminalBlockRenderer, TerminalTexture (DynamicTexture)
- TerminalFocusScreen (transparent input capture)
- TerminalScreen (F12 full-screen overlay)
- TerminalInputHandler (GLFW → ANSI), TerminalFocusHandler
- KeyboardHandlerMixin (charTyped interception)
- HuornMod (registry), HuornModClient (events)
- Fabric + Forge entrypoints
Tests (70+ automated):
- 51 Rust unit tests (rendering, colors, keys, terminal lifecycle)
- 11 standalone Java JNI integration tests
- 8 Minecraft GameTests (block placement, interaction, native bridge)
- 1 automated visual test (launches MC, places block, screenshots, analyzes pixels)
Key bugs found and fixed
- JNI classloader crash — Fabric’s Knot classloader breaks JNI name-based lookup. Fixed with
JNI_OnLoad+RegisterNatives. - Block face not rendering — BER quad at z=-0.01 was depth-tested away by block model. Fixed: z=0.001 (inside block face) + removed north face from block model.
- Mirrored text — UV coordinates backwards for north face viewing angle. Fixed: flipped U coordinates.
- WASD movement during typing — MC polls key state, not events. Fixed: open transparent
TerminalFocusScreeninstead of raw focus mode. - NativeLoader poisoning — Constructor loaded native on server side, poisoned singleton. Fixed: deferred to client-only
startTerminal().
How to use
# Build
cd rust && cargo build --release
cp target/release/libhuorn_minecraft.dylib ../common/src/main/resources/natives/macos-aarch64/
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
./gradlew build
# Run
./gradlew :fabric:runClient
# In-game
/give @s huorn:terminal_block
# Place block, right-click to start terminal
# ESC to exit, F12 for full-screen overlay
# Tests
cd rust && cargo test # 51 Rust tests
./gradlew :fabric:runGametest # 8 MC GameTests
./gradlew :fabric:runVisualTest # Automated screenshot test