An agentic AI tool in C#. One agent loop, many model providers, pluggable tools.
Names. Morat is the product (from the Hebrew morah, “teacher”). Cherub is its guardian system — the virtual filesystem and the web handler that keep file and network access safe. Pre-release builds (v0.1–v1.0) carry the angelic codename angel. The command and binary are
morat; internal identifiers (namespaces, the.cherubdirectory,CHERUB_*variables) keep the Cherub name.
From a checkout, in PowerShell:
.\build.ps1 # test + publish to .\artifacts\publish
.\install.ps1 # elevates, ensures the .NET runtime, copies to C:\Program Files\Morat, adds it to PATHOpen a new terminal and run morat setup. .\uninstall.ps1 removes it. For a build that needs
no .NET runtime installed, use .\build.ps1 -SelfContained then .\install.ps1 -SkipRuntime.
dotnet run --project src/Cherub.Cli -c ReleaseThe first run has no config.cfg, so Cherub asks how you want to run — provider, model, and
how to authenticate — and writes your answers. No API key is entered here; you give the
name of the environment variable that holds it. Re-run it any time with morat setup.
Or write config.cfg yourself (copy config.cfg.example):
Any setting can be overridden for a single run with the matching environment variable
(CHERUB_PROVIDER, CHERUB_MODEL, CHERUB_BASE_URL, CHERUB_EFFORT, …), which takes
precedence over the files.
API keys are never stored in config. Either export the provider's own variable
(ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, MISTRAL_API_KEY,
OPENROUTER_API_KEY), or name your own with "apiKeyEnv": "MY_KEY" and export that. Ollama
and llama.cpp need no key.
Cherub never uses a credential you didn't give it. By default it uses only an API key you
point it at (ANTHROPIC_API_KEY, CHERUB_API_KEY, or the variable named by apiKeyEnv), and
if none is set it stops at startup with a clear message — it will not silently use your
ant auth login profile or spend against your account.
If you want Cherub to use an Anthropic CLI login (Claude Code or ant auth login),
federation, or an ambient token, opt in with "anthropicAuth": "cli" in config.cfg. Check
what will actually be used with:
morat auth| File | Audience | Edited by | Format |
|---|---|---|---|
config.cfg |
you | you | JSONX — JSONC + JSONL |
.cherub/env.cfg |
morat and the agent | generated | flat KEY=VALUE |
JSONX means comments and trailing commas are fine, and you can append further objects one per line — later objects override earlier ones key by key, so a local tweak is one appended line rather than an edit:
{ "provider": "anthropic", "effort": "high" }
{ "effort": "low" } // today only.cherub/env.cfg is generated from config.cfg and holds nothing that isn't derivable from
it, so it is always safe to delete. Cherub regenerates it at startup when it is missing,
corrupt, or out of date; to do it yourself:
morat repair-env| Command | Does |
|---|---|
morat |
Start the interactive session. |
morat auth |
Report which credential will be used, and flag common traps. |
morat repair-env |
Rebuild .cherub/env.cfg from config.cfg. |
| Project | Contains |
|---|---|
Cherub.Core |
Agent loop, transcript, tool contracts, prompts. No dependencies. |
Cherub.Anthropic |
IAgentModel over the Claude Messages API. |
Cherub.OpenAI |
IAgentModel over the OpenAI chat-completions protocol. |
Cherub.Shomer |
The watchman — consent before anything on your machine changes. |
Cherub.Tools |
Built-in tools. |
Cherub.Vfs |
Virtual filesystem, search, the change journal, path redaction. |
Cherub.Config |
JSONX parsing, env.cfg generation, settings resolution. |
Cherub.Tui |
Terminal rendering behind the IAgentConsole seam. |
Cherub.Cli |
Composition root and command dispatch. |
Anthropic uses its native API. Every other supported service speaks the OpenAI
chat-completions protocol, so one implementation covers OpenAI, Gemini's compatibility
endpoint, Mistral, OpenRouter, Ollama, and llama.cpp. Adding another compatible service is a
preset in OpenAIProviders, not a new project.
Only Anthropic has a default model id — for the others, set model. Guessing at another
vendor's catalogue would be the same fabrication the agent's own system prompt forbids.
| Tool | Does | Consent |
|---|---|---|
http_fetch |
Retrieve a URL | — |
fs_list |
List a virtual directory | — |
fs_glob |
Find files by name | — |
fs_grep |
Search file contents | — |
fs_read |
Read a file, plus its revision mark | — |
fs_edit |
Replace an exact piece of a file | on /accept |
fs_write |
Write a whole file | on /accept |
shell_run |
Run a command (off by default) | on approval |
http_fetch retrieves URLs through a built-in HttpClient — no dependency on curl or
wget being installed. It caps the response size, follows redirects with each hop
re-validated, and refuses hosts that resolve into the local network (loopback, RFC1918,
link-local including the cloud metadata endpoint) unless http.allowPrivateNetwork is set.
The agent picks these URLs, so they are treated as untrusted input.
Add a tool by implementing ITool, or wrap a lambda with DelegateTool, and register it in
the ToolRegistry. If it changes anything, implement IWarded and wrap it in WardedTool.
The agent edits a likeness of your project, not your project. Its writes land in a working copy — it reads and searches through that copy, so its own changes are there when it looks for them — and nothing reaches your files until you have read the diff and accepted it.
› refactor the config loader
2 files changed in the working copy (+31 -18) — /diff to review, /accept to apply.
› /diff
› /accept
applied 2 files to the project.
| Command | Does |
|---|---|
/diff [path] |
Review the change as a unified diff |
/accept [path] |
Apply it — all of it, or one file |
/reject [path] |
Discard it; the project was never touched |
It is an overlay rather than an eager copy, for the same reason a virtual DOM is not a copy of the document: a copy costs the whole tree at startup and goes stale the moment you edit a file in your own editor. Reading through to the real project keeps the two in step — and means that if a file did change underneath, accepting reports a conflict and leaves it alone instead of overwriting work nobody reviewed. See demut.md.
One honest caveat: shell_run executes against the real project, so a build will not see staged
edits until you accept them. You are warned at the prompt when that matters.
Nothing on your machine changes without you saying so. Reads run silently; a command is put
to you with a preview of what it will do, and you can allow it once, allow that kind of call for
the session, or refuse. A prompt nobody answers declines — walking away from a running agent is
safe. Standing grants are per program, so trusting git is not trusting rm. (Turn the working
copy off with workspace.staged: false and file writes are put to you the same way.)
Every write that reaches disk is journalled. /changes lists what the agent has done this
session and /undo takes it back — including undoing an undo.
fs_edit changes an exact piece of a file and leaves the rest byte for byte. It requires the
revision mark (siman) that fs_read reported, so an edit written against a stale copy is
refused rather than silently clobbering newer work — and a model can never quietly drop the
parts of a file it did not think to reproduce.
Full detail: shomer, masoret, search, shell.
Every run composes GroundedAnswerPrompt into the system prompt: check whether there is
enough information, whether the source is trustworthy, and whether the claim can be verified
— then answer, state the uncertainty, or decline. No invented facts, citations, URLs, or
APIs.
Full reference is in DOCS/:
| Topic | Document |
|---|---|
| Projects, seams, the agent loop | architecture |
config.cfg, env.cfg, JSONX |
configuration |
| The provider matrix | providers |
Anthropic sign-in and morat auth |
authentication |
The tool contract and http_fetch |
tools |
| The virtual filesystem | vfs |
The working copy, /diff and /accept |
demut |
Consent, standing grants, /undo |
shomer |
| Anchored editing and the siman | masoret |
fs_glob, fs_grep, the glob syntax |
search |
shell_run and how host paths stay hidden |
shell |
| The terminal UI seam | tui |
| Adding a tool / provider / UI | extending |
| Commands and every setting | commands |
| Build, performance, testing | building |
dotnet test -c ReleaseOne test file per module, mirroring the source layout.
MIT — see LICENSE.
{ "provider": "ollama", // anthropic | openai | gemini | mistral | openrouter | ollama | llamacpp | custom "model": "llama3.1", "baseUrl": "http://localhost:11434/v1", "effort": "high", }