A config-driven harness that runs an LLM as a safe, sandboxed task coordinator
An untrusted model that only routes requests, and tools that run as isolated WebAssembly components.
Olivia is a small, auditable runtime for putting an LLM in front of real actions without handing it a shell. You describe an agent and the services that invoke it in a single TOML file; the harness exposes the matching endpoints and routes every request through the agent. The agent never does the work itself — it only decides which tool to call, and every tool runs inside an isolated WebAssembly sandbox.
The "ia" in Olivia stands for intelligenza artificiale — Italian for artificial intelligence.
Two ideas drive the whole design:
- The model is a router, not a solver. It is instructed to perform zero internal logic and to accomplish everything by delegating to declared tools. Its only possible output is a small JSON envelope that either calls a tool or ends the run.
- Tools are untrusted code, so they run sandboxed. Each tool is a WebAssembly component executed under a capability-based WASI sandbox that grants nothing by default. A tool can only touch what the host explicitly hands it.
Together these bound the blast radius of both a misbehaving model and a
misbehaving tool: the worst a jailbroken model can do is "call tool X with
params Y", and the worst a buggy tool can do is limited to the handful of
capabilities it was granted. See Security model.
HTTP request ─┐
Telegram msg ─┼─► service ─► agent ─► done / error ─► reply to caller
cron tick ─┘ ▲ │
│ ▼
┌── agentic loop (repeats think → act until done/error, ≤ MAX_ITERATIONS = 200) ──────
│
│ ① send a FIXED-SIZE context ─────────► LiteLLM ──► Ollama / Anthropic / OpenAI
│ · system: tool catalogue + data stores + rules │
│ · the original request │
│ · latest state — ONE rolling message ▼
│ ▲ ② model returns ONE JSON envelope
│ │ { thought, state, … }
│ │ ┌────────────┴─────────────┐
│ │ state="tool" state="done"|"error"
│ │ { name, params } { result | error_message }
│ │ │ │
│ │ ▼ ▼
│ │ ③ tool registry ─► run in leave the loop; the
│ │ a wasm + WASI sandbox agent returns to the
│ │ (SearXNG · Browserless · service → reply
│ │ Postgres · ClickHouse ·
│ │ S3 · /sandbox)
│ │ │
│ ④ REPLACE the rolling message with the │
│ model's envelope + this tool result ◄┘
└────────────────────────────────────────────────────────────────────────────────────
A request enters through a service, is handled by the agent, and the agent runs an agentic loop:
- A service receives a trigger — an HTTP request, a Telegram message, or a cron tick — and calls the agent with the incoming text.
- The agent asks the model what to do. It assembles a fixed-size context — the developer prompt (the tool catalogue, the available data stores, the rules), the original request, and a single rolling latest state message — and sends it to the model through the LiteLLM proxy, which fronts Ollama, Anthropic and OpenAI.
- The model replies with a JSON envelope — nothing else. Every envelope
carries a
thought(the model's running memory, see below) and astate, one of:{"state":"tool", "name":…, "params":…}— call a tool,{"state":"done", "result":…}— finished, here is the answer,{"state":"error", "error_message":…}— give up with a reason.
- On
tool, the harness looks the tool up in the registry, runs it in a fresh wasm + WASI sandbox, and replaces the rolling latest state message with the model's envelope plus the tool's output — the context stays the same size — then loops back to step 2. - On
done/error, the agent returns to the service, which replies to the caller. The loop is bounded byMAX_ITERATIONS(200).
The context never grows. History is deliberately not preserved between
iterations: each turn re-sends the same three slots and step 4 overwrites the last
one. The model's only memory is the envelope's thought object — a compact
JSON-encoded status it rewrites and copies forward itself every turn:
| Field | Meaning |
|---|---|
goal |
The user's objective, restated once in ≤ 20 words and then copied forward verbatim — never re-derived or reworded. |
plan |
The full step list (≤ 8 entries), each "<n>. <imperative>". Written on the first iteration and carried forward unchanged; on a replan only the not-yet-done tail is rewritten and indices are never reused. |
done |
Append-only log of finished steps, each "<n>:<ok|fail> <outcome>" — outcomes, not narrative, and never a copy of the tool output. |
facts |
Durable key→value data later steps need (file paths, IDs, counts, URLs, schema names). Terse keys, dropped once unused. Never credentials or file contents. |
cur |
The step running now, "<n> <action>", with a short justification only when the choice is non-obvious. |
A fresh model instance handed only the last thought plus the newest tool result
has everything it needs to continue — which is exactly what each iteration is.
Supporting pieces:
- Data stores — Postgres, ClickHouse and S3-compatible object storage are declared in config and surfaced to the model; it reaches them only by passing their connection details to the matching tool.
- The sandbox directory — a shared
/sandboxfolder is mounted into the harness and preopened for every tool, so tools can persist files and hand data to one another across steps. - Sessions — every run is tagged with a UUID. When a run completes
(
done), the agent saves its full message chain under that id, so a caller can resume the same conversation on a later request. See Sessions.
The host/guest boundary is described once, as a
WIT world in
tools/tool.wit. Both the harness and every tool generate bindings from it, so
the ABI is checked at compile time rather than hand-marshalled:
package olivia:tools;
world tool-world {
enum permission { network, file-system }
record tool-info {
name: string,
description: string,
schema: string, // JSON Schema for this tool's params
permissions: list<permission>,// capabilities the sandbox must grant
env: list<string> // host env vars to forward into the sandbox
}
enum tool-output-state { done, error }
record tool-output {
state: tool-output-state,
content: string
}
export info: func() -> tool-info;
export run: func(input: string) -> tool-output;
}info()is called once at load time and returns the tool'sname, human-readabledescription, a JSON Schema for its parameters, the list ofpermissions(capabilities) it needs, and the list ofenvvariables it wants forwarded from the host. The harness uses this to build its registry, to tell the model which tools exist and how to call them, and to decide which capabilities and environment variables to grant the tool's sandbox — a tool receives nothing it did not declare here.run(input)is called when the model routes a request to this tool. It receives a JSON string of parameters (matching the schema) and returns atool-outputwhosestatereports success or failure and whosecontentcarries the result (or the error message).
Any language that can produce a wasm32-wasip2 component implementing this
world can be a tool — see Implementing a tool.
The harness (harness/) is an async Rust service built on
wasmtime. At startup it:
- loads the config file (
/config.tomlby default, override with--config=<path>) — agent + services; - verifies the configured
modelis registered on the LiteLLM proxy, refusing to start otherwise; - loads every
*.wasmfrom the tools folder (/toolsby default, override with--tools=<folder>), calling each tool'sinfo()to build the registry and the tool catalogue injected into the system prompt.
Each tool file is loaded as a wasmtime::component::Component; for every call
the harness instantiates it into a fresh, sandboxed Store, so tools
accumulate no state across invocations. The runtime is async, so tool calls
never block the request loop.
Both startup paths default to the container mount points and can be pointed
elsewhere. Options are accepted only in --key=value form:
| Option | Default | Description |
|---|---|---|
--config=<path> |
/config.toml |
The TOML config file to load. |
--tools=<folder> |
/tools |
The folder scanned for *.wasm tools. |
--sandbox=<folder> |
/sandbox |
Host base directory for per-session sandboxes; each session gets <folder>/<session_id>, mounted into file-system tools at /sandbox. |
harness --config=/examples/hello.toml --tools=/toolsBundled example configs live in examples/ (mounted read-only into the harness
at /examples); examples/hello.toml exposes a single HTTP endpoint that
greets a posted name.
Tools are stateless, but a conversation is not: a caller often needs to send a follow-up that builds on what the agent already did. Sessions carry that context.
Every run is identified by a session id (a UUID). Two entry points on the agent decide whether a run starts fresh or continues an existing one:
accept(request, None)mints a new UUID, prepends the router system prompt, and runs the agentic loop from scratch.accept(request, Some(id))looks up a stored message chain by id, appends the new messages to it, and continues the loop with the full history (an unknown id returns anInvalid session iderror).
When a run finishes with done, the agent stores its complete message chain
(system prompt, every model turn, and every tool result) under the session id
before returning. A later accept with that id replays the whole exchange, so the
model sees everything that happened. Runs that end in error are not saved,
and the store is an in-memory map — sessions do not survive a harness restart.
Every reply carries the id back to the caller:
{
"session_id": "b1f7c0de-1234-4a5b-9c8d-0f1e2d3c4b5a",
"payload": { "state": "done", "result": "…" }
}Each service exposes this differently:
- HTTP — the response is
{ "error": …, "data": { "session_id", "payload" } }. To continue a conversation, send the id back on the next request in theOLIVIA-SESSION-IDheader; the harness parses it and continues that session. A request with no (or an unparseable) header starts a fresh session; a well-formed id that isn't in the store returns anInvalid session iderror. - Telegram —
/do <text>starts a session and the bot remembers it per chat, so any plain follow-up message continues that conversation automatically. Messaging before you have run/doreplies withUnable to restore session. - cron — each tick is a standalone
accept; there is no caller to hand the id to, so cron runs are effectively single-shot.
The harness loads its configuration from /config.toml (override with
--config=<path>):
[agent]
model = "anthropic/claude-haiku-4-5" # must be registered in the LiteLLM model_list
prompt = "You must help us with some simple tasks"
# ── Data stores the agent can reach through the matching tool ──
[agent.stores.default]
type = "postgres"
connection_string = "postgresql://user:password@postgres:5432/olivia"
prompt = "Default relational store — create tables and persist data here."
[agent.stores.analytics]
type = "clickhouse"
host = "http://clickhouse:8123" # must include the http:// or https:// scheme
username = "default" # optional, defaults to "default"
password = "" # optional, defaults to empty
prompt = "Column-oriented store for large-scale aggregations and reporting."
[agent.stores.files]
type = "s3"
bucket = "olivia-files"
endpoint = "http://rustfs:9000" # must include the http:// or https:// scheme
region = "us-east-1" # optional, defaults to "us-east-1"
access_key = "rustfsadmin"
secret_key = "rustfsadmin"
prompt = "S3-compatible object store for files and blobs."
# ── Services that invoke the agent ──
[services.public_api] # an HTTP service (default 0.0.0.0:80)
type = "http"
[services.public_api.endpoints.ask]
method = "post"
path = "/ask"
prompt = "You received a request from the user. Please help."
[services.bot] # a Telegram bot
type = "telegram"
token = "123456:ABC-DEF..." # bot token from @BotFather
prompt = "You are a helpful assistant reachable from Telegram"
[services.nightly] # a scheduled trigger
type = "cron"
schedule = "0 0 3 * * *" # 6-field cron: sec min hour day month weekday
prompt = "Summarise yesterday's activity and store it."[agent] — model (required): a model id registered in the LiteLLM
model_list. prompt (required): the agent system prompt.
[agent.stores.<name>] — an optional data store the agent may use. type
selects the backend (and the tool that talks to it) and prompt describes the
store to the model. Remaining fields depend on type:
type |
Fields |
|---|---|
postgres |
connection_string |
clickhouse |
host (with scheme), username (default default), password (default empty) |
s3 |
bucket, endpoint (with scheme), access_key, secret_key, region (default us-east-1) |
Each store is listed to the model under AVAILABLE DATA STORES; the model
forwards its connection details to the matching tool (postgres_client /
clickhouse_client / s3_client) and is instructed never to leak them.
[services.<name>] — type is http, telegram, or cron:
http—port(default80),address(default0.0.0.0), and one or more[services.<name>.endpoints.<ep>]withpath(default/),method(GET/POST/PUT/DELETE, defaultGET) andprompt. The request body is forwarded to the agent; each endpoint replies with a JSON envelope carrying thesession_idand the agent'spayload. Pass a prior id back in theOLIVIA-SESSION-IDheader to continue that conversation — see Sessions.telegram—token(from @BotFather) andprompt. Answers/helpand/do <text>(starts a run through the agent); plain follow-up messages continue the chat's session — see Sessions.cron—schedule(a 6-field, seconds-precision cron expression viatokio-cron-scheduler) andprompt. On every tick it runs the agent with the prompt; there is no caller, so the result is logged.
| Tool | Path | Params | What it does |
|---|---|---|---|
hello_tool |
tools/hello/ |
{ "suffix": … } |
Reference implementation — returns Hello <suffix>. |
python |
tools/python/ |
{ "script": … } |
Runs a Python 3 script in an embedded RustPython interpreter inside the wasm sandbox. |
web_search |
tools/search/ |
{ "query": … } |
Searches the web via a SearXNG instance and returns the JSON results. |
web |
tools/web/ |
{ "code": … } |
Drives a headless browser through the Browserless /function API (stealth mode on): runs a Puppeteer function you supply and returns its output. |
http |
tools/http/ |
{ "method": …, "url": …, "headers": …, "body": … } |
Makes a direct HTTP request to any URL and returns the status line plus the response body. method ∈ GET/POST/PUT/PATCH/DELETE/HEAD; optional headers (name/value pairs) and body. Any status (incl. 4xx/5xx) comes back as a result; only network-level failures error. For pages needing a real browser, use web. |
download |
tools/download/ |
{ "code": …, "path": … } |
Triggers a browser download through Browserless /download and writes the file's bytes to the absolute sandbox path (e.g. /sandbox/report.pdf). The code must fire a real download (click a link / click an <a download>) — page.goto(fileUrl) alone does not download. |
fs |
tools/fs/ |
{ "operation": …, "path": …, "kind": …, "content": … } |
Manages files and directories inside the sandbox. operation ∈ list (entries of path, each prefixed dir /file) / create (a file or directory per kind, writing optional content and any missing parents) / delete (recursive for directories). path is an absolute sandbox path, e.g. /sandbox/reports/2026.txt. |
postgres_client |
tools/postgres/ |
{ "connection_string": …, "query": … } |
Runs one SQL statement against a PostgreSQL store and returns the result as CSV. Speaks the v3 wire protocol directly over std::net. |
clickhouse_client |
tools/clickhouse/ |
{ "host": …, "username": …, "password": …, "query": … } |
Runs one SQL statement against a ClickHouse store over its HTTP interface and returns a SELECT as CSV (default_format=CSVWithNames). |
s3_client |
tools/s3/ |
{ "bucket": …, "region": …, "endpoint": …, "access_key": …, "secret_key": …, "operation": …, "key": …, "path": … } |
Manages files in an S3-compatible store (SigV4 via rusty-s3). operation ∈ list / create (upload the sandbox file at absolute path to key) / delete / download (save key to absolute path). |
The
pythontool runs builtins-only: the full language is available, but there is no importable standard library (import json,import math, … raiseModuleNotFoundError). It returns whatever the script assigns to the global__OLIVIA__FINAL__RESULT__.
A tool is any wasm32-wasip2 component that implements the tool-world
world in tools/tool.wit. That makes tools language-agnostic: whatever
toolchain can emit a component works. The recipe is always the same:
- Generate bindings from
tools/tool.witfor the worldtool-world. - Implement the two exports:
info()→{ name, description, schema, permissions }, whereschemais a JSON Schema string describing the params (the model readsdescription+schemato learn when and how to call the tool) andpermissionslists the capabilities (network,file-system) the sandbox must grant,run(input)→{ state, content }, whereinputis a JSON string of the params andstateisdoneorerror.
- Build a component targeting
wasm32-wasip2. - Drop the resulting
*.wasmintodata/tools/(mounted read-only into the harness at/tools). New files are picked up as new tools — no harness rebuild required.
Note
Rust is the reference, integrated path (it builds automatically with the
stack and derives the JSON Schema for you). The other languages below produce
a standalone component you place in data/tools/; they are supported by the
contract but exercised less, so treat the commands as starting points.
Rust tools live in the tools/ Cargo workspace and compile to components with
cargo-component. The
common crate provides a define_tool! macro that generates the Guest
implementation, derives the params' JSON Schema from
schemars, and exports the component.
1. Register the crate in tools/Cargo.toml:
[workspace]
members = ["common", "hello", "your-tool"]
resolver = "2"2. Declare it as a wasm component (tools/your-tool/Cargo.toml):
[package]
name = "your-tool"
version = "0.0.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
common = { path = "../common" }
wit-bindgen = "0.60.0"
schemars = "1.2.2"
serde_json = "1.0.151"
serde = { version = "1.0.229", features = ["derive"] }3. Implement it (tools/your-tool/src/lib.rs). Define your params (with a
doc comment per field — it becomes the schema description the model sees), a
run function, and hand them to define_tool! along with the capabilities the
tool needs (vec![] for none) and the host env vars to forward into its sandbox
(vec![] for none):
use schemars::JsonSchema;
use serde::Deserialize;
use common::define_tool;
#[derive(Deserialize, JsonSchema)]
struct HelloParams {
/// The value to greet.
suffix: String
}
fn run(input: HelloParams) -> ToolOutput {
ToolOutput {
state: ToolOutputState::Done,
content: format!("Hello {}", input.suffix)
}
}
define_tool!(
HelloTool, // name is snake_cased -> "hello_tool"
HelloParams,
"A simple hello-world tool returning hello + a string received as argument",
vec![], // capabilities, e.g. vec![Permission::Network, Permission::FileSystem]
vec![], // env vars to forward, e.g. vec!["BROWSERLESS_TOKEN".to_string()]
run
);ToolOutput, ToolOutputState, Permission and the Guest/export! glue all
come from the macro. The tool's registered name is the struct name in
snake_case, the vec![…] of Permissions becomes the permissions the harness
grants its sandbox, and the vec![…] of names becomes the env variables it
forwards. It builds automatically with make develop.
Use jco with ComponentizeJS.
npm install -g @bytecodealliance/jco @bytecodealliance/componentize-js
jco componentize tool.js --wit tool.wit --world-name tool-world --out tool.wasmtool.js implements the world's exports as named exports (the record becomes a
plain object; the enum becomes the string "done"/"error"):
export function info() {
return {
name: "my_tool",
description: "What it does and when to use it",
schema: JSON.stringify({
type: "object",
properties: { suffix: { type: "string", description: "The value to greet." } },
required: ["suffix"],
}),
permissions: [], // e.g. ["network", "file-system"]
env: [], // e.g. ["BROWSERLESS_TOKEN"]
};
}
export function run(input) {
const { suffix } = JSON.parse(input);
return { state: "done", content: `Hello ${suffix}` };
}Use componentize-py.
pip install componentize-py
# inspect the generated stubs for the exact function signatures:
componentize-py -d tool.wit -w tool-world bindings .
# build the component from your module (app.py):
componentize-py -d tool.wit -w tool-world componentize app -o tool.wasmapp.py implements the world's exported functions, returning the generated
ToolInfo / ToolOutput types:
import json
from tool_world import exports # names come from the generated bindings
class ToolWorld(exports.ToolWorld):
def info(self):
return exports.ToolInfo(
name="my_tool",
description="What it does and when to use it",
schema=json.dumps({
"type": "object",
"properties": {"suffix": {"type": "string"}},
"required": ["suffix"],
}),
permissions=[], # e.g. [exports.Permission.NETWORK, exports.Permission.FILE_SYSTEM]
env=[], # e.g. ["BROWSERLESS_TOKEN"]
)
def run(self, input: str):
params = json.loads(input)
return exports.ToolOutput(
state=exports.ToolOutputState.DONE,
content=f"Hello {params['suffix']}",
)Use TinyGo (≥ 0.33, which targets wasip2) with
wit-bindgen-go.
go install go.bytecodealliance.org/cmd/wit-bindgen-go@latest
wit-bindgen-go generate --world tool-world --out ./gen ./tool.wit
tinygo build -target=wasip2 --wit-package ./tool.wit --wit-world tool-world -o tool.wasm main.goIn main.go you assign the generated export hooks in init() and return the
generated record types:
package main
import (
"encoding/json"
"fmt"
tool "example.com/tool/gen/olivia/tools/tool-world"
)
func init() {
tool.Exports.Info = func() tool.ToolInfo {
return tool.ToolInfo{
Name: "my_tool",
Description: "What it does and when to use it",
Schema: `{"type":"object","properties":{"suffix":{"type":"string"}},"required":["suffix"]}`,
Permissions: nil, // e.g. []tool.Permission{tool.PermissionNetwork, tool.PermissionFileSystem}
Env: nil, // e.g. []string{"BROWSERLESS_TOKEN"}
}
}
tool.Exports.Run = func(input string) tool.ToolOutput {
var p struct{ Suffix string `json:"suffix"` }
_ = json.Unmarshal([]byte(input), &p)
return tool.ToolOutput{State: tool.ToolOutputStateDone, Content: fmt.Sprintf("Hello %s", p.Suffix)}
}
}
func main() {}Use wit-bindgen with the
wasi-sdk and
wasm-tools.
wit-bindgen c --world tool-world tool.wit # emits tool-world.c / .h
# implement the exported functions in tool.c, then:
clang --target=wasm32-wasip2 -mexec-model=reactor tool.c tool-world.c -o core.wasm
wasm-tools component new core.wasm -o tool.wasmYou implement the generated exports_*_info / exports_*_run functions,
filling the out-params with the tool_info / tool_output structs the header
declares.
The component model keeps growing: C#/.NET via
componentize-dotnet,
and others as their toolchains mature. Anything that emits a wasm32-wasip2
component implementing tool-world works — generate bindings from tool.wit,
implement info/run, build, and drop the .wasm in data/tools/.
Tools run with no ambient authority. Each grant is opted into by the tool's
declared permissions (see WIT as the tool contract):
ToolState::new() reads that list and hands the WASI context only what it names.
Granted to every tool:
- stdout / stderr — inherited, so a tool's logging surfaces in the harness logs.
Granted only when declared:
network→inherit_network()+ name resolution: raw outbound TCP and DNS, plus the linked-inwasmtime-wasi-httpfor HTTP. Declared by the networked tools (web_search,web,http,download,postgres_client,clickhouse_client,s3_client).file-system→ a per-session/sandboxdirectory, preopened read/write; the only writable path. Declared byfs,python,downloadands3_client, which use it to persist files and exchange data across steps (e.g.downloadwrites a file,pythonreads it,s3_clientuploads it). On the host each session lives under a private subdirectory (<sandbox>/<session_id>) but every tool sees it as/sandbox.- Named env vars — each name a tool lists in its
info().envis looked up in the harness's own environment and, if set, forwarded into the sandbox under the same name. A tool sees only the variables it named (e.g.webanddownloadrequestBROWSERLESS_TOKEN); nothing else from the host environment is visible.
Always denied: any capability a tool did not declare, any other filesystem
path, inbound sockets, process args, and any host env var the tool did not name
in its env list. Each invocation gets a fresh store, so no state leaks between
calls.
The threat model is simple: an LLM cannot be trusted to be correct, and it cannot be trusted with arbitrary execution. Olivia contains that with defense in depth.
Layer 1 — the model has no execution surface of its own. The developer
prompt makes it a strict router: zero internal logic, always delegate, and emit
only the JSON envelope. The one thing it can cause is "call tool X with
params Y", and the harness validates the tool name against the registry, so it
cannot invent a tool that isn't there.
Layer 2 — tools are sandboxed WebAssembly, deny-by-default. Every tool is a memory-isolated component holding only the capabilities listed above. There is no path from "clever prompt" to arbitrary code on the host or an un-granted resource.
Even in the worst case — the model is fully jailbroken and a tool has a bug — the damage is bounded to the small, enumerated capability set.
The stack runs the harness alongside a LiteLLM proxy, a local Ollama instance, the tools builder, and the backing services the tools reach: SearXNG, Browserless, Postgres, ClickHouse and a RustFS S3-compatible store. Startup order is enforced with healthchecks.
make develop # sources .env.develop, then docker compose up --buildThis mounts the source, enables debug logging, rebuilds on change via
cargo-watch, and injects an inline test configuration. Pull the Ollama
model(s) referenced by the LiteLLM config once:
docker compose exec ollama ollama pull gemma4:12b| Command | Description |
|---|---|
make develop |
Build and run the full stack for local development. |
make build |
Build the olivia/harness image. |
make bump <patch|minor|major> |
Rewrite the version in tracked manifests, commit, and tag. |
The tools Docker stage runs cargo component build --release --target=wasm32-wasip2 over the Rust workspace and copies every *.wasm into
/tools; in dev, cargo-watch rebuilds on change. Components authored in other
languages are dropped into data/tools/ directly.
| Variable | Used by | Description |
|---|---|---|
LITELLM_MASTER_KEY |
harness, litellm | Master key for the LiteLLM proxy. |
LITELLM_HOST |
harness | Base URL of the proxy (default http://litellm:4000). |
RUST_LOG |
harness | Log filter (e.g. info, debug). |
OLIVIA_ANTHROPIC_KEY |
litellm | API key for the anthropic/* models. |
OPENAI_API_KEY |
litellm | API key for the openai/* models. |
SEARXNG_TOKEN |
searxng | secret_key for the SearXNG instance. |
BROWSERLESS_TOKEN |
browserless, harness → web/download |
Auth token for Browserless; forwarded into the tools that declare it in info().env. |
POSTGRES_* / CLICKHOUSE_* / RUSTFS_* / S3_BUCKET |
stores | Credentials for the bundled data stores. |
A tool receives a host variable only if it names it in its info().env list (see
Capabilities & the sandbox). The harness looks each
declared name up in its own environment and, when set, forwards it into the
tool's sandbox under the same name — so set the plain variable (e.g.
BROWSERLESS_TOKEN) on the harness and the requesting tool picks it up.
Warning
Do not commit real secrets. Keep them in a local, git-ignored env file and
share a placeholder template (e.g. .env.develop.example) instead.
Released under the MIT License — © 2026 Pasquale Boemio.
Issues and pull requests are welcome. A good first contribution is a new tool
under tools/ (see Implementing a tool) or a guest
example in a language other than Rust.