DraxGUI advanced code runtimes (Java, JavaScript, Python)
Updated 28 October 2026
The code.call logic node calls a function in a project script written in Java, JavaScript or Python. Scripts run in separate worker processes that talk to the server over a small JSON-lines protocol. Scripts never touch the server: they read a snapshot and request effects, which the server checks and applies itself.
Everything else in DraxGUI (menus, editor, packs, visual logic, expressions) works without any of these runtimes. A missing or disabled runtime is reported as a build diagnostic (runtime_unavailable), never skipped silently.
1. Trust and isolation — read this first
| Mode | When | What it means |
|---|---|---|
disabled |
default; also any language whose executable is none or whose probe failed |
No script process starts. Scripts produce runtime_unavailable diagnostics. |
trusted-admin |
scripts.trust: trusted-admin and the runtime probed successfully |
Scripts are written by editor users who hold the script-write permission. They run as the operating-system account of the Minecraft server, in separate processes. |
A worker process is not a sandbox. Without an OS-level sandbox a script can read and write whatever the server account can, open network connections, and use the CPU and memory limits described below and nothing stricter. DraxGUI does not use the Java Security Manager (permanently disabled since JDK 24), and it does not claim that class loaders, node:vm, Python import hooks, timeouts or thread interruption are a security boundary. Only give script-write permission to people you would give shell access to the server account.
What DraxGUI does enforce in every mode:
- argument-list process start (no shell), a private temporary working directory per worker, and a minimal environment: nothing is inherited from the server (no tokens, credentials,
JAVA_TOOL_OPTIONS,NODE_OPTIONS,PYTHON*). Windows workers get onlySystemRoot,PATH(runtime directory +System32),TEMP,TMP; other systemsPATH=/usr/local/bin:/usr/bin:/bin,HOME/TMPDIR= the private directory,LANG=C.UTF-8; - per-call time limits (the script's
timeoutMs), memory limits where the platform supports them, bounded queues, output size, effect count and log size; - the effect model: nothing a script records is applied unless the call returns normally and each effect is in the script's declared
capabilities(checked in the worker and again on the server); - no package installation and no downloads at runtime.
Defence-in-depth measures that are active are listed in the runtime report under hardening. They reduce accidents; none of them is an isolation boundary:
- JavaScript: each script runs in its own
node:vmcontext with norequire,process,console, timers orimport(), and with string code generation (eval,new Function) and WebAssembly disabled in the context. Node is started with--disallow-code-generation-from-strings,--no-addonsand, on Node ≥ 22.13 (or ≥ 20 as--experimental-permission), the Node permission model (--permission --allow-fs-read=<runner.js>): file writes, file reads other than the runner, child processes, worker threads and native addons are refused by Node. The permission model does not restrict the network, and Node documents that it is not a defence against malicious code. - Python:
python -I -S -B -X utf8(isolated mode, no site-packages, no.pycwrites). Scripts get reduced builtins (open,input,eval,exec,compile,breakpoint,help,exit,quitremoved) and an import allow-list (section 4.2). On Windows the runner puts itself in a Job Object that also forbids child processes. - Java: scripts are compiled against the script API JAR only (they cannot reference the worker's or the server's classes at compile time). At runtime the full JDK is reachable.
OS sandbox. For untrusted authors you need a real OS sandbox (container, bwrap, firejail, a separate low-privilege user, …). Configure it as sandboxPrefix: the command is placed before the runtime command, for example
sandboxPrefix: ["/usr/bin/bwrap", "--ro-bind", "/usr", "/usr", "--ro-bind", "/lib", "/lib", "--ro-bind", "/lib64", "/lib64",
"--bind", "/tmp/draxgui-scripts", "/tmp/draxgui-scripts", "--proc", "/proc", "--dev", "/dev",
"--unshare-all", "--die-with-parent", "--"]
The paths of the runtime, the extracted runner files and the private work directory (workDir) must be visible inside the sandbox. The report then says isolation: os-sandbox(configured: bwrap; not verified by DraxGUI). DraxGUI does not inspect what the sandbox enforces; without a prefix the report says isolation: none.
2. Configuration
The plugin maps its scripts: section to ScriptConfig.fromMap(...). Languages that are not listed stay disabled.
scripts:
trust: trusted-admin # disabled (default) | trusted-admin
languages:
javascript:
executable: auto # auto | none | /absolute/path/to/node
memoryMb: 128 # V8 old-space limit
python:
executable: /usr/bin/python3
memoryMb: 256 # RLIMIT_AS on Linux, Job Object on Windows, not enforced on macOS
allowModules: [] # extra *standard-library* modules scripts may import
java:
executable: auto # auto = the server JVM's own java.home; must be a JDK (javac)
memoryMb: 256 # -Xmx
workers: 1 # processes per language (1..8)
maxInFlight: 2 # concurrent calls per Java worker (JS/Python run one at a time)
# optional bounds (defaults shown)
queueCapacity: 64 # waiting calls per language
maxQueueWaitMs: 2000
syncWaitMs: 0 # 0..20 ms the server thread waits for a result so effects land in the same tick (games: 8)
maxOutputBytes: 262144 # one worker response line
maxRequestBytes: 1048576 # arguments + context of one call
maxEffects: 64
maxLogs: 32
maxLogChars: 500
loadTimeoutMs: 15000 # compile + top-level code per script
startTimeoutMs: 20000
killGraceMs: 500 # after a call's timeout before the worker is killed
restartBackoffMinMs: 250
restartBackoffMaxMs: 30000
workDir: "" # parent of the private temp directory; default: system temp
auto never guesses silently: JavaScript and Python search the server's PATH (node; python3 then python, skipping Windows Store app-execution aliases), Java uses the running server JVM. The resolved path and how it was found (auto: PATH, auto: server JVM (java.home), configured) are part of the runtime report. A runtime counts as available only after its worker actually started, answered hello and met the minimum version: Node.js ≥ 18, CPython ≥ 3.9 (Python 2 is never used), Java ≥ 21 with a system compiler (a JRE is reported as unavailable).
3. Runtime report
For each language the plugin keeps a runtime report, which the editor shows:
| Field | Example (a Windows 11 test machine, 2026-10-21) |
|---|---|
available / mode |
true / trusted-admin |
engine, version |
node 22.14.0; cpython 3.12.10; Java HotSpot(TM) 64-Bit Server VM + javac 21.0.10+8-LTS-217 (also tested with 25.0.2+10-LTS-69) |
executable, discovery |
C:\nvm4w\nodejs\node.exe, auto: PATH |
isolation |
none |
limits |
V8 heap 112 MB (--max-old-space-size=64; ArrayBuffer memory is not covered); per-call V8 timeout plus host watchdog; memory: job object 128 MB; ...; heap 123 MB (-Xmx128m); ... |
hardening |
see section 1 |
reason |
why it is unavailable, e.g. not configured (executable: none), advanced code runtimes are disabled (scripts trust: disabled), the configured Java is older than 21 (UnsupportedClassVersionError); ... |
runnerSha256 |
SHA-256 of runner.js / runner.py / java-worker.jar |
/draxgui status shows, per language, whether the runtime is available and its call, failure, restart and loaded-script counts.
4. Writing scripts
A project script ($defs.script) has id, language, source (≤ 200 000 characters), capabilities (effect types it may request) and timeoutMs (10–10 000, default 500). Scripts are compiled and their top-level code is run when a project generation is loaded; problems are diagnostics with the script id and a 1-based line.
A code.call node passes its args map. The function receives (args, dx); its return value becomes the node's result output. If the function throws, times out or requests an effect it did not declare, the node takes its failed exit and no effect is applied.
4.1 JavaScript (Node.js)
// capabilities: ["var.set", "player.message", "menu.set_text"]
function onClick(args, dx) {
const score = (dx.get('score') ?? 0) + args.points;
dx.setVar('score', score);
dx.menu.setText('score_label', 'Score: ' + dx.text(score));
if (score >= 100) dx.message('Well done, ' + dx.player.name + '!');
return score;
}
Top-level function declarations are listed as callable; const f = () => … is callable by name too. Names starting with _ and built-in global names (escape, Object, …) are not callable. Each script has its own global scope; module-level variables persist between calls only until the worker restarts and are per worker process, so keep state in DraxGUI variables. async functions and returned promises are rejected (async_unsupported). There is no require, import(), process, console, setTimeout; use dx.log.
4.2 Python (CPython 3)
# capabilities: ["var.set", "player.give_item"]
import math
def reward(args, dx):
level = int(dx.get("level") or 1)
dx.give_item("minecraft:emerald", math.ceil(level * 1.5))
dx.add_var("level", 1)
return level + 1
A function may take (args, dx), (args) or (). Imports are limited to standard-library modules on this allow-list: math cmath random statistics decimal fractions numbers itertools functools operator collections heapq bisect array copy string re textwrap unicodedata json datetime time calendar enum dataclasses typing abc hashlib hmac base64 binascii struct zlib uuid, plus the administrator's allowModules. There is no pip and no site-packages. open, input, eval, exec, compile are not available. async def functions and generators are rejected. print goes to the worker's stderr log, not to players.
4.3 Java (JDK + javac)
import com.draxionmc.draxgui.script.api.Dx;
import java.util.Map;
public class Shop {
public Object buy(Map<String, Object> args, Dx dx) {
int amount = ((Number) args.get("amount")).intValue();
dx.takeItem("minecraft:gold_ingot", amount * 4);
dx.giveItem("minecraft:diamond", amount);
return amount;
}
}
The source is one compilation unit. The worker compiles it in memory with the worker JDK's javax.tools compiler (language level = that JDK's feature version, reported as version), against the script API JAR only: the JDK is available, the server, Bukkit and DraxGUI internals are not. The class is the public top-level class (or the single top-level class); it needs a no-argument constructor if it has instance methods. Callable functions are its public methods shaped f(Map<String,Object> args, Dx dx), f(Map<String,Object> args), f(Dx dx) or f(). Numbers arrive as Long (integral) or Double; use Number. Static and instance fields persist only until the worker restarts. System.exit ends the worker (the call fails with worker_crashed; the worker restarts). Compiler warnings are reported as compile_warning diagnostics. No external libraries: there is no dependency mechanism.
4.4 The dx API
Read-only snapshot (taken by the server before the call):
| JavaScript | Python | Java | Value |
|---|---|---|---|
dx.player.name, .uuid |
dx.player.name, .uuid |
dx.player().name(), .uuid() |
viewing player |
dx.project, dx.script |
same | dx.project(), dx.script() |
ids |
dx.event |
dx.event |
dx.event() |
event payload (button, value, …) |
dx.vars, dx.get(id) |
dx.vars, dx.get(id) |
dx.vars(), dx.get(id) |
variables the server exposed to the call |
dx.seed |
dx.seed |
dx.seed() |
reproducible seed or null |
dx.text(v) |
dx.text(v) |
dx.text(v) |
DraxGUI text: numbers as ECMAScript (1, not 1.0), null as "", lists as [a, b] |
dx.log(...) |
dx.log(*parts) |
dx.log(o) |
bounded author log (no capability) |
Effects (recorded in order, applied after the call returns). The capability to declare is the effect type, which is the action node type with the same inputs:
| Effect / capability | JavaScript | Python | Java | Args sent to the server |
|---|---|---|---|---|
var.set |
setVar(id, v), addVar(id, n), toggleVar(id), resetVar(id) |
set_var, add_var, toggle_var, reset_var |
setVar, addVar, toggleVar, resetVar |
variable, op (set/add/toggle/reset), value |
player.message |
message(text) |
message(text) |
message(text) |
text |
player.sound |
sound(id, volume=1, pitch=1) |
same | sound(id[, volume, pitch]) |
sound, volume, pitch |
player.give_item |
giveItem(item, amount=1, name?) |
give_item(...) |
giveItem(...) |
item, amount, name |
player.take_item |
takeItem(item, amount=1) |
take_item(...) |
takeItem(...) |
item, amount |
player.command |
command(cmd, as='player') |
command(cmd, as_='player') |
command(cmd, as) |
command, as |
menu.show_hide |
menu.show(el), menu.hide(el) |
same | menu().show/hide |
element, visible |
menu.set_text |
menu.setText(el, text) |
menu.set_text |
menu().setText |
element, text |
menu.set_value |
menu.setValue(el, v) |
menu.set_value |
menu().setValue |
element, value |
menu.show_popup |
menu.showPopup(p, values?) |
menu.show_popup |
menu().showPopup |
popup, values |
menu.hide_popup |
menu.hidePopup(p) |
menu.hide_popup |
menu().hidePopup |
popup |
menu.open_screen |
menu.openScreen(s) |
menu.open_screen |
menu().openScreen |
screen |
menu.switch_tab |
menu.switchTab(el, tab) |
menu.switch_tab |
menu().switchTab |
element, tab |
menu.close |
menu.close() |
same | menu().close() |
— |
grid.op |
grid(op, variable, args?) |
grid(...) |
grid(...) |
op, variable, args |
| any other action node type in the list below | effect(type, args) |
effect(type_, args) |
effect(type, args) |
that node's inputs |
Requestable effect types (EffectPolicy.SCRIPT_EFFECT_TYPES): var.set, player.message, player.sound, player.give_item, player.take_item, player.command, player.money, player.xp, player.potion, menu.show_hide, menu.set_text, menu.set_value, menu.move_scale, menu.open_screen, menu.switch_tab, menu.player_head, menu.qr_code, menu.close, menu.show_popup, menu.hide_popup, grid.op, web.webhook, web.tebex_checkout. code.call is never an effect (no script recursion); flow, event and value nodes are not effects. A capability outside this list produces an unsupported_capability warning at load and grants nothing.
The server applies each accepted effect through the same validation, permission and policy path as the equivalent graph node (inventory space, item descriptors, console-command policy, webhook egress policy, economy provider, …), on the server thread, after re-checking that the player session and generation are still alive. Text from a script is final text: the plugin must not evaluate ${...} templates in it (a player name inside a message must never become an expression).
4.5 Values and limits
Values crossing the boundary are null, booleans, finite numbers, strings, lists and maps with string keys (maps reach the graph as DraxGUI map values). Integral numbers up to 2^53 travel as JSON integers, so Python and Java scripts receive 1, not 1.0. Limits per value: nesting depth 16, 4096 list items, 1024 map entries, 1 MiB characters per string. Non-finite results (NaN, infinity), functions, class instances, sets, BigInt etc. fail the call with bad_value. Per call: maxEffects effects, maxLogs log lines of maxLogChars, a response line of at most maxOutputBytes, a request of at most maxRequestBytes.
Number text: dx.text and the server use ECMAScript Number#toString (0.30000000000000004, 1e+21, 1.5e-7, 0.000001). Known deviation: the Java worker and DraxGUI core print a few subnormal numbers with two digits (Double.MIN_VALUE is 4.9e-324, ECMAScript 5e-324).
4.6 Time, memory and failures
| Situation | JavaScript | Python | Java |
|---|---|---|---|
call exceeds timeoutMs |
V8 interrupts the call inside the worker (timeout); the worker keeps running |
the server kills the worker after timeoutMs + killGraceMs (timeout) and restarts it |
same as Python |
top-level code exceeds loadTimeoutMs |
interrupted, timeout diagnostic |
worker killed, timeout diagnostic, script not reloaded |
same as Python |
| memory | V8 heap limit (--max-old-space-size); exceeding it aborts Node → memory_limit, restart. ArrayBuffer memory is not covered |
Linux RLIMIT_AS, Windows Job Object → MemoryError → memory_limit, worker survives. macOS/other: not enforced |
-Xmx; OutOfMemoryError → memory_limit, the worker exits and restarts with a clean heap |
| crash / exit | worker_crashed (the in-flight call), automatic restart with back-off; other calls on the same worker get worker_restarted |
same | same |
After a restart the worker reloads every loaded script before taking calls. A script whose own load hangs or crashes the worker is dropped (diagnostic) instead of crash-looping. Restarts back off exponentially (restartBackoffMinMs…restartBackoffMaxMs) only for workers that die during or shortly after start; a timeout kill restarts promptly. Failed calls are never retried automatically.
Call error codes: runtime_error, timeout, memory_limit, capability_denied, too_many_effects, bad_value, bad_argument, output_too_large, function_not_found, script_not_loaded, async_unsupported, runtime_unavailable, queue_full, queue_timeout, worker_crashed, worker_restarted, request_too_large, protocol_error, closed. Load diagnostics: compile_error, compile_warning, load_error, timeout, memory_limit, runtime_unavailable, worker_unavailable, bad_script, unsupported_capability.
4.7 A game loop
A real-time game is a timer plus a script that holds nothing between calls. The Redstone Run template does it like this:
- the screen's
every: [{seconds: 0.05, run: [code.call tick]}]passes the session's state inargs(g, a list of numbers;dots,items,mobmode; the key counter): every call starts from what the server has, and an emptygmeans "first tick: initialise"; - the script simulates 0.05 s in two sub-steps, then requests effects:
menu.move_scaleper sprite (ms50 so a Standard client interpolates to the next tick; Shader-Safe steps),setVarfor the state lists and only for the scalars that changed,soundfor what happened; at most 64 effects per call, so pickups are bound withvisibleWhento a list variable instead of one show/hide effect each, and a mob's five looks (normal, frightened, stunned, eyes, flashing) are five images bound to onemobmodevalue each, all moved together; - keys never go to the script directly: the screen's
keyhandler setswantand adds 1 towant_n, and the tick consumes presses newer than the count stored ing. The script never writes those two variables, so a press that lands while a call is in flight is not overwritten by the call's result; timeoutMsis 300 and the Java worker'smaxInFlight2: a call that overruns one tick is still applied in order; a crash drops that tick only.- with
scripts.syncWaitMs: 8the server thread waits up to 8 ms for the answer, so a 1–3 ms tick applies its moves in the same server tick (one tick less between a key and the draw); a slower answer is applied when it arrives, as without the setting.
5. What was tested
On Windows 11 (2026-10-21), with real worker processes: Node 22.14.0, CPython 3.12.10 and Java 21.0.10 (the Java worker also on
JDK 25.0.2). In game, the Redstone Run and Candy Cascade templates ran with their scripts on Paper 1.21.4 and 26.3. Not tested
yet: Linux and macOS (memory limits, file permissions, a sandbox prefix with a real bwrap or firejail) and long-running load.