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:
```rust
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
→ 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 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