Autonomous ReAct agent with multi-provider LLM support (EuLLM Engine, Ollama, OpenAI-compatible APIs, Anthropic), a small set of sandboxed tools, optional modules and a Telegram interface.
New in 0.3.0: the agent becomes a platform. eullm-agent api turns it
into a multi-tenant Core that other applications build on:
- The model proposes, the policy decides, a person approves. Every sensitive action stops in an approval queue (API, Telegram or terminal).
- Everything is on record. Runs, model calls, tools and fetches land in PostgreSQL with an append-only audit trail.
- Safe web access for apps.
POST /v1/fetchcrawls with SSRF checks on every redirect, size limits and a per-host pace. - Costs under control. Model router with fallback and pricing, budgets
per run, and monthly and daily limits per tenant (
429when reached).
Editor Mode, the first vertical built on the Core, uses it to run editorial work for any domain.
cargo build --release
cp config.example.yaml config.yaml # or run without a config for a wizard
./target/release/eullm-agent run "List the files in the workspace"
./target/release/eullm-agent serve # Telegram bot (needs telegram.allowed_users)provider.type |
Endpoint | Tool calling |
|---|---|---|
eullm |
{base_url}/v1/chat/completions (EuLLM Engine, OpenAI-compatible) |
yes |
ollama |
{base_url}/api/chat (Ollama native) |
yes |
openai |
{base_url}/chat/completions (default https://api.openai.com/v1) |
yes |
anthropic |
https://api.anthropic.com/v1/messages |
yes |
Secrets can be given as api_key / token or, preferably, as api_key_env /
token_env naming an environment variable. Every provider request has a
timeout (limits.llm_timeout_seconds) and is retried once on 429, 502, 503 or
504, honouring Retry-After.
Upgrading from 0.1.x: type: eullm now talks to the Engine's /v1 API,
because the Engine's /api/chat ignores tools. If you were pointing
type: eullm at a plain Ollama server, change it to type: ollama.
EuLLM Agent runs actions chosen by a language model, and a model can be steered by any text it reads (a web page, a file, a message). So every capability that touches the machine or the network is off by default and narrowly scoped when turned on. See config.example.yaml for every setting.
| Capability | Default | When enabled |
|---|---|---|
run_program (tools.exec) |
off | Only programs in allowed_programs, argv only (never a shell), run in the workspace with a clean environment, killed with their process group on timeout, output capped |
read_file, list_dir |
on | Confined to workspace: .., absolute paths outside it and symlinks leading out are refused |
write_file |
off (allow_write) |
Same confinement; never writes through a symlink; size capped |
fetch_url (tools.http) |
off | HTTPS GET only; every resolved address must be public (no loopback, LAN, link-local or cloud metadata); each redirect is checked again; the connection is pinned to the checked address; optional domain allowlist; body capped |
| Modules | off | Tools of modules an operator installed with eullm-agent module install; arguments are passed as single argv elements, file arguments are confined to the workspace. The agent cannot install modules |
| Telegram | needs config | Refuses to start without allowed_users; messages without a sender are ignored |
Each task is also bounded by max_iterations, limits.max_run_seconds and
limits.max_tool_output_bytes. The audit log (audit_log, JSONL, mode 0600)
records each run, model call (duration, tokens, errors) and tool call (name,
argument key names, outcome, duration); it never stores argument values,
prompts or outputs.
What this does not protect against: programs you allowlist run with the
agent's own user rights, so allowlisting an interpreter (sh, python, ...)
gives the model full control of that user. Run the agent as an unprivileged
user, ideally in a container.
eullm-agent api runs the agent as a service for other programs (for
example Editor Mode). The contract is
docs/openapi.json.
| Endpoint | What it does |
|---|---|
POST /v1/runs |
Start a run with a profile and an input; returns its id at once |
GET /v1/runs, GET /v1/runs/{id} |
Runs of the caller's tenant, with every model and tool call |
POST /v1/llm/chat |
One model call through the Model Router, recorded with tokens and cost |
GET /v1/approvals, POST /v1/approvals/{id} |
Actions waiting for a person; approve or deny |
POST /v1/fetch |
HTTP GET for applications (crawling, feeds) with the address checks of fetch_url, a size limit and a per-host pace; off unless api.fetch is set |
GET /v1/usage |
Runs, model calls, tokens, cost and fetches of the caller's tenant today and this month (UTC), with its limits; api.tenants sets the limits and a request over one gets 429 |
- Tokens:
eullm-agent token newprints a token and its SHA-256; only the hash goes inapi.tokens. Each token belongs to a tenant and can be limited to some profiles. Data is always scoped to the token's tenant. - State and audit: with
databaseconfigured, runs, model calls, tool calls, policy decisions and approvals are stored in thecoreschema of PostgreSQL (migrations run at start).core.audit_eventsis append-only: updates and deletes are refused by the database. Without a database the state is kept in memory. - Model Router and profiles:
modelsadds named models with fallback and pricing;profileschoose a model, the tools a run may use and its budget (max_tokens,max_cost,max_iterations,max_run_seconds). - Policy:
policy_file(see policy.example.yaml) decidesallow,denyorrequire_approvalper tool and profile. A run that has read external content is tainted, and from then on tools with side effects need approval: the model proposes, the policy authorises, the worker executes and the system records. - Approvals: a run stops at the action and waits for a decision through
the API, Telegram (
/approve <id>,/deny <id> [note], sent to the chat that started the task) or the terminal foreullm-agent run. No decision withinapi.approval_timeout_secondscounts as a refusal.
eullm-agent token new # put token_sha256 in api.tokens
DATABASE_URL=postgres://... eullm-agent api
curl -s -X POST localhost:8088/v1/runs -H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{"input":"List the workspace"}'- Start the Engine and note its address (default
http://localhost:11434) and, if it runs withEULLM_API_KEYS, one of the keys. - Check that the OpenAI-compatible API answers and that the model is loaded:
curl -s http://localhost:11434/v1/models -H "Authorization: Bearer $EULLM_API_KEY" - Configure the agent:
provider: type: eullm base_url: http://localhost:11434 model: qwen3:8b # a model with tool-calling support api_key_env: EULLM_API_KEY # omit if the Engine has no keys
- Verify tool calling end to end; the agent must call
list_dir:The progress lines showmkdir -p workspace && echo hello > workspace/note.txt RUST_LOG=eullm_agent=debug ./target/release/eullm-agent run \ "Which files are in the workspace? Read note.txt and tell me its content."
tool:list_dirandtool:read_file, the debug log showsPOST http://localhost:11434/v1/chat/completions, andeullm-agent-audit.jsonlgets atool_callentry per tool.
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test # unit, provider, agent and security regression tests
cargo test --test security # security regressions only
# PostgreSQL tests run when DATABASE_URL points at a disposable database:
DATABASE_URL=postgres://postgres@localhost/eullm_test cargo test --test pg_storeEuLLM Agent is licensed under AGPL-3.0-or-later. Use it, fork it, modify it, run it commercially: the one condition is copyleft. If you modify EuLLM Agent and let others use it over a network (including as a hosted service), you must offer them the Corresponding Source of your modified version. Versions published up to and including 0.1.7 remain available to everyone under their original Apache 2.0 terms; the AGPL governs new work from this change onwards.
I3K Technologies Srl holds the copyright and also offers this software under a separate commercial licence, for organisations that cannot accept the AGPL's terms. Enquiries: info@i3k.eu
Because the project is licensed both ways, contributions need a Contributor Licence Agreement: a one-line statement in your pull request. You keep the copyright in your own work; CLA.md explains exactly what it grants and why it is necessary. See CONTRIBUTING.md.