diff --git a/.claude/hooks/pre-commit.sh b/.claude/hooks/pre-commit.sh
index 1d56daf..7e80e74 100755
--- a/.claude/hooks/pre-commit.sh
+++ b/.claude/hooks/pre-commit.sh
@@ -12,6 +12,11 @@ fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
+# Skip checks if mix is not available (e.g., in CI or sandboxed environments)
+if ! command -v mix &> /dev/null; then
+ exit 0
+fi
+
echo "Running pre-commit checks..." >&2
if ! mix compile --warnings-as-errors 2>&1; then
diff --git a/.envrc b/.envrc
deleted file mode 100644
index fe7c01a..0000000
--- a/.envrc
+++ /dev/null
@@ -1 +0,0 @@
-dotenv
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8d49fd3
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,54 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ name: Elixir ${{ matrix.elixir }} / OTP ${{ matrix.otp }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - elixir: "1.19"
+ otp: "27"
+ env:
+ MIX_ENV: test
+ steps:
+ - name: Install system dependencies
+ run: sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep
+
+ - uses: actions/checkout@v4
+
+ - uses: erlef/setup-beam@v1
+ with:
+ elixir-version: ${{ matrix.elixir }}
+ otp-version: ${{ matrix.otp }}
+
+ - name: Cache deps
+ uses: actions/cache@v4
+ with:
+ path: |
+ deps
+ _build
+ key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('mix.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
+
+ - name: Install dependencies
+ run: mix deps.get
+
+ - name: Compile (warnings as errors)
+ run: mix compile --warnings-as-errors
+
+ - name: Check formatting
+ run: mix format --check-formatted
+
+ - name: Run tests
+ run: mix test
diff --git a/.gitignore b/.gitignore
index 86e7a58..8a931c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,7 @@ erl_crash.dump
# Environment files with secrets
.env
.env.*
+.envrc
# macOS metadata
.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5437e22..a6b8a77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,25 @@ All notable changes to this project are documented here.
GitHub API via `curl` + `jq`, parses with `Jason`, and generates summaries via
parallel schema queries. Run with `mix rlm.examples web_fetch`.
+- GitHub Actions CI workflow (compile, format, test)
+- Security notice in README documenting the trust boundary
+
+### Changed
+
+- API key log output now shows only last 4 characters instead of first 12
+- `.envrc` untracked and added to `.gitignore` (local direnv preference)
+- Added `@moduledoc` to `RLMWeb.Endpoint`, `RLMWeb.Router`, `RLMWeb.RunListLive`,
+ `RLMWeb.RunDetailLive`, and `RLMWeb.Telemetry`
+- Pre-commit hook skips gracefully when `mix` is not available
+
+### Removed
+
+- `demo_run.exs` — referenced missing `priv/foodlab.epub`; would crash on clone
+- `RLMWeb.PageController`, `RLMWeb.PageHTML` — unused Phoenix generator leftovers
+- `RLMWeb.channel/0` helper — no channels in the application
+- Swoosh dependency and `RLMWeb.Mailer` — no email functionality exists
+- `/dev/mailbox` route — removed with Swoosh
+
---
## [0.3.0] — 2026-02-24
diff --git a/CLAUDE.md b/CLAUDE.md
index 4e81cf2..2673d95 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -35,10 +35,9 @@ rlm/
│ │ ├── router.ex
│ │ ├── telemetry.ex
│ │ ├── gettext.ex
-│ │ ├── mailer.ex
│ │ ├── live/ # RunListLive (/), RunDetailLive (/runs/:run_id)
│ │ ├── components/ # CoreComponents, Layouts, TraceComponents
-│ │ └── controllers/ # ErrorHTML, ErrorJSON, PageController, TraceDebugController
+│ │ └── controllers/ # ErrorHTML, ErrorJSON, TraceDebugController
│ └── mix/tasks/ # mix rlm.smoke, mix rlm.examples
├── test/
│ ├── test_helper.exs
@@ -227,7 +226,7 @@ Read-only Phoenix LiveView dashboard. Serves on `http://localhost:4000`.
|---|---|
| `RLMWeb` | Phoenix web module (verified routes, component imports) |
| `RLMWeb.Endpoint` | Phoenix endpoint using `RLM.PubSub` as pubsub_server |
-| `RLMWeb.Router` | Routes: `/` (RunListLive), `/runs/:run_id` (RunDetailLive), `/dev/dashboard`, `/dev/mailbox` |
+| `RLMWeb.Router` | Routes: `/` (RunListLive), `/runs/:run_id` (RunDetailLive), `/dev/dashboard` |
| `RLMWeb.RunListLive` | `/` — list of all runs (from TraceStore + live PubSub updates) |
| `RLMWeb.RunDetailLive` | `/runs/:run_id` — recursive span tree with expandable iterations |
| `RLMWeb.TraceComponents` | HEEx components: `span_node/1`, `iteration_card/1` |
@@ -236,7 +235,7 @@ Read-only Phoenix LiveView dashboard. Serves on `http://localhost:4000`.
| `RLMWeb.CoreComponents` | Core UI components (flash, button, input, table, icon, etc.) |
| `RLMWeb.Layouts` | App layout, flash group, theme toggle |
| `RLMWeb.Gettext` | Internationalization backend |
-| `RLMWeb.Mailer` | Swoosh mailer |
+
## Config Fields
diff --git a/README.md b/README.md
index 60e9906..907975e 100644
--- a/README.md
+++ b/README.md
@@ -306,6 +306,16 @@ Tools live inside the sandbox — the eval'd code calls `read_file/1`, `bash/1`,
---
+## Security
+
+RLM executes LLM-generated Elixir code via `Code.eval_string` with full access to the
+host filesystem, network, and shell. **Do not expose RLM to untrusted users or untrusted
+LLM providers.** It is designed for local development, trusted API backends (Anthropic),
+and controlled environments. There is no sandboxing beyond process-level isolation and
+configurable timeouts.
+
+---
+
## Further reading
For a comprehensive architecture reference — OTP supervision tree, async-eval pattern,
diff --git a/config/config.exs b/config/config.exs
index 637bfd6..7984104 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -27,15 +27,6 @@ config :rlm, RLMWeb.Endpoint,
pubsub_server: RLM.PubSub,
live_view: [signing_salt: "G4RzK1j2"]
-# Configure the mailer
-#
-# By default it uses the "Local" adapter which stores the emails
-# locally. You can see the emails in your browser, at "/dev/mailbox".
-#
-# For production it's recommended to configure a different adapter
-# at the `config/runtime.exs`.
-config :rlm, RLMWeb.Mailer, adapter: Swoosh.Adapters.Local
-
# Configure esbuild (the version is required)
config :esbuild,
version: "0.25.4",
diff --git a/config/dev.exs b/config/dev.exs
index 781c152..10d283f 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -57,7 +57,7 @@ config :rlm, RLMWeb.Endpoint,
]
]
-# Enable dev routes for dashboard and mailbox
+# Enable dev routes for dashboard
config :rlm, dev_routes: true
# Do not include metadata nor timestamps in development logs
@@ -77,6 +77,3 @@ config :phoenix_live_view,
debug_attributes: true,
# Enable helpful, but potentially expensive runtime checks
enable_expensive_runtime_checks: true
-
-# Disable swoosh api client as it is only required for production adapters.
-config :swoosh, :api_client, false
diff --git a/config/prod.exs b/config/prod.exs
index d6cd726..0c67efd 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -17,12 +17,6 @@ config :rlm, RLMWeb.Endpoint,
hosts: ["localhost", "127.0.0.1"]
]
-# Configure Swoosh API Client
-config :swoosh, api_client: Swoosh.ApiClient.Req
-
-# Disable Swoosh Local Memory Storage
-config :swoosh, local: false
-
# Do not print debug messages in production
config :logger, level: :info
diff --git a/config/runtime.exs b/config/runtime.exs
index e0dfca1..76d8b37 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -88,22 +88,4 @@ if config_env() == :prod do
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
-
- # ## Configuring the mailer
- #
- # In production you need to configure the mailer to use a different adapter.
- # Here is an example configuration for Mailgun:
- #
- # config :rlm, RLMWeb.Mailer,
- # adapter: Swoosh.Adapters.Mailgun,
- # api_key: System.get_env("MAILGUN_API_KEY"),
- # domain: System.get_env("MAILGUN_DOMAIN")
- #
- # Most non-SMTP adapters require an API client. Swoosh supports Req, Hackney,
- # and Finch out-of-the-box. This configuration is typically done at
- # compile-time in your config/prod.exs:
- #
- # config :swoosh, :api_client, Swoosh.ApiClient.Req
- #
- # See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
end
diff --git a/config/test.exs b/config/test.exs
index c4c8650..5baf0bd 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -7,12 +7,6 @@ config :rlm, RLMWeb.Endpoint,
secret_key_base: "wGMmiWW+w1iw84uJuC13MGBtRyTSECbvCuOgLhxi2fgyC3qgc/n+iJQXmqYAdAMo",
server: false
-# In test we don't send emails
-config :rlm, RLMWeb.Mailer, adapter: Swoosh.Adapters.Test
-
-# Disable swoosh api client as it is only required for production adapters
-config :swoosh, :api_client, false
-
# Print only warnings and errors during test
config :logger, level: :warning
diff --git a/demo_run.exs b/demo_run.exs
deleted file mode 100644
index b5bccce..0000000
--- a/demo_run.exs
+++ /dev/null
@@ -1,52 +0,0 @@
-# Load API key from .env
-env_path = Path.join(__DIR__, ".env")
-
-if File.exists?(env_path) do
- env_path
- |> File.stream!()
- |> Enum.each(fn line ->
- line = String.trim(line)
-
- if line != "" and not String.starts_with?(line, "#") do
- case String.split(line, "=", parts: 2) do
- [key, value] ->
- value = value |> String.trim() |> String.trim("\"") |> String.trim("'")
- System.put_env(String.trim(key), value)
-
- _ ->
- :ok
- end
- end
- end)
-end
-
-# Load a cross-section of the cookbook HTML (files 3-45: ~200 KB, rich recipe content)
-path = Application.app_dir(:rlm, "priv/foodlab.epub")
-{:ok, zip_files} = :zip.unzip(String.to_charlist(path), [:memory])
-
-html =
- zip_files
- |> Enum.map(fn {name, content} -> {to_string(name), content} end)
- |> Enum.filter(fn {k, _} -> String.ends_with?(k, ".html") or String.ends_with?(k, ".xhtml") end)
- |> Enum.sort_by(fn {k, _} -> k end)
- |> Enum.slice(3, 42)
- |> Enum.map_join("\n\n", fn {name, content} -> "=== #{name} ===\n#{content}" end)
-
-IO.puts("Input size: #{div(byte_size(html), 1024)} KB")
-
-query = """
-This is HTML from a cookbook. Identify the top 5 most frequently mentioned
-main proteins (meats, fish, legumes) across all recipes in the content.
-
-The input is large — split it into 3 roughly equal chunks and use
-parallel_query to extract protein mentions from each chunk concurrently,
-then aggregate and rank across all results.
-"""
-
-IO.puts("Starting RLM run...\n")
-
-{:ok, answer, run_id} = RLM.run(html, query)
-
-IO.puts("\n=== Answer ===")
-IO.puts(answer)
-IO.puts("\nRun ID: #{run_id}")
diff --git a/examples/smoke_test.exs b/examples/smoke_test.exs
index 75ab007..abf53f6 100644
--- a/examples/smoke_test.exs
+++ b/examples/smoke_test.exs
@@ -160,7 +160,7 @@ defmodule RLM.SmokeTest do
key ->
IO.puts("\nRLM Smoke Test")
IO.puts("==============")
- IO.puts("API key: #{String.slice(key, 0, 12)}...")
+ IO.puts("API key: ...#{String.slice(key, -4, 4)}")
IO.puts("")
end
end
diff --git a/lib/mix/tasks/rlm.examples.ex b/lib/mix/tasks/rlm.examples.ex
index 41b456d..a7dd370 100644
--- a/lib/mix/tasks/rlm.examples.ex
+++ b/lib/mix/tasks/rlm.examples.ex
@@ -137,7 +137,7 @@ defmodule Mix.Tasks.Rlm.Examples do
System.halt(1)
key ->
- IO.puts(" API key: #{String.slice(key, 0, 12)}...")
+ IO.puts(" API key: ...#{String.slice(key, -4, 4)}")
end
end
diff --git a/lib/rlm_web.ex b/lib/rlm_web.ex
index 2052e9d..5f29b53 100644
--- a/lib/rlm_web.ex
+++ b/lib/rlm_web.ex
@@ -1,7 +1,7 @@
defmodule RLMWeb do
@moduledoc """
The entrypoint for defining your web interface, such
- as controllers, components, channels, and so on.
+ as controllers, components, and so on.
This can be used in your application as:
@@ -34,12 +34,6 @@ defmodule RLMWeb do
end
end
- def channel do
- quote do
- use Phoenix.Channel
- end
- end
-
def controller do
quote do
use Phoenix.Controller, formats: [:html, :json]
diff --git a/lib/rlm_web/controllers/page_controller.ex b/lib/rlm_web/controllers/page_controller.ex
deleted file mode 100644
index 7f0bbd1..0000000
--- a/lib/rlm_web/controllers/page_controller.ex
+++ /dev/null
@@ -1,7 +0,0 @@
-defmodule RLMWeb.PageController do
- use RLMWeb, :controller
-
- def home(conn, _params) do
- render(conn, :home)
- end
-end
diff --git a/lib/rlm_web/controllers/page_html.ex b/lib/rlm_web/controllers/page_html.ex
deleted file mode 100644
index 11da15a..0000000
--- a/lib/rlm_web/controllers/page_html.ex
+++ /dev/null
@@ -1,10 +0,0 @@
-defmodule RLMWeb.PageHTML do
- @moduledoc """
- This module contains pages rendered by PageController.
-
- See the `page_html` directory for all templates available.
- """
- use RLMWeb, :html
-
- embed_templates "page_html/*"
-end
diff --git a/lib/rlm_web/controllers/page_html/home.html.heex b/lib/rlm_web/controllers/page_html/home.html.heex
deleted file mode 100644
index b107fd0..0000000
--- a/lib/rlm_web/controllers/page_html/home.html.heex
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Peace of mind from prototype to production. -
-- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -
-