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.

1. [PANEL] Reads <id>.json + <id>.lua (strips script when listing to browser) 2. [DAEMON] Evaluates dynamic_enum functions (system, json, rest available) 3. [DAEMON] Preview: interpolates {{field}} in base_config ──► calls build(ctx, draft) 4. [DAEMON] Instantiate: creates instance in 'installing' state + opens read-only WS terminal 5. [DAEMON] Re-validates dynamic_enum ──► build(ctx, draft) ──► install(ctx, draft, workspace, process) 6. [DAEMON] Success ──► state = 'ready' (auto_start if enabled) | Error ──► state = 'failed' (Retry button)

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.

DAEMON PREREQUISITE: Add 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