LemonBrowser

August 14, 2026 · View on GitHub

Browser capability driver for agents: a supervised local browser session, the guardrails that decide where it is allowed to go, and a view of the files it leaves behind.

lemon_browser is one of the packages that make up the Lemon agent platform. Its only Lemon dependency is lemon_core.

What is in it

ModulePurpose
LemonBrowser.LocalServerSupervised driver process: request/3, status/0, stop/1
LemonBrowser.RoutePolicyNavigation classification and guardrails: validate_navigation/2, safe/1
LemonBrowser.ArtifactsMetadata over saved artifacts: recent/1, summary/1, cleanup/1
LemonBrowser.EnvThe app's environment-variable registry, aggregated by LemonCore.Env

Installation

def deps do
  [{:lemon_browser, "~> 0.1"}]
end

Starting the application starts LemonBrowser.LocalServer under LemonBrowser.Supervisor; no other wiring is required.

Driving a browser

{:ok, result} = LemonBrowser.LocalServer.request("browser.navigate", %{"url" => "https://hex.pm"})

request/3 takes a method name, a map of arguments and an optional timeout in milliseconds (default 30_000), and answers {:ok, result} or {:error, reason}. Requests are multiplexed over one helper process: each one carries an id, and a request that outlives its timeout answers {:error, "Browser request timed out"} without disturbing the others.

LemonBrowser.LocalServer.status/0 reports whether the driver is available and running, the pending/completed/failed counts, the last error, and the resolved driver configuration.

Vetting a URL first

{:ok, policy} = LemonBrowser.RoutePolicy.validate_navigation("https://hex.pm", "public")
# => %{route: "public", effective_route: "public", target_kind: "public_network",
#      scheme: "https", private: false, metadata: false}

{:error, "browser navigation blocked metadata endpoint"} =
  LemonBrowser.RoutePolicy.validate_navigation("http://169.254.169.254/latest/meta-data")

Routes are "auto" (the default), "public" (public http(s) only) and "local" (local documents and private-network addresses only). Cloud metadata endpoints are refused on every route. safe/1 reduces a policy to a camelCased map with nil/false entries dropped, suitable for returning to a caller or writing to a log.

This module is pure and does not touch the driver, so it can be used on its own to vet URLs before handing them to any browser automation.

The Node driver, and what happens without it

The helper process is a Node program that speaks a line-delimited JSON protocol over stdin/stdout — one JSON object per line:

request:  {"id": "...", "method": "browser.navigate", "args": {...}, "timeoutMs": 30000}
response: {"id": "...", "ok": true, "result": {...}}

The driver is not shipped in this package. It is resolved from LEMON_BROWSER_DRIVER_PATH, and otherwise from clients/lemon-browser-node/dist/local-driver.js relative to the current working directory, which is the Lemon monorepo's layout. Outside that monorepo, point LEMON_BROWSER_DRIVER_PATH at a driver implementing the protocol above.

Absence degrades rather than crashes. The port is opened lazily on the first request, so an application that never drives a browser never spawns anything. If node is missing, or the driver path does not resolve, request/3 answers {:error, "node executable not found on PATH"} or {:error, "Local browser driver not built..."}, the error is remembered in status/0, and the supervision tree stays up. If a running helper exits, only the requests in flight fail; the next request starts a fresh one.

Environment variables

VariableDefaultPurpose
LEMON_BROWSER_DRIVER_PATHPath to the driver program
LEMON_BROWSER_CDP_ENDPOINTCDP websocket endpoint to attach to instead of launching a browser
LEMON_BROWSER_ATTACH_ONLYfalseOnly attach to an existing browser, never launch one
LEMON_BROWSER_CDP_PORT18800Local CDP port for a managed browser (positive integers only)

Artifacts

Screenshots and other files a session saves default to <project_dir>/.lemon/browser-artifacts; :dir overrides the directory and :project_dir moves it. recent/1 lists metadata (name, path, bytes, modification time) newest-first, up to :limit entries (default 20, capped at 100). summary/1 reports the count, total bytes and the age boundaries alongside the retention policy in force. cleanup/1 prunes by age and count, defaulting to 14 days and 100 files, and reports what it removed.