Architecture
malac is a Cargo workspace with four crates:
| Crate | Role |
|---|---|
malac-core |
The runtime: providers, tools, agent loop, permissions, memory, sessions, MCP client wiring. No UI. |
malac |
The malac binary and library: CLI, the daemon, its protocol and Rust client, the terminal UI, the line interface, the web server. |
malac-control |
Computer and browser control, served over MCP (stdio or WebSocket). Also a library. |
malac-mcp |
A small Model Context Protocol implementation (client and server) shared by the others. |
Front ends never talk to models or tools directly. Either they create a Runtime in their own process, spawn agents on it and render the event stream (-p, --json and piped runs), or they are clients of the daemon, which holds the runtimes for them (the terminal UI, the line mode, the web UI and the editor extension).
Runtime
Section titled “Runtime”Runtime (crates/malac-core/src/runtime.rs) holds what every agent shares:
- the merged
Config, - a provider cache (
provider/), built lazily per provider name, - the tool list: built-in tools plus tools from connected MCP servers,
Memory, the permissionPolicyand the currentApprover,- the project context (
context/): instruction files, the docs map, the project index and.malac/MALAC.md, - the enabled plugins (
plugins/) with their hooks, and the agent types, skills and slash commands found on disk and in plugins (catalog.rs,commands.rs), - the
EventBusand the registry of live agents - the job table (
jobs.rs)
Several runtimes can share a Host: one sequence counter for their event buses, one job table, and one MCP pool that keys connections by server configuration, so identical servers (the control service included) start once per process. The daemon’s tabs are such runtimes.
Agents
Section titled “Agents”An agent (agent.rs) is one conversation owned by a tokio task. Callers hold an AgentHandle and send it commands (prompt, send, cancel, set_model, compact); the task processes them one at a time, so an agent’s history is never shared or locked.
A turn:
- Attach recalled memories (vector search on the prompt) and append the user message.
- Compact the conversation if the last request used more than 80% of the model’s context window.
- Stream a completion from the provider, publishing text, reasoning and tool-call deltas. Failed requests are retried with backoff when the error is retryable and nothing has streamed yet.
- For each tool call, ask the
Policy; if it says “ask”, theApproverdecides. Consecutive tools of kindSaferun in parallel; others run one at a time, in order. - Append the tool results and loop until the model answers without tool calls.
Subagents are ordinary agents with a parent and a depth. The agent_spawn, agent_wait, agent_send and agent_list tools create and steer them; a semaphore limits how many run at once, and agents at max_depth (default 3) do not get the delegation tools. Agents that may delegate see an “Available models” section in their system prompt, with each model’s description, so they can give each subagent a model that fits its task. Cancelling an agent cancels its whole subtree.
Prompts can carry attachments (attach.rs): text files become a fenced block, images image blocks, folders a listing, other files a note.
Project context and plugins
Section titled “Project context and plugins”context/ gathers what a session knows about its project when its runtime is built: instruction files in every form other agents use (instructions.rs, deduplicated and fitted into a budget by priority), a map of the Markdown docs (docs.rs), the structural index in .malac/index.md (index.rs: a gitignore-aware walk, per-language symbol patterns, incremental by modification time, refreshed in a background thread when stale), the knowledge file .malac/MALAC.md (knowledge.rs), and where the folder sits in its repository. prompt.rs turns it into sections of the system prompt, and the same report backs /context and malac context.
plugins/ reads the Claude Code and Agent Plugins formats (manifest.rs), catalogs (marketplace.rs), sources through git (source.rs) and the state files (store.rs); manage.rs installs, enables and updates plugins, hooks.rs runs hooks, and builtin.rs holds the plugins compiled into the binary. When a runtime is built, the enabled plugins’ skills, commands and agents join the catalog, their approved MCP servers join the configuration, and their approved hooks go to the agents. In a turn, prompt hooks run before the message is sent, and PreToolUse hooks run before the permission check: they may deny a call, change its input, force a question or skip it, and deny rules still win.
bash with background: true starts a job: a shell in its own process group whose output goes to ~/.malac/jobs/<id>.log, with a record next to it. Jobs outlive agents and the process that started them; job_list, job_output and job_kill control them. When the daemon starts, it restores the records, marks jobs whose processes are gone as unknown, watches live ones, and deletes finished jobs older than a week.
Every agent writes an append-only JSONL transcript to ~/.malac/sessions/<id>.jsonl (metadata, messages, usage, compaction summaries). Resuming replays it from the last compaction.
Events
Section titled “Events”Everything an agent does is an Event on the broadcast EventBus, wrapped in an Envelope with the agent id, a sequence number and a timestamp. The terminal renderer, the --json printer and every web UI connection are independent subscribers. This is what makes subagents visible: their events flow through the same bus and every front end can show them. The event types are listed in JSON events.
Providers
Section titled “Providers”Provider::complete takes a provider-neutral Request (system prompt, Messages made of Blocks, tool specs) and returns the assembled message plus usage. Two wire formats are implemented:
- OpenAI-compatible chat completions (
provider/openai.rs): OpenAI, LiteLLM, vLLM, Ollama, OpenRouter, Together and others. Reasoning arrives asreasoning_contentorreasoning. Images returned by tools are re-sent in a user message, since tool messages cannot carry images. - Anthropic Messages (
provider/anthropic.rs), with prompt caching on the system prompt, tools and the latest turn, and signed thinking blocks replayed as required.
For models marked vision = false, images are replaced by descriptions written by vision_model before the request is sent.
Permissions
Section titled “Permissions”Each tool has a kind: Safe (never asks), Edit or Execute. The mode decides the default (ask, edits, autopilot); allow and deny rules (tool or tool(glob)) refine it. Shell commands are split on &&, ||, ;, | and newlines, and every segment must match an allow rule. Deny rules are checked first and apply in every mode, autopilot included. See Permissions and autopilot.
Memory
Section titled “Memory”Markdown files are the source of truth: ~/.malac/memory/*.md (global) and <project>/.malac/memory/*.md (project), each with name and description frontmatter (project ones also a kind), plus a generated index: MEMORY.md for global memories, .malac/MALAC.md for the project’s, grouped by kind. The indexes are part of every system prompt.
When [memory.vector] and [memory.embeddings] are configured, saving a memory also upserts its embedding into a Milvus collection (through the Milvus REST API, so self-hosted Milvus and Zilliz Cloud both work). Search returns the matching files’ current text, so the index can never serve stale content. With auto_recall, every prompt is embedded and close matches are attached to it. malac memory reindex rebuilds the collection from the files.
Control service
Section titled “Control service”malac-control exposes the desktop and browsers as MCP tools. malac connects to it like any other MCP server, in this order:
[control] url, if set,- a running service announced in
~/.malac/control.json(written bymalac-control serve), - a spawned
malac-control stdio, found next to themalacbinary or onPATH.
Its tools are added without a server prefix (browser_click, computer_screenshot). Read-only tools are annotated so they never need approval.
Daemon
Section titled “Daemon”malac daemon is one long-lived process per user that holds every workspace, tab and agent, so agents outlive the terminal that started them and several clients share them.
- Process.
malac daemon start(and any client that needs it) runsmalac daemon runin its own session, logging to~/.malac/daemon.log. It takes~/.malac/daemon.lock, listens onweb.porton loopback (or on all interfaces withweb.remote), and writes~/.malac/daemon.jsonwith its address, token, pid and version. - Hub. The state: workspaces (saved to
~/.malac/workspaces/), one runtime per loaded tab, built lazily from the tab’s folder and options on a sharedHost, the pending approvals, and a broadcast of changes. Commands from every transport go through one entry point. - Approvals. Every tab runtime uses the hub’s approver. Requests reach all clients and wait until one answers; the first answer wins.
- Restarts. On start the hub loads the saved workspaces and restores jobs. A tab loads its runtime when used, and a saved agent resumes from its transcript when prompted.
- Idle exit. With no clients, running agents, jobs or pending approvals for
daemon.idle_minutes(default 120), the daemon exits. - Versions. The daemon keeps the version it started with. Clients send theirs, and the snapshot flags a mismatch so they can suggest
malac daemon restart. - Control service. The daemon connects the control service once at start through the shared MCP pool and reports its state and macOS permissions in the snapshot.
- Remote access (
daemon/access.rs,daemon/tunnel.rs). Each request is local (loopback peer, loopbackHost, no proxy headers) or remote, and remote ones are refused unlessweb.remoteis on. The master token works only locally; other devices pair with a one-time code for a device token, hashed in~/.malac/devices.jsonand revocable, which closes their sockets. A tunnel tool runs as the daemon’s child, and its public address joins the pairing links. - Keep-awake (
daemon/awake.rs). A task follows the running agents and jobs and holdscaffeinate(macOS) orsystemd-inhibit(Linux) while there is work, perdaemon.keep_awake.
| Command | Runs in |
|---|---|
malac -p, --json, piped input, no terminal |
this process; never starts the daemon |
malac (full-screen UI) |
a daemon client, starting the daemon if needed |
malac --plain (or ui.tui = false) |
a daemon client, starting the daemon if needed; in this process when it cannot be reached |
malac web, the editor extension, the web UI |
daemon clients |
malac -C <name> |
a client of the daemon a saved connection names (connections.rs); never started from here |
The protocol is summarized in Protocol.
Web UI and editor
Section titled “Web UI and editor”The daemon serves a single-page UI from the binary; malac web starts the daemon if needed and prints or opens its URL. API requests need a token: the master token on local requests, a device token from pairing otherwise. Remote requests are refused unless remote access is on, which also blocks DNS rebinding; without a token, the page shows a pairing form. The VS Code extension connects to the daemon in ~/.malac/daemon.json, or starts one with malac web --port 0 --json-ready --cwd <folder>. It holds one WebSocket in the extension host, shared by its chat, trees, status bar and notifications; the chat is a webview that talks only to the extension host. Open Full UI embeds the web UI in a panel.
Updates
Section titled “Updates”update.rs reads the latest release’s SHA256SUMS, downloads the archive for the current target, verifies it, unpacks it next to the installed binaries and renames them into place, malac-control first. A rename never disturbs a running process, so agents keep running on the old binary and the next start picks up the new one.
malac is created by lobirus and released under the MIT license. About malac