Lua scripting architecture (P1Start)

This document is the contract for the Lua stack: thin daemon, isolated workers, explicit IPC, and RegCube for paths and policy. It complements the spawn order in userland-matrix.md and boot-and-init.md.


1. Binaries

Artifact

Role

p1-luasvc.elf

Always-on control plane: publishes lua/svc_pid, serves IPC wire 273, validates policy, execve’s the worker, waitpid’s completion.

lua.elf

Worker: separate address space; argv is lua.elf <script-path> or lua.elf - to read the script from stdin (fd 0). Links cstd so ld.elf can install the p1dl bridge pointer (__p1dl_bridge_dispatch_v1) used by minimal Lua require() → p1dl::dlopen (see SDK/lib/cstd/src/p1dl_bridge.rs). Runs P1 minimal Lua (see below).

luactl.elf

CLI: writes lua/req_script_path, sends IPC to the daemon, blocks for the reply datagram.

The stub luad.elf is removed; init now starts p1-luasvc.elf first in the daemon list.

Naming (matches established services)

Role

Binary

Pattern

Daemon

p1-luasvc.elf

Same as p1-timesvc.elf, p1-dispsvc.elf, …

CLI

luactl.elf

Same class as timectl.elf, dispctl.elf, gamectl.elf, …

Worker

lua.elf

Short executable name; not a p1-*svc because it is ephemeral (spawned per run), not an always-on supervision daemon.

Control-plane model (Option A — chosen)

This stack deliberately implements Option A: p1-luasvc is a thin control plane only (policy, RegCube, IPC 273, execve, waitpid). All language execution (today P1 minimal Lua in Rust, tomorrow PUC-Rio Lua or P1SCode→native if you replace the worker body) stays in lua.elf in a separate address space.

Option B (embed the Lua VM inside p1-luasvc) is not the v1 direction: it trades a short implementation path for worse fault isolation (one interpreter bug can kill the whole scripting service), a larger auditable surface in a long-lived process, and mixed concerns (orchestration + semantics) in one binary. For P1Start’s “explicit, minimal, auditable” style, Option A is the proper default.

RegCube hierarchy

All keys use hive HIVE_LOCAL_MACHINE and a topical prefix lua/ — the same style as time/, display/, game_engine/, pxl/, etc. P1Start does not currently use a separate system/lua/ or service/lua/ tree; renaming only Lua would diverge from the rest of userland. Unless the project adopts a global new RegCube layout, lua/* remains the canonical prefix (see §3).

P1 minimal Lua dialect (worker implementation)

Source of truth: apps/p1-lua/src/minimal.rs.

  • Supported: nil; true / false; local x (same as local x = nil); not and or; integers and comparisons (same rules as before); ..; # on strings or tables (dense numeric prefix 1…n); tables: {} array part, t[k] with k number or string, t.field sugar, nil removes slot, shared Rc semantics; function name(…) … end (global) and local function name(…) … end (Lua-style recursion via a forward slot); closures: inner functions capture outer local / parameter bindings through shared cells (upvalues); f(...) calls built-ins or function values; built-ins type, tonumber, tostring, assert, error, require (one string: dlopen after path normalize — bare m → \lib\m.so, /lib/... → \lib\..., rejects ..; success returns true, failure require failed + stderr line); print, local, assignment (resolution order: inner do / local scopes → upvalue → global / outer), do … end (lexical block — local does not leak; see apps/p1-lua/src/minimal.rs), if / while / repeat / numeric for, break, return (chunk return n = exit code; inside function, return yields a value to the caller); comments / ; / ASCII strings.

  • Not supported (yet): varargs …, metatables, floats, generic for, multiple return values; if / while / repeat / for bodies still flatten local declarations into the enclosing function for static capture analysis (Lua-correct do is the scoped exception), …

  • Sample scripts: … esp/bin/test_lua_table.lua, esp/bin/test_lua_func_table.lua (tables + functions), esp/bin/test_lua_closure.lua (local function + upvalues), esp/bin/test_lua_do_block.lua (do … end scope + closure).

  • Full Lua 5.x: plan vendored PUC-Rio C core (or another proven runtime) cross-compiled for x86_64-unknown-none-elf with a narrowly defined platform layer — not another quick dependency that assumes std.


2. IPC (cstd::lua_runtime)

  • Wire: WIRE_LUA_SVC = 273 (between time 271 and display 272 in numbering; dedicated Lua plane).

  • Verbs (IpcMessage::p1 from client): RUN = 1, PING = 2.

  • Replies (p1 from daemon to sender): OK = 100, DENIED = 101, EXEC_FAILED = 102, BAD_PATH = 103, DISABLED = 104.

  • p2: when p1 == OK, the low 32 bits carry the worker’s i32 exit code (from RegCube lua/last_worker_exit after waitpid). Otherwise p2 = 0.

Clients must not pass arbitrary script pointers in IPC payload; the daemon reads the path from RegCube (cross-address-space safe).


3. RegCube (HIVE_LOCAL_MACHINE)

Key

Purpose

lua/svc_pid

Written by p1-luasvc at start; luactl reads it to target IPC.

lua/req_script_path

luactl run sets this before sending RUN; daemon reads it when handling the request.

lua/enabled

Default 1; when false, RUN returns DISABLED.

lua/allow_path_prefix

Optional; if set, script path must start with this prefix (policy hook).

lua/last_status

Informative short string updated by the daemon (done, exec-failed, etc.).

lua/last_worker_exit

Decimal exit code written by lua.elf before exit (from return n, runtime error, or fall-through 0); daemon copies low bits into IPC p2 on success.

Artifact path (not RegCube): lua-last-run.log under \tmp\. The ramdisk includes tmp/ (see image manifest); lua.elf also calls sys_mkdir("\\tmp") before opening the log so older images without the directory still get a log file. Worker tees stdout here; lua.elf uses sys_dup2 (syscall 33, Linux ABI) to map the log file onto fd 2 after open, so code that writes stderr without Rust helpers still lands in the same session file. err_line avoids double-writing when fd 2 is already the log. luactl run dumps this after RUN returns OK and exits with the worker’s exit code (so luactl is script-friendly when a script fails). If open fails, luactl prints a short notice instead of an empty log.


4. Kernel integration

  • sys_execve with explicit argv returns the child PID to the parent; the parent is not replaced (P1Start behavior). The daemon uses this to spawn lua.elf, then sys_waitpid for completion.

  • SERVICE_READY: daemon sends IPC 100 to PID 1 after publishing RegCube defaults (same pattern as p1-timesvc, deskgui).


5. P1SCode and .so libraries

  • P1SCode and host-side tooling are not required for the daemon/worker protocol. If a future tier compiles Lua or P1SCode to native code, keep outputs as normal ELFs or loaded .so behind the existing dynamic loader; do not tie the compositor or IDE directly into p1-luasvc without updating this document.

  • Shared Lua or policy code belongs in a well-versioned SDK or ramdisk library only when it reduces duplication between worker and host tools; the default worker should remain a small, auditable binary.


6. Operator flow

  1. Boot ensures p1-luasvc.elf is running (init order).

  2. From a terminal: luactl status — RegCube snapshot.

  3. luactl run \bin\test.lua — sets path, runs worker; on success luactl prints worker exit code, then dumps \tmp\lua-last-run.log. A bare name (luactl run test.lua) is rewritten to \\bin\\test.lua because lua.elf inherits p1-luasvc’s cwd, not the shell’s.


7. Source layout

Path

Notes

SDK/lib/cstd/src/lua_runtime.rs

Wire + RegCube key constants.

apps/p1-luasvc

Daemon.

apps/luactl

CLI.

apps/p1-lua

Worker binary lua; src/minimal.rs = P1 minimal Lua.


8. Roadmap (phased)

Aligned with external review: the daemon + worker + RegCube + wire 273 split stays; evolve lua.elf and policy, not the compositor boundary.

  1. Dialect and runtime in lua.elf
    Shipped: auditable P1 minimal Lua for demos and bring-up. Next proper steps: grow the subset in-tree or replace the worker core with vendored PUC-Rio Lua (static link + OS hooks for no_std) once the toolchain story is solid. Piccolo / mlua remain out until they support this target without std. P1SCode → native ELF / .so remains compatible with the same p1-luasvc boundary.

  2. Sandboxing
    Process isolation already limits blast radius; next layers are API surface (few syscalls / stubs), path allowlists (extend lua/allow_path_prefix and friends), optional quotas (memory, script size, CPU), and capability-style exposure if the VM exposes native callbacks.

  3. P1SCode vs Lua
    Decide product stance: Lua as interop/embedding bridge, P1SCode as authoritative app language, or both with clear tiers. Either way, p1-luasvc should keep only orchestration (exec, policy, IPC), not language semantics.

  4. luactl synchronous vs async
    luactl run blocking until the daemon’s waitpid completes is appropriate for a minimal CLI. Later: e.g. RUN_DEFERRED verb, lua/last_job_pid or status keys, and luactl run --detach that returns after ack (optional second reply or RegCube-only completion).

  5. Desktop / IDE
    Shortcuts or “Run script” actions should shell out to luactl or send 273 via a small helper—not embed policy in deskgui beyond launch convenience.

  6. Native stderr from embedded runtimes
    The lua.elf worker maps the session log onto fd 2 via sys_dup2 after open, so write(2, …) from a future Lua VM or C library lands in lua-last-run.log. Other worker binaries still need the same pattern if they want native stderr in-session without going through err_line.