Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Cowork Server

FastAPI backend for MindsHub Cowork. Manages projects, conversations, files, scheduling, memory, and agent orchestration with a SQLite-backed data layer.

This repo is the Python backend. The frontend (Electron shell + React SPA) lives in a separate repo: mindsdb/cowork. They are developed and released independently. At runtime, the frontend spawns cowork-server as a local sidecar and communicates over HTTP (127.0.0.1:26866).

Quick Start

Requires Python 3.12+ and uv.

# Install and run
uv tool install cowork-server
cowork-server

The server starts on http://127.0.0.1:26866. Confirm with:

curl http://127.0.0.1:26866/api/v1/health/

Development

# Run from source (auto-manages virtualenv + deps)
uv run cowork-server

When running alongside the Electron app in dev mode, the app spawns the server automatically — no manual start needed. The Electron app looks for a sibling cowork-server/ directory by convention (override with COWORK_SERVER_DIR).

Dev setup helper

uv run cowork-dev-setup

Initializes the database and validates configuration.

Testing

uv run pytest

Tests use an isolated in-memory database and temporary directories — no side effects on your local ~/.cowork/ data.

Logging

Set LOG_LEVEL (default INFO) to control verbosity. Enable file logging with ENABLE_FILE_LOGGING=true (writes to LOG_DIR, defaults to ~/.cowork/logs/).

Releasing

Releases are automatic on merge; there is no version to bump by hand (the package version comes from the tag).

  • Push to main: publish.yml runs the unit tests, cuts a CalVer tag and GitHub release (v0.<yy>.<m>.<d>.<seq>), then builds and publishes to PyPI via OIDC trusted publishing.
  • Push to staging: publish-staging.yml does the same on the rc pre-release stream (v0.<yy>.<m>.<d>.<seq>rc<n>, GitHub and PyPI pre-release), pinning the matching anton-agent rc into the wheel so the pair installs exactly.

Both take their version, tag, and release from the shared calver-release.yml reusable in mindsdb/github-actions (prerelease: true selects the rc stream). The publish jobs stay in these two workflows: PyPI trusted publishing matches the OIDC claim on the workflow filename and does not support reusable workflows.

In the packaged Electron app, a background updater checks PyPI on every launch and upgrades automatically (with rollback on failure). See server-updater.ts in the frontend repo.

Architecture

cowork/
  api/v1/endpoints/   # FastAPI route handlers
  services/           # Business logic
  models/             # SQLModel / DB models
  schemas/            # Pydantic request/response schemas
  db/                 # Database session and migrations
  common/             # Shared utilities, settings
  harnesses/          # Agent adapters (Anton, Hermes, etc.)

The server is designed to be agent-agnostic — core features (projects, conversations, files) are shared across agents, while agent-specific behavior lives in harness adapters. See docs/DESIGN.md for the full architectural rationale.

Harness system

A harness adapts an external agent library (Anton, Hermes, etc.) to the cowork-server interface. All harnesses implement the HarnessProvider protocol (harnesses/base.py), which exposes streaming responses, skill sync, and memory operations. The active harness is selected via the harness user setting. To add a new agent, implement the protocol and register it with the @register decorator.

Streaming & scheduling

Agent responses stream to clients via Server-Sent Events (SSE) on POST /responses/. The server tracks in-flight streams and supports cancellation (/responses/cancel) and late-join tailing (/responses/tail).

A background scheduler loop polls the database every 30 seconds for due schedules, supporting once, hourly, daily, and weekly cadences. Each run creates a conversation and is tracked in schedule_runs. Deleting that conversation does not delete the run: the run keeps its status, timings, and error as audit history, and only its link to the conversation is released. A channel binding pinned to the conversation is released the same way, so the external chat stays bound to its project and the next inbound message starts a fresh conversation.

Data Layer

Data lives in two places: a SQLite database for structured records and the filesystem for project files and agent workspaces. Understanding both is essential.

SQLite database

  • Location: ~/.cowork/cowork.db (override with DATABASE_URI)
  • ORM: SQLModel (SQLAlchemy + Pydantic)
  • Migrations: Alembic (cowork/db/alembic/versions/). Startup runs alembic upgrade head (singular), so the graph must have exactly ONE head: if two branches each added a migration on the same parent, every fresh boot aborts with "Multiple head revisions". After merging or rebasing, check alembic heads; if it prints two revisions, add a no-op merge revision whose down_revision is the tuple of both heads (see f4e2c1a9d3b7 for the pattern).

Key tables:

Table Purpose
projects Project metadata and filesystem path
conversations Conversation threads, linked to a project
messages Individual messages with role, content (JSON), and harness tag
message_events Streaming event payloads for a message
files Metadata for uploaded files (path points to filesystem)
schedules / schedule_runs Recurring prompts and their execution history
settings Key-value user settings; sensitive values Fernet-encrypted
pins User-pinned items (conversations, artifacts, etc.)
channel_* Channel installations, bindings, sessions, and events

All models use UUID primary keys with auto-tracked created_at/modified_at timestamps.

Filesystem storage

~/.cowork/
├── cowork.db                       # SQLite database
├── .master_key                     # Fernet encryption key for settings
├── skills/                         # COWORK_SKILLS_DIR — canonical SKILL.md store
│   └── <slug>/SKILL.md             # one folder per skill (see docs/SKILLS.md)
├── projects/                       # COWORK_PROJECTS_DIR
│   ├── general/                    # Default project (always exists)
│   └── <project-name>/
│       ├── <user & agent files>    # Working directory visible to agents
│       ├── skills/                 # symlinks to skills enabled for this project
│       │   └── <slug> -> ~/.cowork/skills/<slug>
│       └── .anton/                 # Private agent workspace
│           ├── artifacts/          # Agent-produced outputs (HTML apps, docs, etc.)
│           │   └── <slug>/
│           │       ├── metadata.json
│           │       └── <files>
│           ├── memory/             # Persistent agent memory by category
│           └── context/            # Project context for agent runs
├── files/                          # COWORK_FILES_DIR — uploaded files
│   └── <file-id>/<filename>
└── data-vault/                     # COWORK_VAULT_DIR — encrypted connector creds
    └── <engine>/<connection-name>/

How the two layers relate

The database holds structured metadata and relationships (which messages belong to which conversation, which conversation belongs to which project). The filesystem holds the actual content agents work with — project files, artifacts, memory entries, and uploaded documents. The files and projects DB tables store filesystem paths that point into the directory tree above.

This split is the result of an ongoing migration from a purely filesystem-based architecture. Structured data that benefits from querying and relationships — conversations, messages, settings, schedules — lives in SQLite. Components that are inherently file-based — project working directories, agent artifacts, harness-managed memory, connector vault credentials, and skills — remain on the filesystem by design. (Skills briefly lived in a DB table; they were moved back to canonical SKILL.md files so they can be edited, uploaded, and distributed per project — see docs/SKILLS.md.) See docs/SERVER_MIGRATION.md for the full migration story.

Agents (via their harness) have read/write access to their project's working directory and the private .anton/ subdirectory. They do not access the SQLite database directly — all DB interaction flows through the service layer.

Settings use a hybrid approach: user preferences and API keys are stored in the settings DB table (with Fernet encryption for secrets), while connector credentials live in the filesystem vault (data-vault/).

API

All endpoints live under /api/v1/. Key resource groups:

Path Description
/health Readiness probe
/projects Project CRUD and working-folder management
/conversations Conversation threads and message history
/responses Streaming agent responses (SSE)
/files OpenAI-compatible file uploads
/schedules Recurring task scheduling
/skills Agent skill definitions
/memory Persistent agent memory
/artifacts Agent-produced file previews
/publish Publish HTML artifacts to 4nton.ai
/connectors Third-party service connections and OAuth
/settings User preferences and API keys

Who can read what in org mode

Local mode has one user, so none of this applies: every check below is inert on the desktop.

In org mode a request acts as a pair, an organization and a user, and it never carries less. The gateway authenticates the credential, asks the auth service whether that user is still a member of that organization, and injects X-User-Id and X-Organization-Id. cowork-server validates the shape of those headers and then trusts them, so the gateway being the only route to the pod is what makes them trustworthy, and that is a NetworkPolicy rather than anything in this codebase. A request with no valid pair is answered 401 before any route runs, except on /api/v1/health/, which the kubelet probes with no headers, and the channel webhook paths, which third parties call.

Inside one organization, two different rules apply, and which one you get depends on the resource:

  • Shared with the organization: projects, project files at the project root, skills, project memory, connected apps. Every member reads them.
  • Private to whoever created it: conversations and their history, scheduled tasks, personal memory, uploaded files, and everything under a conversation's own workspace at conversations/<conversation_id>/. Live artifacts are in that last group, because the agent writes them into the conversation it is running in.

The private rule is enforced by the service layer rather than by the routes: ConversationService._owned, FileService._owned_select and ScheduleService._owned_select each add created_by == <the caller> when a request is org-scoped. Two places extend the same rule to the filesystem. _conversation_workspace_ok in the project-file routes refuses a path under another member's conversation directory, and artifact_roots drops another member's conversation directories before the artifact list or delete ever sees them, because those routes are addressed by project and slug and never receive a conversation id.

Both of those decide from a resolved path and the route then opens that path, so the decision is carried to the open rather than trusted afterwards: every component below the project directory is opened O_NOFOLLOW, and a symlink planted anywhere in the chain is refused. A pod mounts its own workspace read-write, so without that a swapped directory component between the check and the open reaches another member's tree, or another organization's.

A refusal on a private resource answers 404, not 403, with the same body a genuine miss returns. Telling the two apart would confirm that another member's file exists, which is most of what an attacker wants to know. Policy refusals that reveal nothing personal answer 403 instead: the desktop-only routes say not available in org deployments, and an organization-settings write without the admin role says so plainly.

The HTML preview hands out a bearer token in a URL (preview-mount-file returns one, preview-asset spends it), because an iframe cannot send an Authorization header. The token is random, it is bound to the member and organization that minted it, and it expires after 30 minutes, so a token that escapes into a log or a screenshot is not a way in for anyone else.

Being the minter is not enough on its own, because a mount grants a directory. The gate runs on the file the caller named, and an .html at the project root sits in a directory every member's conversations/<id>/ hangs off, so a token minted on a shared file would otherwise read every workspace under it. A mount therefore reaches only the workspace its own file lived in, and a mount taken on a shared file reaches no workspace at all, not even the minter's own. That check holds no session, deliberately: preview-asset serves every sub-asset a page pulls, and a session there is a database connection per image.

The default model is the one the free allowance covers

Every minds-cloud role defaults to mindshub_air, for all three roles: planning, coding and router. MindsHub's catalog declares it, in the mindshub_model_policy_v1 config that already owns the alias registry, and the declaration arrives as a default_for list on each /v1/models row. So moving a default is a config edit plus an apply, not a release. MindsHub Air's usage draws the monthly included allowance, so a user who has picked no model can finish a whole turn without the wallet being charged for any part of it.

The two roles a user never sees are why this is the default rather than a premium model. Planning is the model in the picker, so a wrong choice there is visible and fixable. Coding (the completion verifier and the scratchpad) and router (respond-versus-delegate gating and history summarization) run unseen, so a paid default there is denied on an empty wallet with nothing on screen to explain why.

An explicitly stored model is never rewritten by this. Paying for a better model is a pick in the Settings picker, and a funded wallet resolves to the same default as an empty one until that pick is made.

A stored model the wallet cannot pay for is swapped, not rewritten

/v1/models marks a model the org cannot currently pay for as enabled: false, and the map is cached as minds_model_enabled. When a stored pin is flagged that way, _resolved_model resolves the role to the first affordable model in the map instead, for all three roles. The alternative is every turn failing on a denial the user may not be able to see.

The stored row is left exactly as the user set it, which is the load-bearing half: the moment the wallet can pay again, the next settings load flips the alias back to enabled: true and the role resolves to the original pick with nothing to re-select.

Two cases share that path and should not be confused. A pin the map flags false is a real MindsHub model that is merely unaffordable right now. A pin absent from a non-empty map is foreign or retired, so it would 404 on every turn rather than 402, and it is healed the same way with no route back.

The desktop closes the loop at the other end: a model the map locks is not offered in either picker, so a swap only ever applies to a pin that was affordable when it was made. Allowing the pick meant the turn silently ran a different model from the one the picker named.

Where the answer comes from, in order

MODEL_ROLE_DEFAULTS in cowork/common/settings/app_settings.py is still a real answer, not a legacy layer. Resolution is synchronous and does no network call in the turn path, so the catalog's declaration has to be persisted before it can be read, exactly as the availability map is.

State What resolves When you are in it
A model is stored for the role that model the user picked one, or saved Settings once
minds_role_defaults names the role the alias the catalog declares any install that has loaded Settings since the catalog declared it
Nothing persisted MODEL_ROLE_DEFAULTS a fresh install sending its first message, and any install that has never reached the catalog

The availability map still overrides the answer in every case where the wallet cannot pay for it, so a declared default is not a grant: it says where to start, never what may be called.

GET /settings/recommended-models writes the map, from the same fetch that already refreshes minds_model_enabled, and it builds the recommendedPair it serves the picker through minds_role_start_models, the same two steps resolution takes: the declared default replaces the compiled one, then availability adjusts it. Those two have to agree, because the picker shows the pair as the model each role starts on and the desktop writes it back as an explicit pin when a save repoints a role onto MindsHub. A pair built from the compiled table, or from a declared default the wallet cannot pay for, would pin a model that turns never run. The pair is rebuilt from whichever map resolution will read: the live one when the gateway published defaults, the cached one when it did not.

Provider probes always use a model any key can call

Why a probe sends a model at all: MindsHub bills per model, so a model the wallet cannot pay for is denied, and that denial is indistinguishable from a bad key. Probing a paid model tells an account with an empty wallet that its working key is invalid. MINDS_PROBE_MODEL (mindshub_air) draws the monthly included allowance instead of the wallet, so the result reports reachability and key validity, which is what these endpoints are for.

Two endpoints, and they do not behave identically.

POST /settings/validate-provider (onboarding, and the only caller is the onboarding screen) probes a chat completion on every branch, and takes an optional model:

  • provider: "minds" always sends MINDS_PROBE_MODEL and ignores model.
  • provider: "openai-compatible" sends model as asked, so validating one specific model never reports a pass earned by a different one. Omit it against a MindsHub base URL and it falls back to MINDS_PROBE_MODEL; omit it against any other host and the generic openai-compatible default applies.
  • provider: "anthropic" sends model or claude-sonnet-4-6.

POST /settings/test-providers (the Settings health dot) probes per provider type, and only the minds-cloud type is a chat completion, on MINDS_PROBE_MODEL. The openai-compatible type is a GET {baseUrl}/models listing probe, so a MindsHub host configured through that card is health-checked against a route MindsHub does not deploy everywhere; those routes answer 404 or 401 even for a valid key, which is the reason the minds-cloud type does not use one.

Every MindsHub-bound chat probe caps the completion at max_tokens: 20, not 1: some models refuse a 1-token budget and fail the probe for a perfectly good key (see _chat_probe). The cap is not sent to a non-MindsHub endpoint, because OpenAI's reasoning models reject max_tokens and want max_completion_tokens.

The desktop app has a second copy of these validators in its Electron main process (cowork/src/main/provider-validation.ts, called from the settings:validate IPC handler in cowork/src/main/index.ts); the endpoints here serve the web build. Both copies have to change together. One asymmetry worth knowing: the desktop MindsHub onboarding path signs in through Keycloak rather than validating a pasted key, so main's validateMinds has no live caller today, and it is the openai-compatible and anthropic validators there that a packaged build actually runs.

Configuration

Configuration is read from the database (UserSettings table) and can be managed through the Settings UI in the desktop app or via PUT /api/v1/settings/.

Environment variables fall into two namespaces:

Server-level (COWORK_*) — control the cowork-server process itself:

Variable Default Description
COWORK_LISTEN_PORT 26866 Server port
COWORK_SERVER_HOST 127.0.0.1 Bind address
COWORK_TENANCY_MODE local local is the desktop sidecar: one user, no organization, no identity headers. org is the cloud deployment and turns on everything in "Who can read what in org mode" above.
COWORK_IDENTITY_ENFORCE enforce Org mode only. enforce answers 401 to a request carrying no valid identity headers. audit logs it and lets it through, which is the rollout mode the org cutover used; it now has to be asked for.
COWORK_SHARED_DIR ~/.cowork Org mode only. Root of the org-keyed tree: <shared>/<org_id>/{skills,memory,projects,files}. In cloud, point it at the durable mount — on the default the data is ephemeral (boot warning).
COWORK_PROJECTS_DIR ~/.cowork/projects Project storage root (local mode only)
COWORK_FILES_DIR ~/.cowork/files Uploaded files root (local mode only)
COWORK_SKILLS_DIR ~/.cowork/skills Skills store root (local mode only)
COWORK_MEMORY_DIR ~/.cowork/memory Memory store root (local mode only)
COWORK_VAULT_DIR ~/.cowork/data-vault Connector credential vault

Harness-level (ANTON_*, HERMES_*) — configure a specific agent harness. These are read by the harness adapter, not by cowork-server core. They use the harness prefix because the upstream agent libraries (anton, hermes-agent) define them:

Variable Harness Description
ANTON_PUBLISH_URL Anton Artifact publish endpoint
ANTON_SKILLS_ROOT_DIR Anton Skill file storage
ANTON_GLOBAL_MEMORY_ROOT_DIR Anton Global memory files
HERMES_HOME / HERMES_ROOT_DIR Hermes Hermes data root

In Docker/Lightsail deployments, the container also receives ANTON_MINDS_API_KEY, ANTON_OPENAI_API_KEY, etc. — these are consumed by the Anton agent library directly (not by cowork-server settings), and are injected by the provisioning lambda via cloud-init user-data.

Docs

License

See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cowork_server-0.26.8.27.1rc6.tar.gz (1.3 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cowork_server-0.26.8.27.1rc6-py3-none-any.whl (1.1 MB view details)

Uploaded Python 3

File details

Details for the file cowork_server-0.26.8.27.1rc6.tar.gz.

File metadata

  • Download URL: cowork_server-0.26.8.27.1rc6.tar.gz
  • Upload date:
  • Size: 1.3 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cowork_server-0.26.8.27.1rc6.tar.gz
Algorithm Hash digest
SHA256 18f3402adb867436ac248d560f64ae69e884fe639b17d60306c5a1755eea7049
MD5 034e5e0bfc9ff3fbe21e172c3292219d
BLAKE2b-256 49fc1f80e129712da8b88b226fc661dbfb35fd095a71e4994ec6849576b0d50c

See more details on using hashes here.

Provenance

The following attestation bundles were made for cowork_server-0.26.8.27.1rc6.tar.gz:

Publisher: publish-staging.yml on mindsdb/cowork-server

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cowork_server-0.26.8.27.1rc6-py3-none-any.whl.

File metadata

File hashes

Hashes for cowork_server-0.26.8.27.1rc6-py3-none-any.whl
Algorithm Hash digest
SHA256 41f6151bd60119a61e7615f677c92911b9842fad8532a2a0b352e262dd84b1e6
MD5 62063c85e21443350276205b11f3da4e
BLAKE2b-256 1f3c49019bd6be0d0fe1c45dcc8218f3e2151336ab6ad3f6daa21062ebb0180c

See more details on using hashes here.

Provenance

The following attestation bundles were made for cowork_server-0.26.8.27.1rc6-py3-none-any.whl:

Publisher: publish-staging.yml on mindsdb/cowork-server

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.26.9.16.1

2 files

0.26.9.14.2

2 files

0.26.9.14.1

2 files

0.26.9.13.1

2 files

0.26.9.6.1

2 files

0.26.9.2.1

2 files

0.26.9.1.1

2 files

0.26.8.31.1

2 files

This release

0.26.8.27.1rc6 This release

2 files

0.26.8.25.1

2 files

0.26.8.24.1

2 files

0.26.8.23.1

2 files

0.26.8.20.4

2 files

0.26.8.20.3

2 files

0.26.8.20.2

2 files

0.26.8.20.1

2 files

0.26.8.18.1

2 files

0.26.8.17.1

2 files

0.26.8.15.1

2 files

0.26.8.14.2

2 files

0.26.8.14.1

2 files

0.26.8.9.1

2 files

0.26.8.2.1

2 files

0.26.7.27.3

2 files

0.26.7.27.1

2 files

0.26.7.22.3

2 files

0.26.7.22.2

2 files

0.26.7.22.1

2 files

0.26.7.20.1

2 files

0.26.7.16.1

2 files

0.26.7.13.3

2 files

0.26.7.13.2

2 files

0.26.7.13.1

2 files

0.26.7.6.4

2 files

0.26.7.6.3

2 files

0.26.7.6.2

2 files

0.26.7.6.1

2 files

0.26.7.3.1

2 files

0.26.6.26.1

2 files

0.1.10

2 files

0.1.9

2 files

0.1.8

2 files

0.1.7

2 files

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page