Skip to main content

CHANGELOG.md

# Changelog

## [Unreleased]

## 0.1.3 - 2026-06-20

### Added

- The `jq` builtin (JSON processing) is now bundled — bashkit's `jq` feature
  (the pure-Rust `jaq` engine) is enabled in the precompiled NIF, so scripts can
  pipe through `jq` (e.g. `curl … | jq '.field'`). Pure computation, no extra
  runtime gate.

## 0.1.2 - 2026-06-20

First release. An Elixir NIF wrapper around
[bashkit](https://github.com/everruns/bashkit) `0.11.0` — a sandboxed, in-process
virtual bash interpreter written in Rust. Every execution semantic comes from
bashkit; ExBashkit only marshals data across the NIF boundary.

### Added

- Stateless `ExBashkit.exec/1` — runs a bash script in a fresh sandbox and returns
  an `%ExBashkit.Result{}` (`stdout`, `stderr`, `exit_code`). A non-zero exit is
  still `{:ok, ...}` — the script ran.
- `ExBashkit.Session` — persistent, stateful sandboxes. Unlike `exec/1`, a
  session's environment variables, working directory, in-memory filesystem, shell
  functions and aliases persist across `ExBashkit.Session.exec/2` calls.
  `ExBashkit.Session.new/1` seeds initial state via `:env`, `:cwd`, `:username`,
  and `:hostname`. Each session is an independent sandbox and serializes its own
  calls.
- Virtual filesystem access from Elixir. `ExBashkit.Session.write_file/3` and
  `read_file/2` place and retrieve files in a session's in-memory filesystem —
  shared with scripts, so the host can stage inputs and pull back results
  (round-tripping arbitrary binary content) without going through a script.
  `Session.new/1` gains a `:files` option to seed files (creating parent dirs) up
  front.
- Host directory mounts. `ExBashkit.Session.new/1` accepts `:mounts` —
  `{vfs_path, host_path, mode}` tuples (`:read_only` / `:read_write`) — mapping
  real host directories into a sandbox, plus `:allowed_mount_paths` to opt into
  bashkit's sensitive-path default-deny. bashkit enforces canonicalization and
  symlink/`..` escape rejection; misconfigured mounts (unknown mode, missing or
  non-directory host path) raise from `new/1`. (`:overlay` is intentionally
  unsupported — bashkit has no real-FS overlay mode.)
- Resource limits. `ExBashkit.Session.new/1` accepts `:limits` (keyword list or
  map) to tighten bashkit's execution bounds for untrusted scripts:
  `:max_commands`, `:max_loop_iterations`, `:max_total_loop_iterations`,
  `:max_function_depth`, `:max_input_bytes`, and `:timeout_ms`. Exceeding a limit
  returns `{:error, message}`; unknown keys or non-integer values raise.
- Network access. `ExBashkit.Session.new/1` accepts `:allow_net` — a list of URL
  patterns the `curl`/`wget`/`http` builtins may reach, or `:all` for any host.
  The allowlist is default-deny (a session with no `:allow_net` cannot reach the
  network at all), matches scheme/host/port/path-prefix literally, and does not
  follow redirects. Requests to private/reserved IP ranges are blocked by default
  (SSRF protection); `:block_private_ips` (default `true`) controls this. The NIF
  bundles bashkit's `http_client` feature (reqwest + rustls), so network support
  ships in the precompiled binary. Invalid `:allow_net`/`:block_private_ips`
  values raise from `new/1`.
- Custom builtins. `ExBashkit.Session.new/1` accepts `:builtins` — a map of
  `name => fun` registering Elixir-defined virtual executables a script invokes as
  `name args…`. Each builtin is a 1-arity function receiving
  `%{args, stdin, env, cwd}` and returning `{:ok, iodata}` (stdout/exit 0),
  `{:error, iodata}` (stderr/exit 1), or a full `%ExBashkit.Result{}`. The call is
  a blocking Rust→Elixir round-trip serviced by a short-lived per-`exec/2` process;
  a handler that raises or exceeds `:builtin_timeout_ms` (default 30_000; exit 124)
  fails only that command, not the session. A builtin must not call `exec/2` on the
  same session (reentrancy deadlock); driving a different session is fine.
- Elixir-backed virtual filesystems. `ExBashkit.Session.new/1` accepts
  `:virtual_fs` — a map of `mount_path => backend` mounting filesystems whose reads
  and writes a script performs under that path are serviced by your application
  (generate content on demand, proxy to a real store). A backend is a module
  implementing the `ExBashkit.VirtualFs` behaviour (as `module` or `{module, arg}`),
  or a single dispatch function for inline use. Read **and** write are supported
  (`read`/`write`/`append`/`mkdir`/`remove`/`list`/`stat`, returning tagged
  results); `exists` is derived from `stat`, the mount root is a directory, `chmod`
  is a no-op, and `rename`/`copy`/`symlink`/`read_link` are not yet proxied.
  Composes with the in-memory FS, `:files`, and host `:mounts`.
- Host filesystem primitives on `ExBashkit.Session`: `stat/2`, `list_dir/2`,
  `mkdir/3`, `remove/3`, `rename/3` — lock-free introspection/mutation of a
  session's (shared) virtual filesystem, alongside `read_file/2`/`write_file/3`.
- Snapshot & resume. `ExBashkit.Session.snapshot/2` captures a session's shell
  state (variables, env, cwd, aliases, functions) and in-memory filesystem
  contents as a binary; `ExBashkit.Session.restore/3` loads it back into a session,
  returning `{:ok, session}` or `{:error, message}`. A snapshot carries interpreter
  state, not session *config* (custom `:builtins`, `:virtual_fs` backends, host
  `:mounts`, `:limits`), so to resume you rebuild a session with the same
  capabilities and restore into it; restore validates the whole snapshot before
  mutating, leaving the session usable on a bad/tampered/wrong-key load. `snapshot/2`
  options: `:key` (a non-empty binary → HMAC-keyed snapshot for crossing trust
  boundaries; the matching key is required on restore, and a wrong key or tampered
  bytes are rejected — without a key the embedded digest detects accidental
  corruption only), `:exclude_filesystem`, and `:exclude_functions`.
- Sandboxed `python` builtin. `ExBashkit.Session.new(python: true)` registers
  `python`/`python3` virtual executables that run sandboxed Python (via the optional
  `:ex_monty` dependency) **sharing the session's virtual filesystem** — a file a
  bash step writes, the Python reads, and vice versa. Supports `python file.py`,
  `python -c "…"`, and a program piped on stdin; Python's `pathlib`/`os` filesystem
  operations are routed to the session, while every other effect (network, clocks)
  is denied. A Python error or timeout fails only that command, not the session.
  Opt-in by adding `:ex_monty` to your deps; ExBashkit compiles and runs without it,
  and `python: true` raises a helpful error if it is absent. (Limitations: no
  `sys.argv`; `pathlib.Path` I/O, not `open()`.) See `ExBashkit.Python`.
- LLM tool recipe. A session can be used as an agent "bash" tool with a small
  amount of plain data (a JSON schema, a system prompt, and a function that runs a
  tool call and formats the result) — documented in the README and a runnable
  `examples/llm_tool.exs` (with a ReqLLM wiring snippet). Deliberately *not* a
  module: the glue is framework-specific and tiny, so ExBashkit stays agnostic.