ref:main

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.sh computes next version from latest Anvil release tag
  • .anvil-ci.yml pipeline: compute version → build natives → build JARs → test → create Anvil release → tag commit
  • gradle.properties defaults to 0.0.0-dev for 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)
  • @ExpectPlatform abstraction bridges Fabric and Forge implementations
  • All permission checks are server-side. TerminalBlock.use() checks enableOnServers config and HuornPermissions.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 from terminal.rs, wraps alacritty_terminal::tty::Pty
  • DockerBackend: stub (checks /var/run/docker.sock, spawn returns error — full implementation is next phase)
  • BackendRegistry maps backend names to implementations
  • TerminalState now holds Box<dyn TerminalSession> instead of direct tty::Pty
  • JNI native_create accepts backend parameter, looks up in registry
  • Backend selection: respects defaultBackend config + huorn.use.docker permission

Audit Logging

  • AuditLogger: JSONL writer to logs/huorn-audit.log, thread-safe via Mutex<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 matching
  • IdleTracker: 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:

  1. Rust crate + native lib rename
  2. Java packages + class rename
  3. Resources + metadata rename
  4. Documentation cleanup
  5. Config restructure
  6. CI pipeline (CalVer + Anvil CI)
  7. Permission system (@ExpectPlatform)
  8. TerminalManager + admin commands
  9. Sandbox architecture (backend traits)
  10. Audit logging + security
  11. Integration (backend selection + audit command)

Bugs found and fixed during real execution testing

  1. Native library not rebuilt after rename — The .dylib in natives/ still had old JNI class path io/fangorn/alacrittymc/nativelib/NativeTerminal. cargo test and ./gradlew build both pass — only GameTest (real server boot) catches this. Fix: rebuilt native lib.
  2. NativeTerminal class loading on server initnativeInitAudit() call in HuornMod.init() triggered NativeTerminal.<clinit> which calls System.load(). Crashes dedicated servers. Fix: defer audit init to first terminal creation.
  3. fabric-permissions-api upgrades Fabric Loader0.3.1 transitively pulls fabric-loader:0.15.10, which brings a Mixin version that crashes with NoSuchFieldError: 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
NativeImageDynamicTextureOpenGL
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 translation
  • renderer.rs — Pixel buffer renderer, ANSI 256-color palette
  • glyph_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

  1. JNI classloader crash — Fabric’s Knot classloader breaks JNI name-based lookup. Fixed with JNI_OnLoad + RegisterNatives.
  2. 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.
  3. Mirrored text — UV coordinates backwards for north face viewing angle. Fixed: flipped U coordinates.
  4. WASD movement during typing — MC polls key state, not events. Fixed: open transparent TerminalFocusScreen instead of raw focus mode.
  5. 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