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 only SystemRoot, PATH (runtime directory + System32), TEMP, TMP; other systems PATH=/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:vm context with no require, process, console, timers or import(), and with string code generation (eval, new Function) and WebAssembly disabled in the context. Node is started with --disallow-code-generation-from-strings, --no-addons and, 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 .pyc writes). Scripts get reduced builtins (open, input, eval, exec, compile, breakpoint, help, exit, quit removed) 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 in args (g, a list of numbers; dots, items, mobmode; the key counter): every call starts from what the server has, and an empty g means "first tick: initialise";
  • the script simulates 0.05 s in two sub-steps, then requests effects: menu.move_scale per sprite (ms 50 so a Standard client interpolates to the next tick; Shader-Safe steps), setVar for the state lists and only for the scalars that changed, sound for what happened; at most 64 effects per call, so pickups are bound with visibleWhen to 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 one mobmode value each, all moved together;
  • keys never go to the script directly: the screen's key handler sets want and adds 1 to want_n, and the tick consumes presses newer than the count stored in g. 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;
  • timeoutMs is 300 and the Java worker's maxInFlight 2: a call that overruns one tick is still applied in order; a crash drops that tick only.
  • with scripts.syncWaitMs: 8 the 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.