# Architecture – Workforce agent OS
> The map of the code as it is now – for every human and agent that works on it: layers, modules, data, flows and the
> rules each part owns. The product is Autark; the code still says `workforce`. Build history: BUILD_LOG (internal).
## 1. The product in one paragraph
One person runs an AI company on their PC: a manager, teams of specialist agents (video, web, research, social,
finance …) and a private secretary, across company, personal brand and private life. The person talks to agents
in **chats** (a chat is a place, not an agent), gives **tasks** (one simple task model: whoever owns it works on it
until it is done or they ask), and looks at a calm **My day**: the few things only a human can do, what they
handed off, and their calendar. Agents are trusted like senior employees: they act on the whole PC without
asking, hire and archive colleagues (themselves included), split work into parallel parts (Ultra), use any model
(Codex, Claude Code, ACP plan CLIs, API keys, local servers) and work on linked PCs and Android phones. The
system behind it is deliberately small: **a SQLite store plus a few plain files and skills**. Anything without a
screen of its own (connectors, look, settings, devices) the AI changes by editing those files – the person just
tells the AI.
## 2. Process model
```
┌──────────── Electron main (src/main) ─────────────────────┐
│ window · tray · updates · launcher · protocols · IPC │
│ the person's browser (native tabs + side driver) │
│ media analyzer window · capture · Host services │
└──────┬───────────────────────────────┬────────────────────┘
│ utilityProcess + MessagePort │ MessagePort (handed over by the preload)
┌──────▼───────────────────┐ ┌──────▼──────────────────┐
│ Runtime (src/runtime) │◄──►│ UI renderer (src/ui) │
│ pure Node, no electron │RPC │ Preact + signals │
│ SQLite · agents · tools │ └─────────────────────────┘
│ engines · MCP hub │◄── WebSocket /ws ─────── PWA on phone/browser (same src/ui), wf CLI
│ HTTP :47811 │◄── POST /rpc + bearer ── scripts and agents (token in runtime.json)
│ peer listener :47812 │◄── pinned-key channel ── linked PCs (same runtime) · meeting server
└──────┬───────────────────┘◄── MCP over HTTP ─────── Codex / Claude Code / ACP CLI processes
│ child processes as leases: codex app-server per account, claude -p per turn, ACP CLIs,
│ PowerShell UIA helper, adb, stdio MCP servers, Playwright browsers of agents
```
* **Runtime** (`src/runtime`) owns all state and behaviour and never imports `electron`. The few things only the
host can do (OS keychain encrypt/decrypt, screenshots, notifications, power events, the person's browser,
the media analyzer, restart) it asks through `Host` (`runtime/host.ts`; `host-electron.ts` over the host bridge,
`host-node.ts` headless). Entry points: `entry-electron.ts` (the utility process, bundled to `app/runtime.mjs`)
and `cli.ts` (headless, `app/runtime-cli.mjs --data
`, used on worker PCs and in tests). The composition
root is `runtime/main.ts`: `compose/services.ts` builds every service, `compose/network.ts` everything that
serves clients, `compose/lifecycle.ts` holds the restore-before-open and the ordered stop steps.
* **Repair runtime**: `repair/boot.ts` asks the start guard (`/boot.json`); after repeated start crashes or
a failed upgrade the utility process runs `repair/server.ts` instead, and the UI shows `ui/repair/RepairScreen.tsx`
(Retry, put back the pre-update copy, diagnostics export).
* **Main** (`src/main`) is thin: `index.ts` (pre-ready: single instance, schemes), `desktop.ts` (composition),
`runtime-host.ts` (fork, launch-token handshake, restart with backoff), `runtime-link.ts`/`renderer-link.ts`,
`window.ts` (hardened window, hidden title bar), `window-recovery.ts` (a renderer crash reloads; more than two
in a minute show "Try again"), `tray.ts` (Open, Quit, "Stop all agent work"), `work-stop.ts` (global shortcut),
`companies.ts` (several companies per installation, §3), `host-services.ts` (notifications with their sound, §4.2),
`updates/*` (electron-updater state machine; on macOS, where Squirrel.Mac refuses an ad-hoc signed app, an own
self-update `updates/mac-self-update.ts`, only once the product is named Autark), `identity-move.ts` (B-066: at the
first start as Autark the old data folder `Proactive Agent Workforce` moves by one atomic rename – never copied or
merged – and the old updater cache goes; a no-op unless packaged, named exactly Autark and on the default data
folder; `logs/identity-move.log`; installer side `assets/installer.nsh`), `launcher*.ts` ("Start with Windows" = keep agents running:
one per-user Task Scheduler task, `run-state.json`, `next-due.json`), `protocols/*` (`app://`, `wf-sandbox://`,
`wf-file://`), `session-guards.ts` (microphone only for our page while armed), `workspace-*.ts` and `side-driver/`
(§10), `media-analyzer*.ts` (keyframes, audio decode, image prep in a hidden offscreen window), `title-bar.ts`
(§11). Closing the window does not stop agents when `closeToTray` is on.
* **Preload** (`src/preload`) exposes `window.workforce` (`shared/api/desktop.ts`) over IPC; `main-world.ts` guards
the runtime port. `ipc.ts` accepts only our window's main frame; `ipc-args.ts` checks every argument.
* **UI** (`src/ui`) is one Preact app for the desktop window *and* the PWA served by the runtime. It only talks
RPC (`ui/lib/rpc.ts`: MessagePort on desktop, WebSocket elsewhere). Layouts: desktop columns and the phone stack.
* **Shared** (`src/shared`): pure types, the RPC contract (`shared/api`), models (`shared/model`), the generative-UI
block catalog (`shared/blocks`), text and voice helpers. No Node, DOM or Electron imports.
* **Who calls** (`shared/api/protocol.ts` `ClientScope`, decided by the transport, never by the client): `owner`
(desktop window, local CLI, bearer token from `runtime.json`), `device` (a paired phone/browser of the owner),
`person` (a device of an employee), `peer` (a linked PC runtime), `agent` (an MCP lease of a running turn),
`external` (an external agent docked into one chat). `rpc/server.ts` dispatches, `rpc/scopes.ts` filters pushed
events, the handler table (`rpc/handlers/*`) decides which scope may call what.
* **HTTP server** (`runtime/http/server.ts`, port 47811 or the next free one): static UI and service worker for the
PWA, `/ws` RPC, `POST /rpc` (tokenless pairing/join exchange, or a bearer token for programs), `GET /events`
(Server-Sent Events, `docs/EXTERNAL_PROTOCOL.md`), `/mcp` (the MCP hub, loopback only), `/files/`,
`/sandbox/app` (mini-tools for the PWA). `http/peer-server.ts` is the separate peer listener (47812) other PCs
dial; it is open only while needed. Host allowlist, Origin check and the bind gate are in `http/security.ts`.
## 3. Data: a SQLite store plus a few files
**The system is a few files plus skills.** Everything a person or an AI is meant to change lives as a plain file
in the data folder; the database holds what the app writes (chats, tasks, runs, knowledge). There is no data-folder
guard: agents read and write these files like any other.
`` = Electron userData (headless: `--data`, tests: `WORKFORCE_TEST_DATA`). Layout: `runtime/root/layout.ts`;
paths: `runtime/system/paths.ts`.
**Companies** (`main/companies.ts`, `runtime/root/companies.ts`, `shared/model/companies.ts`): a company is one data
folder. The first is `/Autark`, every other one `/Autark Companies/` (id `[a-z0-9][a-z0-9-]{0,39}`,
new ones `company-2`, `company-3` …; the folders are the list). `main/index.ts` reads `--company=` and sets userData
before the single-instance lock, so each company is its own process with its own lock, runtime, database,
`settings.json`, tray, window, Remote ID and "Keep agents running" launcher (task, LaunchAgent or unit named with its id);
the workspace of every company but the first is `/workspace`, and `AUTARK_DATA=` points its agents' `wf`
at its own runtime. The profile menu (`ui/shell/ProfileMenu.tsx`, desktop) lists the companies (the
label from the profile's company or name) and "New company"; `companies.open` (host op `company.open`) starts the target
detached or shows it if it runs, and the asking window hides to its tray while its agents keep working; a new company's
first window runs the normal first run. Only the first company runs the updater (About says so in the others); after an
update it starts every other company whose run-state was running. The tray's "Stop all agent work" and the global
shortcut stop every company on this PC (`work.stop` over loopback `/rpc` with each one's token from its `runtime.json`;
a company whose shortcut registration failed retries while its agents work); the window's own stop and Resume act on
its own company. The uninstaller removes the other companies' launcher tasks too (`assets/installer.nsh`).
| Entry | What it is | Owner (code) |
|---|---|---|
| `README.md` | Generated map: every entry, what editing it does, how to call the running app | `root/readme.ts` |
| `settings.json` | **The only settings store**, written whole: JSON with comments (profile, preferences, notifications, voice, routing = "Models", onboarding, feedback, pwa, devices, day, calendar). Every key is present with its current value and a comment line above it naming its purpose, options or range and default; `null` means the default | `settings/schema.ts` (the one table), `read.ts`, `service.ts`, `validate.ts`, `defaults.ts` |
| `settings.schema.json` | JSON Schema (draft-07) of the same table for editors and scripts: enums, ranges, patterns, defaults, descriptions; written by the app once per start – never edit | `settings/schema.ts` (`schemaText()`), `settings/service.ts` |
| `theme.css` | The app's look: CSS applied live on top of the shipped styles (tokens such as `--surface`, `--text`; dark mode `:root[data-theme=dark]`). Delete it to go back | `root/theme.ts` |
| `agents//` | `SYSTEM.md` (the agent's instructions – authoritative), `skills//SKILL.md` (its Main Skills), `agent.json`, `avatar.png/jpg` copy; `agents/README.md` says who is who | `agent-core/*` |
| `memory/` | **The memory tree** (§5.2): one Markdown file per entry in folders by who it is for – `company/`, `branches//`, `agents//`, `references//`, `chats//` and any topic subfolder (`_folder.md` describes a folder). The files are the truth; anyone may edit them; writing or changing one makes it findable with `vector.search` | `knowledge/memory-*.ts`, `tools/builtin/memory.ts` |
| `prompts/` | Own versions of the shipped prompts (`identity`, `rules`, `skills`, `compaction`, `onboarding`, `agent-default`, `subconscious`) – a file here replaces the shipped one | `context/templates.ts` |
| `modes/` | Own modes (`.md`), replacing a shipped mode of the same key | `modes/service.ts` |
| `secrets/` | The secrets catalog: one `.md` per route listing entries (id, name, kind, tags, description) – never a value | `secrets/files.ts` |
| `connectors/` | `mcp.json` (MCP servers, the usual `mcpServers` map), `mail.json` (mailboxes), `status.json` (written by the app) | `apps/connectors.ts`, `apps/service.ts` |
| `chats/` | A readable mirror of the chats (`chat.json`, `messages/-.jsonl`); the database stays the original | `chat-files/mirror.ts` |
| `side-tabs.json` | The browser tabs per space: `global` (the person's pane), `chat:` (a chat's area), `agent:` (an agent's own area, kept by that agent); the window follows an edit within a second (§10) | `root/side-tabs.ts`, `main/workspace-tabs.ts` |
App-managed (never reported by the watcher): `workforce.db` (+ WAL), `blobs/ab/cd/` (content-addressed
files, `files/store.ts`), `accounts///` (isolated CLI homes), `browser-profiles//`, `logs/`,
`tmp/`, `backups/`, `exports/`, `engine-cwd/` (empty folder CLI engines start in), `engine-prompts/`,
`models/speech/` (local speech models and, on the same download rail, the local embedding model `embedding-minilm-l12-int8`, fetched on first need, §5.3), `engine-transcripts/` (rebuilt Claude Code sessions, §5.5), `runtime.json` ({port, token} while the app runs), `boot.json`,
`run-state.json`, `next-due.json`, `window-state.json`, `bin/` (the agents' `wf` shim, below). The workspace for projects
and artifacts is `/Workforce` (`downloads/`, `inbox/`, shared skills in `skills//SKILL.md`); another
company's is `/workspace`.
**One watcher** (`root/watcher.ts`) watches `` by OS events (no polling). Each entry has one owner that
registers with it; the app's own writes are recognised (`root/marks.ts`) and not reported back. Text files the app
writes and others may edit at the same time go through `root/text-doc.ts` (atomic writes; an unusable edit is kept
as `.rejected.md` with a quiet notice, `root/notice.ts`).
**settings.json rules** (`settings/schema.ts`, `read.ts`, `service.ts`): one table (`schema.ts`) lists every section and
key – purpose, options or range, pattern, default – and everything else is built from it: the file text
(`settingsText()`, a header, `"$schema": "./settings.schema.json"`, one comment line per section and key such as
`// Colour scheme of the window. Options: system | light | dark. Default: system`), `settings.schema.json`
(`schemaText()`), the defaults (`defaults.ts`), the checks (`validate.ts`) and the reader (`parseText()`/`stripJsonc()`:
`//` and `/* */` comments outside strings and trailing commas are accepted, no dependency). Reading (`read.ts`): defaults
⊕ file; each section is validated on its own; an invalid section keeps its last good value and shows a quiet notice;
`null` or a missing key means the default; `"$schema"` is skipped; unknown keys are dropped. Writing: the app writes
the **whole** file after every update and after every read whose text differs from the whole current text – that keeps
the comments current, turns an old file of differences into the whole file once (every value the person set stays) and
leaves an invalid value as written with a `// Not applied () …` line above it. Never overwritten: the store
from before the rebuild until its import moved it aside, a file that is not JSON at all (copied to
`settings.rejected.json` before the app writes again), and a file whose sections only fail together. A deleted file is
written again from the current values (write `{}` to reset). A valid edit applies within a second. Consequences:
a default changed in a later version does not reach existing installs for keys already in the file; a script reading
the file must strip the comments (`parseText(text).data`, never plain `JSON.parse`); editors need `allowComments`
(the schema sets it; a strict ajv needs `strict: false`). There is no spending, monthly or budget key anywhere (a test
asserts it). The skill `app-settings` is one short paragraph (open the file, every key has its options above it, change
the value, save) plus pointers to what is not in the file. The old `settings` table is emptied after its one-time move
into the file and kept only so an old backup can still be moved. Without a data folder (tests) settings live in memory.
**theme.css**: `root/theme.ts` emits `theme.updated` {css} on every change; `ui/main.tsx` puts it into
`