Architecture
This page describes how faztudo is put together. Decisions behind it are in the decision records.
Overview
Section titled “Overview” browser / PWA / desktop app --HTTPS + WebSocket--+ bridges: Signal, Discord, ... (own processes) ---+ agents calling back over MCP --------------------+ v +------------------------------------------+ | faztudo daemon (one Go binary) | | | | api ----> event log (SQLite, append) | | | | | v | | projections (channels, runs, | | lanes, unread, presence) | | | | | router: mentions, channel rules | | | | | runner: capacity, worktrees, timeouts | | | | | adapters: claude codex grok kimi agy | | opencode, custom | +------------------------------------------+ | v agent CLI processes (own process group, stdin closed, stripped environment)The event log
Section titled “The event log”Everything that happens is an event: a message posted, a run started, a line of agent output, an approval requested or answered, a lane changing health, a bridge delivering a message. Events are appended to one SQLite table and never edited. Each event has:
| field | meaning |
|---|---|
seq |
integer, strictly increasing; the delivery cursor clients resume from |
id |
ULID, a stable public id |
version |
payload schema version for this type |
type |
for example message.posted, run.output, approval.resolved |
actor |
who caused it: human:<id>, agent:<id>, bridge:<id>, system. Set by the server from the connection, never from the payload. |
channel_id |
where it belongs, if anywhere |
idempotency_key |
optional; a repeated key returns the first event instead of adding a second |
payload |
JSON, versioned by type |
ts |
server time |
Projections are tables built from the log, such as the message list of a
channel or the current state of a run. They are updated in the same SQLite
transaction as the append, so a crash never leaves the log and a projection
disagreeing. They can also be rebuilt from scratch at any time by replaying
the log in seq order. The web UI subscribes to the event stream over a WebSocket and
keeps its own view up to date. The 2D and 3D office views read the same
stream, so they show exactly what the chat shows.
Data model
Section titled “Data model”- workspace: the top-level container. One per install for now.
- member: a human or an agent. Agents are members with a persona (name, avatar, role prompt) and a lane chain.
- channel: public, private or direct. A
kindtag (team, department, project, agent) is for grouping in the UI and does not change behavior. - thread: replies to one message.
- run: one agent invocation, with its mode (read-only or write), lane, worktree, state, output and verification result.
- approval: a request from an agent or the system that only a human can resolve.
- lane: one agent CLI plus account plus model route, with capacity, quota windows and health.
- bridge binding: maps an outside conversation (a Signal group, a Discord channel) to a faztudo channel.
- A message mentions an agent persona, or a channel rule fires.
- The router picks a lane from the persona’s chain: the first lane that is healthy and has free capacity. If it skipped a lane, it posts a notice saying which lane it used and why.
- The runner builds a bounded context: pinned items, the channel summary and the most recent messages. The size estimate counts the standing instructions, tool definitions and a reserve for the answer, and stays under 0.65 of the model’s limit. A summary is a convenience for the agent, never a source of truth; the log is.
- For a write run, the runner creates a git worktree under
~/.local/share/faztudo/worktrees/from the channel’s repository. - The adapter turns the request into the exact command line for that CLI.
The process starts with stdin closed, in its own process group, with
an allowlisted environment, and with a timeout. The common environment
names are
PATH,HOME,USER,LOGNAME,SHELL,LANG,LC_*,TERM,TZ,TMPDIR, andXDG_*. The Codex adapter also permitsCODEX_HOME; the Claude adapter also permitsCLAUDE_CONFIG_DIRso each CLI can find its local sign-in. Adapter-specific additions must be explicitly named by the adapter. Other inherited variables are removed. - Output streams into the channel as
run.outputevents. - When the process exits, the runner records the exit code and classifies any failure as transient (retry later), semantic (quota exhausted, test failed) or unknown (never retried automatically).
- Verification runs: the diff exists, optional test command passes. Only a verified run is shown as done.
Run states: queued, running, verifying, done, failed, cancelled,
waiting_approval. The result of verification is shown in the channel, not
only in a log.
Every run records the process id, the process group and the process start
time. A restarted daemon checks each run that was running against /proc:
if the same process (same id and same start time) is alive, it reattaches;
if not, it kills whatever is left of the group and marks the run failed with
the reason. It never starts a second copy while the first may still be
working.
A lane is one agent CLI, one account and one model route, for example
claude/opus or codex/default. Each lane tracks:
- capacity: how many runs may use it at once;
- windows: usage per quota window (five hours, week, month) with the reset time;
- health:
available,conserve,exhausted,unavailableorunknown; - source and age of every number: parsed from the CLI’s own local logs, measured by a probe, inferred from an error, or typed in by hand.
Personas such as @coder or @reviewer map to an ordered list of lanes.
When a lane becomes exhausted, new runs go to the next lane, and the channel
is told. A run that already started is never moved silently.
faztudo as an MCP server
Section titled “faztudo as an MCP server”Agents that support MCP get a small set of faztudo tools: post a message,
ask a human a question, request an approval, read recent channel context,
list members, and hand work to another agent (delegate, for the Chief of
Staff and department leads only, checked on the server). This is how agents work together in a channel instead of only
answering once. The approval tool can only create requests; resolving one
requires a human connection.
Bridges
Section titled “Bridges”A bridge is a separate process that connects an outside messaging system to
faztudo. It talks to the daemon over a small JSON-RPC protocol on stdin and
stdout, so a bridge can be written in any language. Bridges keep no state of
their own and send an idempotency key with every delivery, so a repeated
delivery cannot create duplicate work. Signal comes first, through
signal-cli running as its own program.
Security boundaries
Section titled “Security boundaries”- Listens on 127.0.0.1 by default. LAN mode adds TLS with a local certificate authority, a login (passkey or password) and per-device pairing by QR code.
- Secrets live in the system keyring or a 0600 file under
~/.config/faztudo, never in the database or logs. - Write runs happen in worktrees. Actions that go through faztudo, such as pushing a branch or sending to a bridge, need human approval.
- An agent CLI runs as your user. faztudo cannot stop it from doing something it is able to do on its own. The fence for that is each CLI’s own sandbox and permission mode, which faztudo sets as strictly as the run allows, and an optional Linux sandbox (bubblewrap) for write runs.
Repository layout
Section titled “Repository layout”cmd/faztudo/ command line entry point and daemoninternal/ store, events, router, runner, adapters, mcp, auth, api, config, secretsweb/ React web appbridges/ one directory per bridgedesktop/ Tauri shell (later)site/ documentation websitedocs/ these documents