# OpenCompany - Claude Documentation ## Project Overview OpenCompany is an open-source operating system for self-improving AI employees. Employees use browsers, apps, tools, integrations, and the computer to complete work. Home lets people describe a job, review its setup, hire an employee, and work with it through chat and a shared Workspace. Hiring assembles a lead and specialist agents automatically. The React Flow editor in Dev mode exposes the underlying workflows, tools, context, memory, and skills for technical customization. Public documentation should preserve the employee OS vision, explain it in plain language, and keep development and infrastructure details in a separate technical section. ## Documentation Reference **Always refer to these documentation files for detailed guides:** | Document | Description | |----------|-------------| | **[Browser AI Agent deployment](./docs-internal/browser_agent_deployment.md)** | Shared creation recipe, local/distributed prerequisites, PostgreSQL offline transfer, exclusive browser ownership, owner recovery, direct task history and release gates. Browser effects route to owner queues even with worker pools disabled. Direct tasks use the existing native agent/Workspace path and remain separate from deployment Start/Stop/Resume. | | **[1Password credentials](./docs-internal/onepassword_credentials.md)** | CLI-first approved API/login bindings, pinned executable verification, desktop/service-account authorization, metadata-only catalogues, Activity-local resolution, protected CDP login and the local-only OAuth/CLI compatibility boundary. No resolved values in models, Temporal inputs, viewer captures or task history. | | **[Frontend Architecture](./docs-internal/frontend_architecture.md)** | Current frontend stack (React 19 + Vite + Tailwind v4 + shadcn/ui + Radix + RHF/zod + TanStack Query + Zustand). Tokens, primitives, forms, credentials exemplar, ownership boundary, `uiHints` catalogue. Brand: the Open Council mark and the outlined Newsreader wordmark are SVG paths in `client/src/components/brand/geometry.ts`, drawn in the text colour by `brand/Logo.tsx`; the favicon (`client/public/opencompany-icon.svg`), the desktop icon (`desktop/build/icon.svg`) and the icon-only logo SVGs in `media/brand/` embed its `MARK_PATH` verbatim (`brand/__tests__/appIcons.test.ts`), so change the mark there, paste the new path into each, and render the square PNGs beside those SVGs again (`media/brand/README.md` says how). | | **[UI Migration Plan](./docs-internal/ARCHIVE/ui_migration_plan.md)** | *Archived.* antd → shadcn/ui migration completion log (Waves 1–10). Current state lives in [Frontend Architecture](./docs-internal/frontend_architecture.md) and [Theme System](./docs-internal/theme_system.md). | | **[Temporal Cleanup & Resilience Plan (Waves 15-18)](./docs-internal/ARCHIVE/TEMPORAL_CLEANUP_AND_RESILIENCE_PLAN.md)** | *Archived — all four waves shipped.* 15 dead-code retirement (Redis-Streams branch in event_waiter, APScheduler cron stack, orphaned `connection_status` factory), 16 per-queue task routing (`TemporalWorkerPool`), 17 resilience hardening (cron `catchup_window`, SDK interceptors, `DEPLOYMENT_MODE`, periodic heartbeats; the one-shot LLM-step retry it records was later reverted to unlimited-with-backoff), 18 worker performance tuning. Current settings live in [Temporal Architecture](./docs-internal/TEMPORAL_ARCHITECTURE.md). | | **[Node Allowlist](./docs-internal/node_allowlist.md)** | Single-config UI visibility — `server/config/node_allowlist.json` controls which nodes / credential categories / skill folders show in the UI. Five lists with two enforcement tiers (mode-gated allowlist + absolute blocklist). `useNodeAllowlist` hook exposes `isVisible` / `isBlocked` / `isAllowed` / `isCredentialCategoryDisabled` / `isSkillFolderDisabled`. Adding a new disabled domain = single JSON edit, no code change. | | **[Normal Mode (Home)](./docs-internal/normal_mode.md)** | The owner-facing landing screen: describe a job, an LLM on the server drafts a setup screen (a JSON UI spec the client parses, normalises and renders from a fixed catalogue), and Hire builds, validates, saves and starts one workflow per AI employee (`services/employees/`, graph built from `config/employee_apps.json` plus the owner's skill library and a Canvas node, and checked against `node_allowlist.json` `enabled_nodes`). Covers the screen switch (`app/`, `shellMode` / `ui_shell_mode`, `VITE_NORMAL_MODE=false` kill switch, the same two themes and theme button as the editor), the employee page (the shared chat, `features/chat`, with the employee named in the header beside New conversation; Stop is in the Workspace's row), deleting an employee from the team sidebar (stopped through Reset first, then gone from every tab), the live Ask first rule (`workflow_rules`, the chat's Ask first chip via `set_ask_first`; app replies wait in `approvalGate`, sending tool calls are held as drafts by `services/approvals/tool_calls.py` and sent after a 5 s Undo window by `ApprovedToolCallWorkflow`; cards in the chat; builder v3 gates every app reply, attaches every app tool and gives the talk agent `chatUi` and the registry's `talk_send` tools, with instructions identical whatever the rule; older employees are upgraded on Apply / Turn on Talk / Start by `services/employees/upgrade.py`; in a generation that waits for the owner (RFC-0007, `approval_wait_version` 1, which Start does not write yet) Ask first becomes a mode over a class table (`services/approvals/classes.py`) minus always-allow rules (`allows.py`), the agent answering a chat run waits in its turn on `ApprovalWorkflow` and runs the allowed call itself, and the gate no longer holds Stop), run records for "done today" (`machina-run-record-v1`), automatic-pause reasons, Settings (Profile fields and switches, among them Generated UI, off by default and also in Dev's Settings panel: with it off a hire fills a fixed form and the server draws the setup card, `services/employees/setup_form.py`; usage-only Billing via `get_employee_usage`; Skills, the user-skills library each new hire copies onto a Skills node; consumer Connectors with `publisher` / `verified`; Plugins, the starter bundles in `hire/starters.json`; the last three share one catalog page), the Workspace dock (one header-high row: pill tabs, the employee's status dot (`workspaceStatus`; Live in red), their main action (a round Stop or Resume, else a small labelled Start, Start again, Connect or Retry reset), Expand and Close; an employee's Canvas board, named by the summary's `canvas_node_id`; a live Browser tab under one row of page controls, shared with Dev mode; Mobile shows the managed local Android phone; a floating status pill with the latest step from the step log `services/workspace_steps.py`, read with `workspace_steps_list` and again on `workspace_step`; Take over, in the Browser row and the phone's device bar, which takes the screen and stops the employee until Hand back, in the "You're in control" banner the status pill becomes meanwhile, and shows again on a screen that let go of the owner; Dev mode shows the same dock for the open workflow: both screens mount one shell, `components/workspace/WorkspaceDockShell` (sized by `components/workspace/dockSize`), around one body, `features/home/workspace/WorkspaceView`, with the shared `WorkspaceTabs` / `FullView` / `BrowserWorkspace` / `MobileWorkspace`; the editor's `CanvasDock` only adds its own (nodes from the canvas, the Canvas node picker, file previews, and Dev mode's extras through the views' `dev` prop), and its row has no main action because the toolbar has Start, Stop and Resume), the "Web browser" app (attached whole, each call read-only while the owner asks first through its approval spec's `restrict_while_asking`; while the agent waits in `request_user` the summary's `browser_request` shows Needs you and Help in browser), Talk on the employee page (a thread over the chat session whose id is the workflow id, rows written through `services/chat_thread.py` and, for the owner's messages and their chat runs, `services/chat/ledger.py` (see [Chat Protocol](./docs-internal/chat_protocol.md)); the talk line is chatTrigger → an agent → `chatReply` "Reply in Chat": a chat hire's own agent is its talk agent, other hires get a "Talk with {name}" agent sharing the worker's tools and skills with its own Context, and schedule hires also "Post to Talk"), Turn on Talk (for talk `off`) and Apply (while the summary's `pending_changes` is true) via `enable_employee_talk` / `apply_employee_changes`, each a restart on the saved graph through `services/deployment/restart.py`, self-extension from Talk through the Agent Builder within the Hire rule (`services/employees/policy.py`), the 3D orb, the wire contract and the `employee_lifecycle` / `approval_lifecycle` / `chat.updated` / `workspace_step` broadcasts. | | **[Chat Protocol](./docs-internal/chat_protocol.md)** | The wire contract between the chat runtime (`server/services/chat/`) and the shared chat UI: AG-UI's event model on the existing WebSocket, snake_case, in the CloudEvents envelope. A message a deployed chat trigger will answer starts a **chat run** admitted with it in one reserved write transaction (`Database.reserved_session`, SQLite `BEGIN IMMEDIATE`): one live run per session (the lane, also a partial unique index; a second send answers `run_in_progress`), idempotent `client_message_id`, claimed and finished by the MachinaWorkflow that answers it behind `machina-chat-run-v1` (the run id is trusted only from an event whose source is `opencompany://services/chat`; `run_scope` rides every node context; only the claimant finishes), the reply saved as `a_` by `chatReply`, every message appended after the session's active leaf (`chat_threads`) so concurrent writes never fork the thread. `chat_run_event` frames reach only sockets that passed `chat_subscribe`'s owner check (`services/chat/hub.py`: per-run `seq`, `hub_epoch`, bounded per-socket queues with a resync), never the status broadcast; a standalone worker sends its run events, `chat.updated` and `approval_lifecycle` to the backend over `/ws/internal` (`services/chat/relay.py`, `chat_run_publish`, ordered, dropped events resynced). A watchdog (`services/chat/watchdog.py`, started by `main.py`) ends runs nothing will finish (`not_delivered`, `timed_out`, `interrupted`). The agent wired into Reply in Chat (`answers_chat_run`) streams its answer as `text.*` events (`services/chat/stream.py`, activity input only, no workflow patch), its tool calls show as steps (`services/chat/steps.py`; label from the plugin's `chat_step`), and `stop_chat_run` stops a run cooperatively: its LLM step polls the run and returns what was shown, unstarted tools are skipped, the reply is saved `stopped`, and the watchdog cancels the workflow of a run still stopping after `runs.stop_grace_s`, and of one running past `runs.max_running_s` (`timed_out`). Generated UI: one owner-wide switch, `UserSettings.generated_ui` (off by default), read by `services/genui/switch.py` when a run starts, when Reply in Chat saves the reply and on each setup request; while it is off, `collect_agent_connections` and `collect_teammate_connections` leave `chatUi` out of every agent's and teammate's tools, the guide asks for no follow-ups and the hire screen is a fixed form. The `show_ui` tool (`chatUi`; its description written by json-render's `catalog.prompt()`, `bun run genui:prompts`) is checked against `config/chat_genui_catalog.json` (`services/genui/spec.py`; refused with every reason so the model retries), saved as a run part, streamed as `activity.*` RFC 6902 patches and sealed into the reply (`services/chat/parts.py`); `chat_ui_state` keeps what the owner set, and a button press comes back as `send_chat_message{ui_event}` -> a `[ui-event]` turn. The answering agent's system prompt ends with a fixed guide (`services/chat/guide.py`), what the owner set reaches its next turn as a `[ui-state]` note (`chat_notes`, `services/chat/notes.py`: claimed by the payload, told when the run answered), a trailing `` block becomes `parts.followups` (kept only while Generated UI is on), and a `chat_sources` tool's results are numbered per conversation for `[n]` citations (`services/chat/sources.py`). Approvals (`services/approvals/`, its own section): a plugin's `approval` spec marks a tool call that sends; while the workflow asks first it is held as a `tool_call` draft, shown as a card on the reply that made it, sent once after the Undo window by `ApprovedToolCallWorkflow`, with the outcome told to the employee as an `[update]` note; in a generation that waits for the owner (RFC-0007, not yet written by Start) the mode and the call's class decide what asks, the agent answering the run waits in its turn for Allow or Decline (`services/approvals/waiting.py`; a message sent meanwhile declines the call and answers `declined`), and the watchdog does not time the wait out. Branches (`services/chat/branches.py`): `get_chat_messages` returns the path to the thread's active leaf, each message with its versions (`siblings`), `editable` and the owner's rating; `edit_chat_message` / `regenerate_chat_reply` / `switch_chat_branch` move the path in one reserved transaction under the conversation store's lock, taking each agent's stored conversation back to the cursor its run recorded at `prepare_payload` (`chat_runs.context_cursors`) or restoring the branch's kept copy (`chat_branch_snapshots`), refused on `revision_conflict`, `run_in_progress`, `cannot_rewind`, `branch_unavailable` or `older_generation`; `set_chat_feedback` keeps a rating and tells the next turn (`[feedback]` note). Attachments (`services/chat/attachments.py`): the chat uploads files under `uploads/` and sends only their paths, the server rebuilds each reference from the file (refusing anything else, at most six), the answering agent reads an `[attachments]` line and images ride the opening user message for a provider with `vision.user_images`; `options.web: false` drops the `search` tools for that run; Home's model picker (`services/chat/choice.py`, `get_chat_models`) sends `options.model` / `options.effort`, resolved at admission (Auto = the owner's default model; refused with `model_unavailable`, never swapped) and applied only to the answering agent, effort only to a provider's `effort_models`; `get_chat_context` gives slash commands (generic plus the apps' `commands`) and capabilities; dictation is the speech plugin's `dictation_status` / `transcribe_audio`. Lists every event, handler, part and error code (among them `engine_unavailable`: with Temporal not connected a workflow's chat saves nothing, since nothing could deliver it). | | **[Theme System](./docs-internal/theme_system.md)** | Two themes, `light` and `dark` (the design handoff's ten skins are gone: nine were removed on 2026-10-12 and the last, `atomic`, on 2026-11-09), driven by `` + per-theme CSS files in `client/src/themes/`. Home, the editor and the sign-in screen all show the chosen theme, and one theme button ([components/ui/ThemeButton.tsx](./client/src/components/ui/ThemeButton.tsx)) in the Home header, the editor's toolbar and the sign-in screen toggles Light and Dark (`cycleTheme`; every theme name comes from `THEME_LABEL` in `ThemeContext.tsx`). Token taxonomy (surface / fg / border / accent / typography / motion) in **hex + `color-mix()`**; `@theme inline` bridge maps `--color-X: var(--X)` (no `hsl()` wrapper); per-theme files own the colour hex, base.css owns the shared `--tint-*` alpha scale; action role tokens (incl. `-ink` readable text), **per-theme `--code-*` syntax tokens** (tier 6 — the code editor, console/output JSON viewers, and chat code blocks paint in each theme's own palette; replaced the global dracula `--prism-*` block + dead `getPrismTokenCSS`; the `OutputPanel` JSON viewer reads the same vars), the `--pulse-keyframe` animation system + `.opencompany-*` helpers in `animations.css`, per-component decoration hooks (`.canvas-host`, `.toolbar`, `.sq-node`, …; kept as the theme contract, mostly unstyled by light.css and dark.css), **canvas-node visual contract via `--node-color` CSS custom property** (no inline `background` / `border` on node components — base.css + per-theme CSS owns visuals; `NodeStyle` helper type at [types/NodeTypes.ts](./client/src/types/NodeTypes.ts) makes the inline custom-prop typecheck-clean), **`--node-pulse-color` separate from `--node-color`** so executing-node glow uses each theme's highest-contrast accent regardless of plugin accent, **`data-page-hidden` animation pause** (set by `app/usePageActivitySync.ts`, mounted in `App.tsx` above the sign-in gate so it covers every screen, while the page is hidden per the Page Visibility API, never merely because the window lost focus; base.css declares `html[data-page-hidden] *, *::before, *::after { animation-play-state: paused !important }` to prevent compositor stall on tab return), **per-theme canvas-grid + custom cursors** via `--canvas-grid` / `--cursor-default` slots, **parameter panel migrated to Tailwind tokens** (no `useAppTheme()` reads; section headers carry the display-typography triplet; raw `