OpenClawn
Lightweight, safe, self-improving multi-role AI agent framework. Hybrid local + cloud LLMs with self-calibrating routing, decaying skills, and confidence-gated learning — 25 sandboxed tools for code, data, docs, git, and the web.
Lightweight, safe, self-improving multi-role AI agent framework. Hybrid local + cloud LLMs with self-calibrating routing, decaying skills, and confidence-gated learning — 25 sandboxed tools for code, data, docs, git, and the web.
Lightweight, safe, self-improving multi-role AI agent framework. Hybrid local + cloud LLMs with self-calibrating routing, decaying skills, and confidence-gated learning — 25 sandboxed tools for code, data, docs, git, and the web.
The Open-Source Control Plane for Trusted AI Agents.
Policy-before-dispatch, human-approval checkpoints, and immutable audit evidence — built in, not bolted on.
Policy-Before-Dispatch · Human-Approval Checkpoints · Immutable Audit Evidence
By 2026, the AI agent market shifted: multi-agent orchestration, tool calling, and RAG became table stakes — nearly every framework has them. What enterprises are actually buying now is governance: the ability to run an AI worker safely, with a paper trail, and stop it before it does something wrong. That gap is real — research on 2026 enterprise adoption found 72% of organizations already run agentic AI in production, but only 21% have a mature governance model for it.
OpenCLAWN is built to close exactly that gap. It's a control plane — the layer that decides whether an agent's action is allowed, that stops it for a human when the rule says so, and that proves after the fact what happened and why — sitting on top of the agent logic itself:
| What it does | How |
|---|---|
| Policy-Before-Dispatch | Every tool call carries requires_approval; nothing destructive runs before the policy check clears |
| Human-Approval Checkpoints | Approval is a blocking gate in the loop, not a log line after the fact — the agent stops and waits |
| Immutable Audit Evidence | Every routing decision, tool call, and skill promotion is logged before it happens and finalized after — a real paper trail, not best-effort logging |
Underneath that, 4 core innovations most agent frameworks skip make the control plane self-improving instead of static:
| Innovation | Problem Solved |
|---|---|
| Routing audit + self-calibration | No agent records why a routing decision was made or whether it was correct |
| Skill decay | Skill trees accumulate forever — stale skills pollute context |
| Confidence-gated crystallization | Self-evolving agents store skills from bad solutions |
| Role output contracts | Multi-agent handoffs without typed contracts are fragile |
Built on top of those, the agent compounds — the skill library tidies and improves itself as it's used, all gated and reversible:
| Capability | What it does |
|---|---|
| Multi-agent conversation | Pipeline / debate / orchestrator where roles hand off with validated contracts; live stop & interject |
| Task graph (DAG subtask) | Agent splits one goal into explicit, dependency-aware subtasks; independent ones run concurrently, each in its own isolated session — failure of one blocks only its dependents, not the whole graph |
| Skill compounding | Skills get promoted when proven, refined when corrected, and merged when duplicate (all versioned & revertible) |
| Autopilots | Scheduled agent runs — read-only; actions needing approval become proposals, never silent execution |
| Skill packs | Export/import skills between installs (Markdown + hash), imported as draft behind SSRF + injection guards |
| Activity timeline | Chronological view of every agent action across routing, tools, handoffs, conversations |
| Multilingual routing | Language-agnostic complexity signals + optional script-aware tier bump |
| Guardrails | NeMo-style input/output rails (native, no LangChain): block prompt-injection, block system-prompt leaks, redact PII — config-toggleable, fail-safe on |
Stack: Python 3.12 · FastAPI · HTMX · SQLite (aiosqlite) · Ollama + Gemini + Claude · httpx · Pydantic · structlog · tenacity
Those frameworks are good at what they do — orchestrating the agent loop, tool calling, multi-agent coordination. But a 2026 comparative analysis of the space put it plainly:
"None of them governs risky actions before they hit production — pair your pick with an agent control plane for policy, approvals, and audit... you still need policy-before-dispatch, explicit human approval states, and audit evidence." — LangGraph vs CrewAI vs AutoGen, 2026 enterprise comparison
That's precisely the gap OpenCLAWN's 4 core innovations close:
| Gap the analysis names | How OpenCLAWN answers it |
|---|---|
| No policy-before-dispatch | Every tool declares requires_approval; code_run can never bypass it (enforced at two independent points, not one) |
| No explicit human approval states | Approval is a blocking node in the agent loop (security/approval.py) — the agent waits, it doesn't just log and proceed |
| No audit evidence | Routing decisions are logged before the LLM call and finalized after (core/audit.py) — not best-effort, structured for replay |
This isn't a claim that OpenCLAWN is "better" at agent orchestration than those frameworks —
it's a narrower, more honest one: they don't ship a governance layer, and OpenCLAWN's core
design is one. "agent control plane"
is itself now an established market category — GitHub, Google, and Microsoft all shipped
products under that name in 2026 — and this is where OpenCLAWN sits, self-hosted and
open-source instead of a vendor platform.
What OpenCLAWN is not, to be equally direct: self-hosted single-instance is still the
only supported deployment model — no horizontal scaling, no managed multi-region offering.
Event-driven runtime (core/event_bus.py), multi-tenant schema (tenant_id), and OAuth/OIDC
login now exist, but multi-tenant isolation is honestly partial: chat_sessions and
skills are fully tenant-filtered, while a few other tables (memory_l1, memory_l2,
routing_events, approval_log) carry the column but aren't query-filtered yet — see
Scope & Production Posture below for exactly what's wired vs.
what's a foundation.
git clone https://github.com/MuhammadHasbiAshshiddieqy/OpenClawn.git
cd OpenClawn
# Recommended: uv with the committed lockfile (reproducible, identical to CI)
uv sync --frozen --extra dev
# Or with pip
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Create .env from example
cp .env.example .env
# Fill in keys: GEMINI_API_KEY and/or ANTHROPIC_API_KEY (heavy tiers)
# Local-only is fine too — Ollama handles light tiers without any key
# Run database migration
mkdir -p data
sqlite3 data/openclawn.db < migrations/001_initial.sql
# Pull Ollama models — one per local tier (or just gemma4:e4b to start)
ollama pull gemma4:e2b
ollama pull gemma4:e4b
ollama pull gemma4:12b
# Build sandbox image for code_run / shell_run
docker build -t openclawn-sandbox:latest -f Dockerfile.sandbox .
# Start the app
uvicorn web.main:app --reload --port 8000
Open http://localhost:8000 to chat · http://localhost:8000/metrics for the routing calibration dashboard.
graph TB
subgraph UI["Web UI — FastAPI + HTMX + SSE"]
CHAT["/chat/stream · single agent"]
CONVERSE["/converse/stream · multi-agent (+ /interject · /stop)"]
ACTIVITY["/activity · timeline + blockers"]
AUTOPAGE["/autopilots · scheduled runs"]
SKILLS["/skills · decay · curation · packs"]
METRICS["/metrics · calibration · telemetry"]
SETTINGS["/router · /settings · model map"]
CHATSESS_EP["/chat-sessions · history, resume, delete"]
LOGIN_EP["/login · opt-in session auth"]
end
subgraph CONVO["Multi-Agent Layer"]
ORCH["ConversationOrchestrator"]
PIPE["PipelineStrategy · PM → Dev → QA"]
DEBATE["DebateStrategy · round-robin"]
LEAD["OrchestratorStrategy · dynamic delegation"]
end
subgraph SCHED["Autopilots — scheduled, proposal-gated"]
SCHEDULER["AutopilotScheduler · asyncio loop"]
PROPOSAL["Approval-gated actions → proposals (never silent)"]
end
subgraph AGENT["Agent Loop — iterative, not recursive"]
direction TB
SHIELD["Guardrails (input) · injection scan"]
ROUTE["SmartRouter · soul-aware · multilingual"]
LLMCALL["LLM Client · stream + fallback"]
TOOLLOOP["Tool Loop · max 5 hops + loop guard"]
GROUT["Guardrails (output) · PII redact · leak block"]
POST["Post-Turn · memory + decay + compounding"]
end
subgraph MODULES["Core Modules — 4 innovations + compounding"]
AUDITOR["RoutingAuditor + Calibration · innovation #1"]
MEMORY["MemoryManager · L1–L4 + FTS5"]
DECAY["SkillDecay · innovation #2"]
CRYSTAL["Crystallizer (+ refine) · innovation #3"]
CONTRACTS["RoleNegotiator · innovation #4"]
CURATOR["SkillCurator · merge/dedup (I1)"]
FEEDBACK["SkillFeedback · promote/refine (I2/I3)"]
USERMODEL["UserModel · dialectic profile (I5, opt-in)"]
ACTIVITYMOD["ActivityTimeline · SkillPack"]
COMPACTOR["ContextCompactor · token budget"]
end
subgraph TOOLS["Tools — 28 total, all workspace-bounded"]
FS["Filesystem · read/write/edit/append/patch/glob/grep/list_dir · read_many"]
WORKDIR["set_workdir · switch active folder mid-chat, persists per session"]
EXEC["Execution · code_run · shell_run (both sandboxed)<br/>build_sandbox_image · per-project deps, network only at build"]
NET["Network · web_fetch · web_search · http_request (SSRF-guarded)"]
DATA["Data/docs · db_query · json_query · pdf_read · doc_write · pdf_write"]
DEVT["Dev/agent · git_status/diff/log · todo_write · report_blocker"]
end
subgraph SECURITY["Security"]
VAULT["Vault · API keys, never in context"]
APPROVAL["ApprovalGate · HITL + proposal queue"]
SHIELD2["Shield · injection scan · SSRF guard"]
GUARD["Guardrails (NeMo-style) · input + output rails<br/>injection · prompt-leak · PII redaction"]
AUTH["Auth · opt-in shared-secret session + CSRF"]
RATELIM["RateLimiter · sliding window per-session"]
end
subgraph INFRA["Infrastructure"]
DB["SQLite · aiosqlite · WAL · POWER()"]
SANDBOX["Docker Sandbox · network none · read-only · non-root"]
WORKSPACE["SessionWorkspaceStore · per-session working dir"]
CHATSESS["ChatSessionStore · history, resume, delete"]
BACKUP["infra/backup.py · SQLite Online Backup API"]
end
UI --> CONVO
UI --> AGENT
UI --> SCHED
CONVO --> AGENT
SCHED --> AGENT
SCHED --> PROPOSAL
AGENT --> MODULES
AGENT --> TOOLS
AGENT --> SECURITY
AGENT --> INFRA
UI --> AUTH
EXEC --> SANDBOX
WORKDIR --> WORKSPACE
MODULES --> DB
SECURITY --> DB
flowchart TD
U(["User sends message via Web UI"]) --> SHIELD
subgraph PRE["0 · Input Processing"]
SHIELD{"Guardrails — INPUT rails<br/>injection scan (Shield + NFKD)<br/>config-driven, fail-safe on"}
SHIELD -->|blocked| REJECT["Rejected"]
SHIELD -->|clean| CORRECT
CORRECT{"Check correction<br/>from previous turn?"}
CORRECT -->|"yes: had_correction=1"| RESOLVE
CORRECT -->|no| RESOLVE
RESOLVE["SkillFeedback.resolve_previous()<br/>success → revive + promote draft (I2)<br/>corrected → reset + refine skill (I3)"]
RESOLVE --> LOAD_SKILL
end
subgraph MEM["1 · Memory Loading"]
LOAD_SKILL["Load active skills<br/>SkillDecay: score > 0.3,<br/>max 8 + 1 draft trial (I2)"]
LOAD_SKILL --> LOAD_CTX["Load memory context<br/>L1: state · L2: facts · L3: skills<br/>L4: FTS5 archive · User profile (I5, opt-in)"]
end
subgraph BUILD["2 · Context Building"]
LOAD_CTX --> COMPACT["ContextCompactor.build()<br/>system + memory + history<br/>+ message, within budget"]
end
subgraph ROUTE["3 · Routing Decision"]
COMPACT --> DIMS["10 dimensions scored<br/>(+ has_code_signal · query_script)"]
DIMS --> SOUL{"soul.toml<br/>upgrade_kw hit?"}
SOUL -->|"yes: +3 score"| PREFER
SOUL -->|no| PREFER
PREFER{"prefer_local?"}
PREFER -->|"yes: threshold +1<br/>stay local longer"| LANG
PREFER -->|"no: normal threshold"| LANG
LANG{"language bump?<br/>(opt-in)"}
LANG -->|"script outside local<br/>threshold -1: bump tier"| LABEL
LANG -->|"local script / off"| LABEL
LABEL["Complexity label (+ calibration offset)<br/>TRIVIAL → SIMPLE → MODERATE<br/>→ COMPLEX → CRITICAL"]
LABEL --> OVERRIDE{"/settings<br/>override active?"}
OVERRIDE -->|yes| USE_OV["Use chosen model<br/>(audit still logs router decision)"]
OVERRIDE -->|no| USE_ROUTE["Use router model<br/>(/router tier→model map)"]
end
subgraph AUDIT1["4 · Pre-Call Audit — innovation #1"]
USE_OV --> LOG["Auditor.log_decision()<br/>10 dims + score + label<br/>+ model + reason → DB"]
USE_ROUTE --> LOG
end
subgraph LLM["5 · LLM Call with Fallback"]
LOG --> STREAM["LLMClient.stream_with_fallback()"]
STREAM --> HEALTH{"Ollama health check"}
HEALTH -->|up| PRIMARY["Try primary model"]
HEALTH -->|down| FALL["Fallback chain"]
PRIMARY -->|error| FALL
FALL --> F1["1 · gemma4:e4b (local)"]
F1 -->|error| F2["2 · deepseek-r1 (local)"]
F2 -->|error| F3["3 · qwen3.5:9b (local)"]
F3 -->|error| F4["4 · gemini-2.5-flash (cloud)"]
F4 -->|error| FAIL["ProviderUnavailable"]
end
subgraph TOOL_LOOP["6 · Iterative Tool Loop — max 5 hops"]
PRIMARY --> PARSE{"Tool call in stream?"}
FALL --> PARSE
PARSE -->|"tool_call found"| LOOPGUARD
PARSE -->|"text only"| YIELD["Yield text to user"]
YIELD --> DONE_CHECK{"Another tool call?"}
DONE_CHECK -->|no| GUARD_OUT
LOOPGUARD{"Same call<br/>repeated 2×?"}
LOOPGUARD -->|yes| HALT["Loop halted<br/>(hard break)"]
LOOPGUARD -->|no| ALLOWED
HALT --> GUARD_OUT
ALLOWED{"Role allowed?"}
ALLOWED -->|no| ERR2["Tool denied"]
ALLOWED -->|yes| APPROVAL{"requires_approval?"}
APPROVAL -->|no| RUN_TOOL["Run tool"]
APPROVAL -->|yes| AUTOMODE{"autopilot mode?"}
AUTOMODE -->|"yes: queue proposal<br/>(no silent execution)"| TOOL_RESULT
AUTOMODE -->|no| HITL{"User approves?"}
HITL -->|reject/timeout| ERR3["Approval denied"]
HITL -->|approve| RUN_TOOL
ERR2 --> TOOL_RESULT
ERR3 --> TOOL_RESULT
RUN_TOOL --> TOOL_RESULT["Result → append to messages"]
TOOL_RESULT --> HOP{"hop < 5?"}
HOP -->|yes| PRIMARY
HOP -->|no| GUARD_OUT
end
subgraph POST["7 · Post-Turn Processing — throttled, non-blocking"]
GUARD_OUT{"Guardrails — OUTPUT rails<br/>on full turn.content<br/>before it is stored"}
GUARD_OUT -->|"prompt-leak"| GBLOCK["Block: replace with safe message"]
GUARD_OUT -->|"PII match"| GREDACT["Redact → [REDACTED]"]
GUARD_OUT -->|clean| FINALIZE
GBLOCK --> FINALIZE
GREDACT --> FINALIZE
FINALIZE["Auditor.finalize()<br/>tokens, cost, latency → DB"]
FINALIZE --> WRITE_MEM["MemoryManager<br/>L1 checkpoint · L4 archive (if threshold)"]
WRITE_MEM --> DECAY_PASS["SkillDecay.maybe_run_decay_pass()<br/>throttled: 1×/hour"]
DECAY_PASS --> RECORD["SkillFeedback.record_usage()<br/>(skills used this turn → next-turn outcome)"]
RECORD --> CURATE["SkillCurator.maybe_run_curation_pass() (I1)<br/>merge duplicates · judge ≥4 · revertible"]
CURATE --> AUTOTUNE["Calibration.maybe_auto_apply() (I4)<br/>opt-in · clamp ±1 · revertible"]
AUTOTUNE --> USERMOD["UserModel.maybe_update() (I5)<br/>opt-in · versioned"]
USERMOD --> CRYST_CHECK{"Crystallizer<br/>should_attempt?<br/>(≥3 tool calls)"}
CRYST_CHECK -->|yes| SELF_EVAL["Self-evaluate<br/>evaluator ≥ generator<br/>confidence 1–5"]
SELF_EVAL --> STORE{"conf ≥ 4 AND<br/>no critical gaps?"}
STORE -->|yes| ACTIVE["Store as active skill"]
STORE -->|no| DRAFT["Store as draft<br/>(not auto-injected)"]
CRYST_CHECK -->|no| DONE
ACTIVE --> DONE
DRAFT --> DONE
DONE(["Turn complete"])
end
style REJECT fill:#f66,stroke:#900,color:#fff
style FAIL fill:#f66,stroke:#900,color:#fff
style HALT fill:#f66,stroke:#900,color:#fff
style GBLOCK fill:#f66,stroke:#900,color:#fff
style GREDACT fill:#ff6,stroke:#990
style ACTIVE fill:#6f6,stroke:#090
style DRAFT fill:#ff6,stroke:#990
style DONE fill:#6cf,stroke:#069
All 29 tools are workspace-bounded — every file path is resolved with Path.resolve()
and rejected if it escapes the workspace root (defeats ../ and symlink escape). Tools that
mutate state or run code require explicit approval.
Around the whole turn sit guardrails (NeMo-style, native — no LangChain): input rails scan the user message for prompt-injection before the pipeline runs; output rails check the full response before it is stored — blocking system-prompt leaks and redacting PII (email, card, API key). Each rail is config-toggleable and fail-safe on (corrupt/missing config → all rails active).
flowchart LR
subgraph GUARDRAILS["Guardrails — NeMo-style rails (config-driven, fail-safe on)"]
direction TB
GIN["INPUT: prompt-injection → block"]
GOUT["OUTPUT: prompt-leak → block · PII → redact"]
end
subgraph SAFE["No approval — read-only / inspect / internal / sandboxed"]
direction TB
R1["file_read · read_many · list_dir · glob · grep"]
R2["web_fetch · web_search · pdf_read (SSRF-guarded net)"]
R3["memory_search · json_query · git_status/diff/log"]
R4["todo_write · report_blocker (internal tables)"]
R5["set_workdir (session-scoped, not filesystem-destructive)"]
R6["shell_run — sandboxed, not approval-gated<br/>(container isolation is the real boundary, §17)"]
end
subgraph GATED["Requires approval — mutate / execute / reach out"]
direction TB
G1["file_write · file_edit · file_append · apply_patch"]
G2["code_run — ALWAYS gated, never bypassable (CLAUDE.md §1)"]
G3["http_request (SSRF-guarded) · db_query (SELECT-only)"]
G4["doc_write · pdf_write"]
end
subgraph APPROVAL_GATE["ApprovalGate"]
AG["Interactive: wait for user<br/>timeout 120s · fail-safe deny"]
AGP["Autopilot: queue as proposal<br/>(never silent execution)"]
end
subgraph SANDBOX["Docker Sandbox — code_run AND shell_run"]
direction TB
S1["network none"]
S2["read-only filesystem"]
S3["non-root user"]
S4["memory 256m · cpus 0.5"]
S5["timeout 30s · no-new-privileges"]
end
GIN --> SAFE
GIN --> GATED
GATED --> AG
GATED -.autopilot.-> AGP
G2 --> SANDBOX
R6 --> SANDBOX
SAFE --> GOUT
AG --> GOUT
Security note:
code_runandshell_runnever execute on the host — both run inside the Docker sandbox. If Docker is unavailable, they fail safe (return an error) rather than falling back to host execution. Onlycode_runrequires human approval —shell_rundoesn't, because the sandbox (not the approval click) is the real security boundary for both;code_runstays gated regardless because arbitrary code execution is treated as strictly higher-risk than a shell command, and that gate can never be bypassed (not even by trust mode).db_queryis SELECT-only.web_fetch/http_requestpass an anti-SSRF guard (reject loopback, private, link-local incl. cloud metadata). In autopilot mode, approval-gated tools are queued as proposals for later review — never run unattended. Guardrails wrap the turn: input rails block injection before the pipeline; output rails run on the full response before storage — blocking system-prompt leaks and redacting PII so it never reaches stored memory (L1/L4). Note: tokens already streamed can't be unsent — output rails operate on the completeturn.contentto keep PII out of storage and flag the UI.
flowchart LR
subgraph TURN["One Agent Turn"]
T1["Audit: log<br/>routing decision"] --> T2["Route: soul-aware<br/>10-dim scoring"]
T2 --> T3["LLM call<br/>+ tool loop"]
T3 --> T4["Audit: finalize<br/>tokens / cost / latency"]
T4 --> T5["Decay pass<br/>(throttled)"]
T5 --> T6["Crystallize<br/>(confidence-gated)"]
end
I1["#1 · Routing Audit<br/>+ Self-Calibration<br/><i>pre-call log + post-correct</i>"] -.-> T1
I1 -.-> T4
I2["#2 · Skill Decay<br/><i>exponential + throttle</i>"] -.-> T5
I3["#3 · Confidence-Gated<br/>Crystallization<br/><i>eval ≥ generator</i>"] -.-> T6
I4["#4 · Role Output<br/>Contracts<br/><i>Pydantic validated</i>"] -.-> T3
C["Compounding (builds on #1–#3)<br/><i>I1 merge · I2 promote · I3 refine · I4 auto-tune · I5 profile</i>"] -.-> T5
C -.-> T6
Beyond single-agent turns, roles can talk to each other. One orchestrator loop drives three pluggable strategies; each turn is a full agent run (routing, tools, memory all intact). You can stop mid-conversation or interject with your own message, counted on the next turn.
flowchart TD
START(["User message + mode"]) --> STRAT{"Strategy"}
STRAT -->|Pipeline| P["PM → Dev → QA<br/>sequential, contract-validated handoff"]
STRAT -->|Debate| D["Round-robin, N rounds<br/>full transcript shared each turn"]
STRAT -->|Orchestrator| O["Lead delegates dynamically<br/>via JSON directive each turn"]
O --> ODYN{"Directive<br/>parseable?"}
ODYN -->|yes| OWORK["Route to chosen worker"]
ODYN -->|no| OFALL["Fallback: lead → all workers → synthesis"]
P --> NEXT{"next_speaker()"}
D --> NEXT
OWORK --> NEXT
OFALL --> NEXT
NEXT -->|role| RUN["Run AgentLoop for that role<br/>(cooperative stop check between tokens)"]
RUN --> CONTRACT{"wants_contract?"}
CONTRACT -->|yes, valid| REC["Record handoff · validation_ok=1"]
CONTRACT -->|yes, invalid| DEG["Degrade: keep raw text<br/>validation_ok=0, continue"]
CONTRACT -->|no| LOOP
REC --> LOOP
DEG --> LOOP
LOOP{"stopped OR<br/>max_turns OR<br/>strategy done?"}
LOOP -->|no| NEXT
LOOP -->|yes| END(["conversation_end"])
NEXT -->|none| END
style END fill:#6cf,stroke:#069
style DEG fill:#ff6,stroke:#990
Every routing decision is logged before the LLM call with 10 dimensions (token count, tech keywords, soul upgrade hits, a language-agnostic code signal, detected script, etc.) and updated after with latency, cost, and correction signals. The /metrics dashboard shows which complexity labels have the highest correction rate — letting you tune the router with real data.
Skills age with exponential decay (score × 0.97^days_since_used). Unused skills drop below 0.3 and get archived. A revived skill recovers score immediately. Decay runs throttled (max once per hour) so it never blocks a turn.
After a successful multi-step task, the agent evaluates its own solution using a model at least as capable as the generator (EVALUATOR_FOR map: e4b→12b, Sonnet→Sonnet). Solutions with confidence < 4/5 or critical gaps are stored as draft, not active, and never injected into future context automatically.
Handoffs between roles (PM → QA → Dev) use Pydantic models as typed contracts. Invalid output is stored with validation_ok=0 for debugging — no crash, no silent data loss.
The router scores 10 dimensions, then maps a complexity label to a model. Light tiers stay local (Ollama, free, private); heavy tiers escalate to a cloud model. The exact mapping is configurable. Local tiers are ordered by model capacity (harder case → more capable model); heavy tiers go to the cloud. The shipped default:
Query complexity → model selection:
TRIVIAL → gemma4:e4b (Ollama · local, lightest)
SIMPLE → deepseek-r1 (Ollama · local, reasoning)
MODERATE → qwen3.5:9b (Ollama · local, most capable)
COMPLEX → gemini-2.5-flash (cloud) # or claude-haiku-4-5
CRITICAL → gemini-2.5-pro (cloud) # or claude-sonnet-4-6
Cloud tiers are pluggable: point them at Gemini or Claude depending on the API key you
provide. The shipped default routes heavy tiers to Gemini; swap to Claude in core/router.py if
you prefer. Local tiers are easy to remap too — just edit the MODELS dict.
The router is soul-aware: each role's soul.toml can define upgrade_keywords that force
higher complexity, and prefer_local=true to resist escalating to the cloud. Soul upgrade
keywords override prefer_local — the soul has higher priority.
If Ollama is offline, the client falls back down the chain automatically
(gemma4:e4b → deepseek-r1 → qwen3.5:9b → gemini-2.5-flash). Every fallback is logged to the
audit DB.
openclawn/
├── core/ # agent_loop · llm_client · router (multilingual) + router_config
│ # audit · calibration · crystallizer · compactor
│ # conversation (multi-agent) · event_bus (event-driven runtime)
│ # activity (timeline) · autopilot (scheduler) · skill_pack · tool_audit
│ # mcp_client · mcp_registry · guardrails_config · prometheus_metrics
├── infra/ # config · database (WAL, POWER(), multi-tenant migration) · logging
│ # env · workspace · users (RBAC) · chat_sessions · settings
│ # manifest (clawn.yaml) · backup
├── memory/ # layers (L1–L4) · skill_decay · curator (merge) · skill_feedback
│ # user_model · search (FTS5)
├── roles/ # pm/qa/dev/data/security soul.toml · contracts (Pydantic) · registry
├── tools/ # 29 tools: file_ops · read_many · search · shell · code · sandbox_image · web · git
│ # document (pdf_read · doc_write · pdf_write) · todo · report_blocker
│ # mcp_tool (external MCP servers)
├── security/ # vault (+ encryption-at-rest) · shield (NFKD)
│ # guardrails (NeMo-style rails) · approval (HITL, per-user)
│ # policy_engine · auth · oidc · rate_limit · question · skill_scanner
├── web/ # FastAPI app · HTMX templates · SSE · /activity /autopilots /skills
├── migrations/ # 001_initial.sql · 002_multi_tenant.sql
└── tests/ # 850+ tests — innovations, tools, web, compounding, guardrails, RBAC
The 4 core innovations are stable; everything above (multi-agent, autopilots, skill compounding, skill packs) builds on them. See CHANGELOG.md for the full feature history.
(structure continued — key runtime pages)
/ chat · single & multi-agent modes
/activity timeline of agent actions + open blockers
/autopilots scheduled runs + pending proposals
/skills decay curves · crystallization · curation · skill packs
/metrics routing calibration · tool telemetry (+ /metrics/prometheus)
/conversations multi-agent transcripts
/router · /settings tier→model map · model override (admin-only when auth is active)
/mcp external MCP server registry (admin-only)
/admin/users RBAC role management (admin-only)
/evidence/{id} · /feedback/{id} audit evidence lookup · human feedback rating
pytest tests/ -v
All tests use in-memory SQLite and mocked LLM calls — no real Ollama, Gemini, or Claude API needed.
Detailed reference for every module, class, and function:
| Folder | Doc |
|---|---|
infra/ |
docs/infra.md — config, database, logging, users (RBAC), chat sessions, manifest |
core/ |
docs/core.md — agent loop, LLM client, router (multilingual), audit, crystallizer, calibration, conversation, activity, autopilot, skill packs, event bus, MCP client/registry |
memory/ |
docs/memory.md — L1–L4 layers, skill decay, curator (merge), skill feedback (promote/refine), user model, FTS5 search |
roles/ |
docs/roles.md — contracts, role registry, soul.toml format |
security/ |
docs/security.md — vault (+ encryption-at-rest), shield, guardrails (NeMo-style rails), approval gate HITL (per-user), policy engine, auth, OIDC, rate limiting, skill scanner |
tools/ |
docs/tools.md — 29 tools, permission matrix, Docker sandbox |
web/ |
docs/web.md — FastAPI endpoints, SSE streaming |
| Database | docs/database.md — full schema + example queries |
| Tests | docs/tests.md — test index + patterns |
| Postgres migration | docs/postgres-migration.md — SQLite→PostgreSQL path, if/when it's needed |
| OpenConnector integration | docs/tools.md — connect 1000+ SaaS providers via MCP |
| Architecture spec | openclawn-core-spec-v0.4.md — full architecture reference, kept in sync with the built system (superseding v0.3, the original greenfield blueprint) |
| Sprint | Focus | Status |
|---|---|---|
| 0 | Infra · LLM client · Agent loop · Web UI · Audit | Done |
| 1 | Soul-aware router · Memory L1–L4 · Compactor + caching | Done |
| 2 | Tools · Docker sandbox · Crystallizer · Skill decay | Done |
| 3 | Role contracts · Vault · Shield · ApprovalGate (HITL) | Done |
| 4 | Coverage · Calibration advisor · (router tuning needs live data) | Ongoing |
| 5 | Multi-agent conversation · Gemini provider · UI redesign | Done |
| 5+ | Tooling to 26 (git · todo · docs · pdf · blocker) · SSRF guard · CI + uv.lock | Done |
| 5++ | Autopilots (scheduled, proposal-gated) · Activity timeline · Skill packs | Done |
| 6–8 | Compounding intelligence: skill curator · draft promotion · refine · guarded auto-apply · user model | Done |
| — | Multilingual routing (structural + script-aware signals) | Done |
| — | MCP client (external tools, approval-gated) · /health · stale-draft cleanup |
Done |
| — | Guardrails (NeMo-style input/output rails: injection · prompt-leak · PII redaction) | Done |
| P3 | Policy Engine (soul.toml conditions, deny/require-approval) · clawn.yaml manifest |
Done |
| P4 | Event-driven runtime (core/event_bus.py, lightweight event-sourcing) |
Done |
| P5 | Multi-tenant schema foundation · OAuth2/OIDC login · multi-user RBAC (admin/member/viewer) | Done |
| P6 | Cross-role skill marketplace · Prometheus metrics endpoint · OpenConnector integration | Done |
| P7 | Production-readiness audit: cross-user IDOR (chat/approvals) · XSS · missing RBAC gates · encryption-at-rest for MCP credentials · ReDoS mitigation · non-root container · cookie Secure flag |
Done |
code_run and shell_run only run inside Docker (network none, read-only, non-root, timeout); they never touch the host. Web tools have an anti-SSRF guard; autopilots never execute approval-gated actions (they queue proposals). Input/output guardrails (NeMo-style, native) wrap every turn — fail-safe onhttpx for all LLM calls, intentional for audit transparencyskill_decay, audit, crystallizer, contracts, curator, activity, guardrails have clean interfacesOpenCLAWN targets self-hosted deployment — one operator running one instance, not a
managed multi-region SaaS platform. Within that constraint, it has grown past "single
shared login, one workspace": genuine multi-user access control and a multi-tenant schema
foundation now exist, each added because a concrete internal need justified it (see
CLAUDE.md §7 for the explicit approval trail behind each), not speculatively.
| Capability | Status |
|---|---|
| Multi-user accounts with RBAC | Implemented — users table, admin/member/viewer roles. System-config endpoints (/settings, /admin/users, /router, /mcp/*, /skills/import, calibration apply/revert, skill visibility) are admin-only. Shared-secret login still works unchanged and always bootstraps as admin — this isn't a breaking change for existing single-secret deployments |
| OAuth2/OIDC login | Implemented, opt-in and additional to shared-secret (not a replacement) — any standards-compliant provider via .well-known/openid-configuration discovery |
Multi-tenancy (tenant_id) |
Partial, honestly. chat_sessions and skills are fully tenant-filtered (proof of concept, including defense-in-depth on single-ID operations). memory_l1, memory_l2, routing_events, and approval_log carry the tenant_id column but queries against them aren't filtered by it yet — that's a separate follow-up, not an oversight. Not a finished multi-tenant product, but no longer "one workspace, one user" either |
| PostgreSQL / horizontal scaling | Still out of scope. SQLite (WAL) remains the default — data sovereignty for a single self-hosted org. A migration path is documented (docs/postgres-migration.md) but nothing is driving the switch yet; adopting it without a real need would be scope creep |
Chat/approval ownership is also per-user, not just per-tenant: each chat session and
pending approval is tied to the user who created it (owner_user_id), so one logged-in
user can't read another's chat history or — more importantly — approve/reject another
user's pending destructive actions. This wasn't always true; see
CHANGELOG.md § 0.12.0 for the IDOR this closed.
By default (OPENCLAWN_AUTH_TOKEN unset) OpenCLAWN runs with no login — correct for
localhost or a VPN/Tailscale overlay, where the network boundary is the access control.
If you expose it on a public IP, enable the built-in hardening first:
OPENCLAWN_AUTH_TOKEN in .env (see .env.example) — a single shared password
gate. Requests without a valid session are redirected to /login; no session state is
kept server-side (signed cookie, HMAC-SHA256, pure stdlib — no new dependency).Caddyfile.example (auto Let's Encrypt).
Never bind uvicorn directly to a public IP without TLS; credentials and chat content
would travel in plaintext.OPENCLAWN_AUTH_TOKEN is set — every POST form
carries a signed token validated server-side./chat/stream and /converse/stream (in-memory
sliding window, no Redis needed — single-process is enough for one user)./health now also reports Ollama reachability, which cloud API keys are configured,
and whether auth is enabled — wire it into your process manager or docker-compose
healthcheck (already configured in docker-compose.yml).data/openclawn.db has no automatic backup by
default. Use scripts/backup_db.py (wraps SQLite's Online Backup API, safe to run while
the server is live under WAL mode):python scripts/backup_db.py --keep 14 # backup now, keep the 14 newest
python scripts/backup_db.py --list # show existing backups
Schedule it with cron (0 3 * * * cd /path/to/openclawn && .venv/bin/python scripts/backup_db.py --keep 14) or a systemd timer — an example unit is documented at the
bottom of the script. Restore is a straight file copy: stop the server, replace
data/openclawn.db with the chosen backup file, restart.core/audit_chain.py) makes retroactive edits detectable, not
impossible, on its own: truncating the last entry or rewriting the whole chain with
consistent hashes both pass GET /audit/verify unaided. Closing that gap needs a
snapshot recorded somewhere the chain itself doesn't control. Use
scripts/anchor_audit_chain.py (writes to a separate JSONL file, data/audit_anchors.jsonl
by default) alongside your backup cron:python scripts/anchor_audit_chain.py # record a new anchor if there's new activity
python scripts/anchor_audit_chain.py --verify # exits 1 if any anchor no longer matches
This alone only raises the bar (an attacker with full filesystem access could still edit
both files consistently) — it becomes a real independent anchor only once you copy that
file off-host periodically, the same way you'd copy a database backup. docs/core.md §
core/audit_anchor.py documents this limit in full; don't oversell it as making the chain
immutable.This hardening still doesn't turn OpenCLAWN into a managed multi-tenant SaaS platform — one self-hosted instance can now serve multiple accounts with real access control, but it's still one deployment, one operator responsible for it, not a horizontally-scaled service with per-tenant billing/isolation guarantees. It closes the gap between "safe on a trusted network" and "safe to expose on the open internet" for that operator and their users.
What "production-ready" means here (for self-hosted, operator-run deployment):
reliable for its users, safely reachable from the internet if you choose to, with real
access control instead of an all-or-nothing shared secret. That posture is met —
Docker-sandboxed execution, SSRF guard, HITL approval scoped per-user, fail-safe error
handling, CI on every push, opt-in auth (shared-secret or OIDC) + CSRF + rate limiting +
RBAC, a dependency-aware /health endpoint, custom error pages (no leaked stack traces),
and stale draft-skill cleanup. Remaining polish is tracked in CHANGELOG.md.
Common review misread: OpenCLAWN is not an under-built SaaS product pretending to be more than it is.
shell_runandcode_runrun only in the Docker sandbox (never on the host); the DB is never served statically; there are noexcept: passswallows; CI exists. Evaluate it as a self-hosted control plane with real (if intentionally scoped) multi-user support — not a horizontally-scaled managed SaaS.
OpenConnector by oomol-lab, licensed under the Apache License 2.0. An open-source auth gateway connecting 1,000+ SaaS providers (GitHub, Gmail, Notion, Slack, and more) to AI agents via MCP, HTTP/OpenAPI, and SDK.
OpenCLAWN does not vendor or fork OpenConnector's code — it runs as an
independent Docker service (docker-compose.yml, opt-in connector profile)
and is connected purely as an external MCP server through the existing
MCPRegistry/MCPTool integration, same as any other MCP tool (always
requires_approval=True, no special-cased trust). See
docs/tools.md § Integrasi OpenConnector
for setup steps and Caddyfile.example for exposing its
dashboard alongside OpenCLAWN in a self-hosted deployment.
Full credit to the OOMOL/oomol-lab team for OpenConnector. Provider names and trademarks referenced through it belong to their respective owners.
MIT — see LICENSE