feat: add sandboxed Lua backend via :lua VM#45
Conversation
Adds `Jido.Shell.Backend.Lua`, a pure-Elixir Lua 5.3 backend backed by
the `:lua` hex package (tv-labs/lua / luerl). Mirrors the Bash backend
in structure and callback contract.
Architecture:
- `Lua.Session` holder GenServer owns committed `%Lua{}` state; the
holder never evals — each execute spawns a killable worker so
timeout/cancel cannot pin the holder.
- Commit happens only on clean success; error/timeout/cancel leaves
prior state intact.
- Per-command context (emit, cwd, env, limits) injected via
`Lua.put_private` and stripped before commit to prevent stale
emitter capture across commands.
- `persistent: false` config flag skips the holder and builds a fresh
`Lua.new()` per eval (stateless mode).
`JidoApi` bridge exposes the command registry under `jido.*` via
`use Lua.API, scope: "jido"` + `deflua`; custom `print` global streams
`{:output, …}` to the session. Arg escaping ports `JidoInterop.escape_arg/1`
so spaces, `;`, `&&`, quotes, and backslashes stay single Jido arguments.
Security: `Lua.new/0` sandbox disables `io`, `os.execute`, `os.exit`,
`os.getenv`, `require`, `load`, `loadstring`, `loadfile`, `dofile`,
`package.loadlib` by default. No VfsAdapter needed; file access only
through bridged `jido.*` commands → `CommandRunner` → VFS.
Also fixes `ShellSessionServer.apply_state_updates` to sync env changes
from `{:state_update, %{env: …}}` back into `backend_state.env` so Lua
(and other backends) see the updated env on subsequent execute calls.
14 tests covering: output streaming, global persistence, jido bridge,
cwd sync, arg escaping, sandbox escape proof, output/runtime limits,
timeout holder survival, cancel reusability, terminate cleanup, and
stateless mode non-persistence.
….API modules
Allows callers to pass extra Lua.API modules via the backend config:
backend: {Jido.Shell.Backend.Lua, %{apis: [MyApp.LuaApi]}}
The extra modules are loaded after JidoApi in both the persistent session
path (start_lua_session/2) and the stateless eval path (eval_stateless/4).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mikehostetler
left a comment
There was a problem hiding this comment.
Thanks for the substantial backend work here. I don't think this should land as-is because it repeats the optional dependency compile failure tracked in #48.
This PR adds {:lua, "~> 0.4", optional: true}, but the new modules compile-time reference the dependency unconditionally (use Lua.API, %Lua{} patterns, Lua.* calls). In downstream consumers that depend on jido_shell without also depending on :lua, these modules will still be compiled and should fail in the same way Bash.Interop is currently failing for optional :bash.
Before this lands, we need one of these paths:
- make
:luaa required dependency ifJido.Shell.Backend.Luais part of the compiled package surface, or - conditionally compile/load the Lua backend modules only when
:luais available and ensure docs/tests cover the consumer-without-lua case.
Given #48 is already forcing us to tighten backend dependency semantics, I would prefer we resolve that package-level policy first and then rebase this PR onto it.
|
Pushed a maintainer update in
I have not run local validation on this updated branch yet. |
Summary
Jido.Shell.Backend.Lua— a pure-Elixir Lua 5.3 backend using the latest:luaHex package from Lua.ex / deflua.com (~> 1.0.0-rc.1), mirrorsJido.Shell.Backend.Bashin structure and all 7 callbacks.:luaa required runtime dependency because the Lua backend is included in the compiled Hex package surface; this avoids optional-dependency compile failures without guarded compatibility code.Lua.Sessionholder GenServer owns committed%Lua{}state; eval runs in a killable spawned worker so timeout/cancel cannot pin the holder — commit only on clean success, prior state intact on failure.JidoApibridge exposes command registry underjido.*viause Lua.API, scope: "jido"+deflua; customprintglobal; arg escaping portsJidoInterop.escape_arg/1behavior.persistent: falseconfig flag for stateless-per-eval mode (freshLua.new()each call).ShellSessionServer.apply_state_updatesto sync{:state_update, %{env: …}}back intobackend_state.envfor all backends.Security
Lua.new/0sandbox disablesio,os.execute/exit/getenv,require,load,loadstring,loadfile,dofile,package.loadlibby default. No VfsAdapter needed — file access only through bridgedjido.*commands →CommandRunner→ VFS.Validation
:luawithmix hex.info lua:1.0.0-rc.1published 2026-06-02mix compile --warnings-as-errorsmix test(636 passed, 3 excluded)mix hex.build --unpack -o /tmp/jido_shell_pr45_hexMIX_ENV=prod mix deps.get && MIX_ENV=prod mix compile --warnings-as-errorsfrom the unpacked package