1. PRESET LIFECYCLE (PANEL → DAEMON)
Presets are stored centrally on the panel in <presets_dir>/<id>.json (with an optional companion <id>.lua file loaded automatically when "script": "" is empty). However, all Lua execution happens on the target daemon selected in the UI.
2. JSON SCHEMA & FIELD TYPES
Every string inside base_config supports {{field_id}} placeholder replacement before build(ctx, draft) is invoked. Because interpolation always produces strings, numeric or boolean overrides (like resources.memory_bytes or auto_start) should be assigned inside build.
| field_type | Validation & UI Control |
|---|---|
string, path, password |
Text / password input. Validated as JSON string. |
boolean |
Checkbox input. Passed to Lua ctx as a boolean. |
integer, number |
Numeric input. Validated as integer (i64) or floating-point (f64). |
enum |
Static dropdown validated against the non-empty options: ["a", "b"] array. |
dynamic_enum |
Dynamic dropdown populated on the daemon by calling options_function(ctx). Supports cascading updates via depends_on: ["earlier_field"]. Re-validated on the daemon during installation. |
3. SANDBOXED LUA 5.4 RUNTIME API
Standard libraries os, io, package, debug, require, load, and dofile are removed (nil). Memory is capped at 64 MiB, and cancellation is checked every 10,000 VM instructions.
| Global / Function | Availability | Behavior |
|---|---|---|
system.info() |
All hooks | Returns { os = "linux"|"windows"|..., arch = "x86_64"|"aarch64"|... } of the target daemon. |
json.encode(v) / json.decode(s) |
All hooks | Serializes/deserializes Lua tables and JSON strings. |
rest.request(req) |
All hooks | Performs an HTTP(S) request (url, method, headers, body/json, timeout_seconds 1..300) to an origin listed in preset_http_allowed_origins. Returns { status, headers, body, json } (8 MiB limit). |
rest.download(req) |
install only |
Streams a file to a temp path, verifies max_file_bytes and optional sha256, sets Unix executable bits if requested, and atomically moves to path. Returns { path, bytes, sha256 }. |
workspace.mkdir(p)workspace.write_text(p, s)workspace.read_text(p)workspace.exists(p)workspace.replace(p, from, to, count)workspace.set_executable(p, bool) |
install only |
Safe file operations strictly confined to the new instance's cwd. Rejects .., absolute paths, and symlink escapes. replace verifies exact match count (default 1). |
process.run(req) |
install only |
Runs an installer command (program, args array, cwd, env, stdin, timeout_seconds), streams live stdout/stderr to the installation terminal, and returns { success, code, stdout, stderr, stdout_truncated, stderr_truncated }. |
4. EXAMPLE A: PAPERMC SERVER (CASCADING DYNAMIC_ENUM + SHA-256 + INITIALIZER)
Demonstrates: cascading dynamic_enum fields (build depends on version), querying PaperMC v2 API via rest.request, dynamic RAM allocation in build(ctx, draft), streaming JAR download with SHA-256 verification via rest.download, writing eula.txt and server.properties via workspace, and running a first-time server bootstrap via process.run.
preset_http_allowed_origins = ["https://api.papermc.io"] to daemon.toml.
File 1: presets/paper.json
{
"id": "paper",
"name": {
"en": "Minecraft PaperMC Server",
"uk": "Сервер Minecraft PaperMC"
},
"description": {
"en": "Downloads PaperMC with SHA-256 verification, configures server.properties, and initializes world files.",
"uk": "Завантажує PaperMC із перевіркою SHA-256, налаштовує server.properties та ініціалізує сервер."
},
"enabled": true,
"fields": [
{
"id": "name",
"field_type": "string",
"label": { "en": "Instance name", "uk": "Назва інстанса" },
"required": true,
"default": "survival-1"
},
{
"id": "version",
"field_type": "dynamic_enum",
"label": { "en": "Minecraft version", "uk": "Версія Minecraft" },
"required": true,
"default": "",
"options_function": "paper_versions",
"depends_on": []
},
{
"id": "build",
"field_type": "dynamic_enum",
"label": { "en": "Paper build", "uk": "Білд Paper" },
"required": true,
"default": "",
"options_function": "paper_builds",
"depends_on": ["version"]
},
{
"id": "memory_mb",
"field_type": "integer",
"label": { "en": "Memory limit (MiB)", "uk": "Ліміт пам'яті (МіБ)" },
"required": true,
"default": 4096
},
{
"id": "port",
"field_type": "integer",
"label": { "en": "Server port", "uk": "Порт сервера" },
"required": true,
"default": 25565
},
{
"id": "online_mode",
"field_type": "boolean",
"label": { "en": "Mojang online-mode", "uk": "Ліцензійна перевірка (online-mode)" },
"required": false,
"default": true
}
],
"base_config": {
"resources": {
"cpu_percent": null,
"cpu_affinity": null,
"memory_bytes": null
},
"nickname": "{{name}}",
"start_command": "java -Xms512M -Xmx{{memory_mb}}M -jar paper.jar --nogui",
"stop_command": "stop",
"stop_timeout": 30,
"cwd": "servers/{{name}}",
"ie": "utf-8",
"oe": "utf-8",
"pty": true,
"cols": 120,
"rows": 30,
"auto_start": false,
"auto_restart": true,
"max_restarts": 5,
"tags": ["minecraft", "papermc"],
"process_type": "general",
"env": {}
},
"script": ""
}
File 2: presets/paper.lua
---@class PaperPresetCtx : KeystonePresetContext
---@field name string
---@field version string
---@field build string
---@field memory_mb integer
---@field port integer
---@field online_mode boolean
function paper_versions(ctx)
local res = rest.request({
url = "https://api.papermc.io/v2/projects/paper",
timeout_seconds = 15
})
if res.status ~= 200 or not res.json or not res.json.versions then
error("Failed to fetch PaperMC versions (HTTP " .. tostring(res.status) .. ")")
end
local out = {}
local list = res.json.versions
for i = #list, 1, -1 do
local v = list[i]
table.insert(out, { value = v, label = { en = v, uk = v } })
end
return out
end
function paper_builds(ctx)
if not ctx.version or ctx.version == "" then
return {}
end
local res = rest.request({
url = "https://api.papermc.io/v2/projects/paper/versions/" .. ctx.version,
timeout_seconds = 15
})
if res.status ~= 200 or not res.json or not res.json.builds then
error("Failed to fetch builds for " .. ctx.version)
end
local out = {}
local builds = res.json.builds
for i = #builds, 1, -1 do
local b = tostring(builds[i])
table.insert(out, {
value = b,
label = { en = "Build #" .. b, uk = "Білд #" .. b }
})
end
return out
end
---@param ctx PaperPresetCtx
---@param draft KeystoneInstanceConfig
function build(ctx, draft)
local mem_mb = tonumber(ctx.memory_mb) or 4096
local jvm_heap = math.max(512, mem_mb - 512)
-- Enforce OS-level memory limit + JVM heap headroom
draft.resources.memory_bytes = mem_mb * 1024 * 1024
draft.start_command = string.format(
"java -Xms512M -Xmx%dM -jar paper.jar --nogui",
jvm_heap
)
draft.env.MC_VERSION = ctx.version
draft.env.PAPER_BUILD = tostring(ctx.build)
draft.env.SERVER_PORT = tostring(ctx.port)
return draft
end
---@param ctx PaperPresetCtx
---@param draft KeystoneInstanceConfig
---@param workspace KeystoneWorkspaceApi
---@param process KeystoneProcessApi
function install(ctx, draft, workspace, process)
local meta_url = string.format(
"https://api.papermc.io/v2/projects/paper/versions/%s/builds/%s",
ctx.version,
ctx.build
)
print("Fetching build metadata:", meta_url)
local meta = rest.request({ url = meta_url, timeout_seconds = 20 })
if meta.status ~= 200 or not meta.json then
error("Cannot read Paper build metadata: HTTP " .. tostring(meta.status))
end
local app = meta.json.downloads and meta.json.downloads.application
if not app or not app.name or not app.sha256 then
error("Build metadata is missing application download info")
end
local dl_url = meta_url .. "/downloads/" .. app.name
print("Downloading", app.name, "with SHA-256 verification...")
local dl = rest.download({
url = dl_url,
path = "paper.jar",
sha256 = app.sha256,
timeout_seconds = 0
})
print("Verified", dl.bytes, "bytes, sha256 =", dl.sha256)
workspace.write_text("eula.txt", "eula=true\n")
workspace.write_text(
"server.properties",
string.format(
"server-port=%d\nonline-mode=false\nmotd=K.E.Y.S.T.O.N.E. %s\n",
tonumber(ctx.port) or 25565,
ctx.name
)
)
if ctx.online_mode then
workspace.replace("server.properties", "online-mode=false", "online-mode=true", 1)
end
print("Running initial server bootstrap (sending 'stop' to stdin)...")
local init = process.run({
program = "java",
args = { "-Xms512M", "-Xmx1024M", "-jar", "paper.jar", "--nogui" },
stdin = "stop\n",
timeout_seconds = 900
})
if not init.success then
error("Paper bootstrap failed (exit " .. tostring(init.code) .. "): " .. init.stderr)
end
return draft
end
5. EXAMPLE B: CROSS-PLATFORM BINARY INSTALLER (SYSTEM.INFO + EXECUTABLE BIT)
Demonstrates: inspecting the selected daemon's OS and CPU architecture via system.info(), adapting the binary filename and start_command for Windows vs. Linux/BSD, setting Unix execute permissions with executable = true and workspace.set_executable, and verifying the binary with process.run.
{
"id": "dedicated-bin",
"name": {
"en": "Cross-Platform Dedicated Binary",
"uk": "Кросплатформний виділений сервер"
},
"description": {
"en": "Detects daemon OS/arch, downloads the matching native binary, patches config, and verifies startup.",
"uk": "Визначає ОС та архітектуру демона, завантажує відповідний бінарник, патчить конфіг і перевіряє запуск."
},
"enabled": true,
"fields": [
{
"id": "name",
"field_type": "string",
"label": { "en": "Server name", "uk": "Назва сервера" },
"required": true,
"default": "edge-node"
},
{
"id": "channel",
"field_type": "enum",
"label": { "en": "Release channel", "uk": "Канал релізу" },
"required": true,
"default": "stable",
"options": ["stable", "staging"]
},
{
"id": "bind_port",
"field_type": "integer",
"label": { "en": "Listen port", "uk": "Порт прослуховування" },
"required": true,
"default": 8080
}
],
"base_config": {
"resources": { "cpu_percent": null, "cpu_affinity": null, "memory_bytes": null },
"nickname": "{{name}}",
"start_command": "./bin/server --config config/app.toml",
"stop_command": "^C",
"stop_timeout": 15,
"cwd": "services/{{name}}",
"ie": "utf-8",
"oe": "utf-8",
"pty": false,
"cols": 120,
"rows": 30,
"auto_start": false,
"auto_restart": true,
"max_restarts": -1,
"tags": ["native", "{{channel}}"],
"process_type": "general",
"env": {}
},
"script": ""
}
Companion Script: presets/dedicated-bin.lua
function build(ctx, draft)
local host = system.info()
local exe = host.os == "windows" and "bin\\server.exe" or "./bin/server"
draft.start_command = exe .. " --config config/app.toml"
-- Legacy Windows console encoding support if needed
if host.os == "windows" then
draft.ie = "utf-8"
draft.oe = "utf-8"
end
draft.env.RELEASE_CHANNEL = ctx.channel
draft.env.TARGET_ARCH = host.arch
return draft
end
function install(ctx, draft, workspace, process)
local host = system.info()
print("Target daemon platform:", host.os, host.arch)
workspace.mkdir("bin")
workspace.mkdir("config")
local rel_bin = host.os == "windows" and "bin/server.exe" or "bin/server"
local meta = rest.request({
url = string.format(
"https://releases.example.org/manifest?channel=%s&os=%s&arch=%s",
ctx.channel, host.os, host.arch
),
timeout_seconds = 30
})
if meta.status ~= 200 or not meta.json then
error("Release manifest unavailable for " .. host.os .. "/" .. host.arch)
end
rest.download({
url = meta.json.url,
path = rel_bin,
sha256 = meta.json.sha256,
executable = true,
timeout_seconds = 300
})
workspace.write_text("config/app.toml", string.format(
"name = %q\nlisten = \"0.0.0.0:%d\"\nmode = \"placeholder\"\n",
ctx.name, tonumber(ctx.bind_port) or 8080
))
workspace.replace("config/app.toml", "mode = \"placeholder\"", "mode = " .. string.format("%q", ctx.channel), 1)
workspace.set_executable(rel_bin, true)
local check_prog = host.os == "windows" and "bin\\server.exe" or "./bin/server"
local verify = process.run({
program = check_prog,
args = { "--version" },
timeout_seconds = 30
})
if not verify.success then
error("Binary self-check failed: " .. verify.stderr)
end
print("Installed version:", verify.stdout)
return draft
end
6. EXAMPLE C: SELF-CONTAINED WORKER WITH CPU/RAM QUOTAS (NO EXTERNAL HTTP)
This preset requires zero external network origins and works out-of-the-box on any Linux or Windows daemon. It demonstrates setting native OS resource limits (cpu_percent and memory_bytes) inside build, generating a configuration file with json.encode, writing a cross-platform launcher script in workspace, and running a self-test via process.run.
{
"id": "capped-worker",
"name": {
"en": "Resource-Capped Telemetry Worker",
"uk": "Ізольований сервіс із лімітами ресурсів"
},
"description": {
"en": "Self-contained preset demonstrating CPU/RAM quotas, json.encode config generation, and workspace scripts.",
"uk": "Автономний пресет, що демонструє квоти CPU/RAM, генерацію конфігу через json.encode та скрипти workspace."
},
"enabled": true,
"fields": [
{
"id": "name",
"field_type": "string",
"label": { "en": "Worker name", "uk": "Назва воркера" },
"required": true,
"default": "worker-alpha"
},
{
"id": "cpu_quota",
"field_type": "integer",
"label": { "en": "CPU limit (% where 100 = 1 logical CPU)", "uk": "Ліміт CPU (% де 100 = 1 логічний CPU)" },
"required": true,
"default": 50
},
{
"id": "memory_mib",
"field_type": "integer",
"label": { "en": "Memory limit (MiB)", "uk": "Ліміт пам'яті (МіБ)" },
"required": true,
"default": 256
},
{
"id": "interval_sec",
"field_type": "enum",
"label": { "en": "Heartbeat interval (seconds)", "uk": "Інтервал звіту (секунд)" },
"required": true,
"default": "5",
"options": ["2", "5", "10", "30"]
},
{
"id": "autostart",
"field_type": "boolean",
"label": { "en": "Start immediately after install", "uk": "Запустити одразу після встановлення" },
"required": false,
"default": true
}
],
"base_config": {
"resources": { "cpu_percent": null, "cpu_affinity": null, "memory_bytes": null },
"nickname": "{{name}}",
"start_command": "sh ./run.sh",
"stop_command": "^C",
"stop_timeout": 5,
"cwd": "workers/{{name}}",
"ie": "utf-8",
"oe": "utf-8",
"pty": true,
"cols": 120,
"rows": 30,
"auto_start": false,
"auto_restart": true,
"max_restarts": 3,
"tags": ["worker", "capped"],
"process_type": "general",
"env": {
"HEARTBEAT_SEC": "{{interval_sec}}"
}
},
"script": "function build(ctx, draft)\n local host = system.info()\n draft.resources.cpu_percent = tonumber(ctx.cpu_quota) or 50\n draft.resources.memory_bytes = (tonumber(ctx.memory_mib) or 256) * 1024 * 1024\n draft.auto_start = ctx.autostart == true\n if host.os == 'windows' then\n draft.start_command = 'cmd /C run.cmd'\n else\n draft.start_command = './run.sh'\n end\n return draft\nend\n\nfunction install(ctx, draft, workspace, process)\n local host = system.info()\n workspace.mkdir('config')\n local cfg_json = json.encode({\n worker = ctx.name,\n interval_sec = tonumber(ctx.interval_sec) or 5,\n limits = draft.resources\n })\n workspace.write_text('config/worker.json', cfg_json .. '\\n')\n\n if host.os == 'windows' then\n workspace.write_text('run.cmd', '@echo off\\r\\necho [worker] starting %KEYSTONE_INSTANCE_NAME%\\r\\nping -n 999999 127.0.0.1 >nul\\r\\n')\n local res = process.run({ program = 'cmd', args = { '/C', 'echo', 'Self-test OK' }, timeout_seconds = 10 })\n if not res.success then error(res.stderr) end\n else\n workspace.write_text('run.sh', '#!/bin/sh\\necho \"[worker] node=$KEYSTONE_INSTANCE_NAME uuid=$KEYSTONE_INSTANCE_UUID\"\\nwhile true; do\\n echo \"[heartbeat] $(date -u +%H:%M:%S) interval=$HEARTBEAT_SEC\"\\n sleep \"${HEARTBEAT_SEC:-5}\"\\ndone\\n')\n workspace.set_executable('run.sh', true)\n local res = process.run({ program = 'sh', args = { '-n', 'run.sh' }, timeout_seconds = 10 })\n if not res.success then error('Shell syntax check failed: ' .. res.stderr) end\n end\n print('Worker workspace initialized at', draft.cwd)\n return draft\nend"
}
7. LUALS / EMMYLUA IDE AUTOCOMPLETE
The repository includes keystone_preset_api.lua containing full LuaLS type annotations for KeystoneInstanceConfig, system, json, rest, workspace, and process. Add its directory to Lua.workspace.library in VS Code / Neovim and set runtime to Lua 5.4.
---@class CustomPresetContext : KeystonePresetContext ---@field name string ---@field version string ---@param ctx CustomPresetContext ---@param draft KeystoneInstanceConfig ---@return KeystoneInstanceConfig function build(ctx, draft) draft.nickname = ctx.name draft.env.SELECTED_VERSION = ctx.version return draft end