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.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 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