# muselab Constitution > [简体中文](constitution_zh.md) > > The single source of truth for **how muselab is built and may change**. > Specs and AI-generated code derive from this document; when code and this > document disagree, this document wins and the code is the bug. > > Scope: this governs *engineering invariants*, not feature wishlists. Feature > intent lives in per-change specs; product roadmap and known issues live on > [GitHub Issues](https://github.com/hesorchen/muselab/issues). - **Version:** 6.0.0 - **Ratified:** 2026-05-31 - **Last amended:** 2026-08-25 - **Derived from:** `docs/architecture.md`, `CONTRIBUTING.md`, `SECURITY.md`, `pyproject.toml`, and the backend/frontend source as of 2026-08-25. Normative keywords **MUST / MUST NOT / SHOULD / MAY** follow RFC 2119. --- ## 1. Core Principles ### P1 — Readable over clever, small over featureful muselab is intentionally small so the whole codebase stays human-readable. Contributions MUST prefer clarity over cleverness. Any change that materially increases conceptual surface area MUST justify itself against this principle. ### P2 — Clone-and-run: no build step, ever The frontend MUST remain runnable by editing a file and refreshing the browser. - No bundler / transpiler / npm install step MAY be introduced (webpack, vite, esbuild, tsc, etc.). - Third-party browser libs MUST be vendored under `frontend/vendor/` with their license recorded in `THIRD_PARTY_LICENSES.md`. - Frontend code MUST be written in the dialect Alpine.js v3 + modern browsers understand directly — no compile step is allowed to bridge the gap. ### P3 — SDK over raw API muselab drives Claude through the **Claude Agent SDK** (the same engine as Claude Code), never the raw Messages API directly. This is what makes MCP, Skills, Subagents, plan mode, and `CLAUDE.md` auto-load behave uniformly across every provider. New capabilities MUST be expressed through SDK-native mechanisms rather than bypassing the SDK. ### P4 — Workspaces belong to the user; the repo never touches them The install root and workspace roots remain separate (see §2). Code MUST NOT write its own state into a user workspace except under the default workspace's reserved `/.muselab/` path and each workspace's recoverable trash. Installation, upgrade, and migration MUST NOT depend on user files. ### P5 — Whole-file as the unit of input The assistant reaches user files on demand via Read / Grep / Edit. muselab MUST NOT pre-embed, pre-index, or RAG-chunk user workspaces. Context comes from the SDK-discovered `CLAUDE.md` hierarchy for the active cwd plus on-demand reads. ### P6 — Personal data is radioactive in shipped artifacts This is an open-source repo. No real personal data MAY appear in code, docs, commits, test fixtures, examples, or README copy. Tests MUST run against a throwaway workspace directory. --- ## 2. Architecture Invariants These are non-negotiable structural facts. A change that violates one is an architecture change and MUST amend this constitution (see §8) before landing. ### A1 — Install root and workspace roots stay separate | Root | What | Backups | |---|---|---| | **repo** (`muselab/`) | code + per-install state (`.env`, `sessions/`) | with the install | | **default workspace** (`MUSELAB_ROOT`) | user files + `.muselab/` application state | independently, without touching the install | | **other registered workspaces** | additional user files and workspace trash | separately | `backend/settings.py` owns `ROOT` (the default workspace), while `backend/workspaces.py` manages other registered roots. Each session stores its cwd and the SDK discovers `CLAUDE.md` from the corresponding hierarchy. ### A2 — Layered backend, one router per concern The backend is FastAPI, mounted in `backend/main.py`. Each domain is one module exposing an `APIRouter`. New surface area MUST follow this one-router-per-concern shape rather than growing a god-module: | Module | Owns | |---|---| | `main.py` | app factory, uvicorn entry, route mounting, static frontend, log token-redaction, asset-version stamping | | `auth.py` | `X-Auth-Token` guard (header or `?token=`) | | `chat.py` | `/api/chat/*` — SDK client pool + SSE turn loop | | `endpoints.py` | provider `CATALOG` + per-request env wiring | | `files.py` | `/api/files/*` — safe-resolve read/write/grep + dustbin | | `workspaces.py` | workspace registration, selection, and directory boundaries | | `sessions.py` | session index, cwd, sidecars, queue | | `terminal.py` / `terminal_worker.py` | `/api/terminals/*`, profiles, WebSocket tickets, and PTY workers | | `scheduler.py` / `api_scheduler.py` | asyncio cron loop + its API | | `push.py` / `api_push.py` | Web Push / VAPID + its API | | `activity.py` / `activity_api.py` | activity-center state + its API | | `transcript_index.py` | transcript index | | `api_settings.py` | `/api/settings` — hot-rewrite `.env` + `os.environ` | | `prompts.py` | short starter messages for self-contained UI workflows | | `ask_user_question.py` | browser state bridge for native `AskUserQuestion` | | `permission_request.py` | tool-permission and interactive-question round-trip | | `settings.py` | `ROOT` / `PORT` / `HOST`, `atomic_write_text`, `env_int` | ### A3 — Per-session env override with config isolation Third-party providers are wired by setting, per request, `ANTHROPIC_BASE_URL` + `ANTHROPIC_API_KEY` + an **isolated** `CLAUDE_CONFIG_DIR`. The isolated config dir is mandatory: it blocks the CLI from silently falling back to Pro OAuth and billing third-party traffic to the user's Anthropic account. Any new provider path MUST preserve all three. ### A4 — Client pool keyed by `(session_id, model, effort, service_tier)` `chat.py` pools `ClaudeSDKClient` instances on exactly this key. The default LRU cap is 3 and may be adjusted through supported runtime configuration. Each assistant message stores its own `model` so badges stay accurate after reload. Effort and service tier are SDK/Gateway process-launch contracts; changing either MUST rebuild the matching session runtime. Changing the pool key is an architecture change (it interacts with MCP spawning — see A6). ### A5 — The UI protects model continuity by default The first real turn establishes the session model. For a non-empty session, the frontend MUST fork when switching to an incompatible model, avoiding cross-vendor thinking-block signatures that can produce unrecoverable `400` errors. The management API MAY explicitly update a session model, but callers own transcript-compatibility risk. Sessions created before a provider existed self-heal to a configured model on first send. ### A6 — MCP: attribute-driven, gated, default-zero - The shipped default configures **zero** MCP servers; every connector is opt-in. Interactive questions use the SDK-native `AskUserQuestion`; no MCP server may be registered solely to duplicate that built-in capability. - Every server (preset or user-added) is stored in `mcp.json` by **attributes** (`transport`, `disabled`, pinned `version`), never a hard-coded catalog. - Versions MUST be pinned. Shipped config MUST NOT use `npx -y latest` / unpinned `uvx`. - The backend **readiness gate** (`_await_mcp_ready` in `chat.py`) MUST hold turn 1 open until every enabled external MCP server reaches a terminal state, to prevent mid-turn toolset changes from wedging the session. The gate MUST be skipped when no external MCP is enabled (`_has_enabled_external_mcp`). - MCP is for **capabilities the built-in tools lack** (external auth'd systems). Anything `Read`/`Edit`/`Grep`/`Glob`/`Bash`/`WebFetch` already covers MUST NOT become an MCP server. ### A7 — MCP vs Skill boundary If the hard part is *connecting & authenticating to an external system* → MCP. If the hard part is *encoding how to do a task well* (connection is just an API key) → Skill (a folder with `SKILL.md` + optional assets, progressive disclosure). New extensions MUST be classified by this rule. ### A8 — Transcripts are owned by the CLI, not muselab Conversation transcripts are owned by the Claude CLI. Claude sessions normally use its default configuration root, while third-party-provider sessions may use isolated configuration roots. muselab's `sessions/` holds only the sidecar metadata layered on top (name, cwd, per-message model badge, cost, attachments). muselab MUST NOT duplicate or fork ownership of the transcript itself. ### A9 — Native instruction ownership muselab MUST NOT inject a global or per-session custom system prompt. Persistent identity, response preferences, and personal context belong in the SDK-discovered `CLAUDE.md` hierarchy. Reusable task workflows belong in Skills, and tool-specific behavior belongs in tool descriptions and enforced permission configuration. UI starter prompts MAY describe a self-contained task without creating another instruction layer. Runtime modes MUST NOT inject a parallel identity, persona, or reusable workflow through system prompts or transient hook context; reusable workflows remain in user, workspace, plugin, generated, or repository-extension Skills. --- ## 3. Technology Stack & Constraints - **Language/runtime:** Python `>=3.12`. Dependency + venv management via `uv`. - **Web:** FastAPI on `starlette>=1.3.1`, served by `uvicorn[standard]`. - **Agent:** `claude-agent-sdk>=0.2.120,<0.3` — the only path to the model. No direct `anthropic` SDK / raw HTTP to model endpoints in app code. - **Frontend:** vanilla HTML + Alpine.js v3 + CSS. No framework, no build (P2). - **Persistence:** flat files (JSON sidecars, JSONL transcripts owned by CLI). No database dependency MAY be introduced for core function. - **Adding a dependency** MUST be justified (new capability, not convenience), pinned with a floor version, and — if it pulls a runtime binary (`npx`/`uvx`) — detected by the install scripts with a warning (CONTRIBUTING checklist). - **Provider integrations** MUST expose an Anthropic-compatible Messages endpoint and land as a single `CATALOG` entry in `endpoints.py`. OpenAI-only protocols are out (the SDK expects Anthropic-compatible). --- ## 4. Code Conventions ### Python - PEP 8. **No formatter is enforced** — do not introduce `black`/auto-format that would churn the existing terse style. - Lint: `ruff` with `select = ["E","F","W"]`, `ignore = ["E501"]`. New code MUST pass `ruff check backend/ tests/`. Opinionated families (B, I, N, UP) MUST NOT be enabled — they'd churn working code for marginal gain. - Type hints on **public** functions; not required everywhere. - Writes to disk MUST go through `atomic_write_text` (or an equally atomic path) — never a bare truncating write to a user-visible file. ### JavaScript / CSS - No transpiler. Write the dialect Alpine v3 + modern browsers run directly. Match the semicolon style of neighbouring code. - CSS: per-component sections with a comment header. Theme via CSS variables (`--c-*`, `--sp-*`); colors MUST NOT be hardcoded. - Editing `app.js` / `styles.css` / `index.html` / `i18n/index.js` / `data/constants.js` participates in asset-version stamping — keep them in the candidate list when adding split-out modules so clients refetch. ### Internationalization User-facing UI strings MUST exist in both `en` and `zh` tables in `frontend/i18n/index.js`. Docs that ship a translation use the `_zh.md` sibling convention. --- ## 5. Security Requirements (Authoritative detail in `SECURITY.md`; these are the constitution-level invariants that code reviews and specs MUST enforce.) - **Auth on every request.** Normal APIs use `X-Auth-Token`. Browser real-time connections MUST first obtain a short-lived, single-use ticket through an authenticated request; compatibility query tokens are reserved for resource requests that cannot send headers. New endpoints MUST NOT bypass the corresponding authentication dependency. - **Files API path traversal is closed.** Every browser file path MUST resolve through the safe-resolve logic in `files.py` and stay within the selected registered workspace. Write/upload/rename/copy MUST refuse the `.muselab-dustbin/` path (`_guard_not_trash`); deletes are soft (move to dustbin), with restore/purge as separate endpoints. - **No secret leakage in logs.** The uvicorn access-log `token=` redaction filter (`_TokenFilter` in `main.py`) MUST stay in place; new log surfaces that could carry tokens/keys MUST be scrubbed the same way. - **Local MCP consent.** Adding a stdio server MUST show the exact untruncated command, warn it runs with app privileges, flag dangerous patterns (`sudo`, `rm -rf`, `curl` to home/SSH paths), and require explicit approval. - **Local HTTP servers** MUST bind `127.0.0.1`, validate `Origin` (DNS-rebinding), and require a token. Prefer remote HTTP connectors over `npx` commands (fewer supply-chain risks). - **Permission boundaries MUST be described accurately.** Files API access is restricted to the selected registered workspace. Agent tools and real terminals run with service-user permissions and MUST NOT be described as the same path sandbox. Terminals MUST use same-origin checks, single-use tickets, and a safe environment allowlist. - **No secrets** in code, commits, or test fixtures. `.env` and `sessions/` remain gitignored and MUST NOT be added. --- ## 6. Testing & Quality Gates A change is not done until these pass (CONTRIBUTING checklist is the enforced form): - [ ] `uv run pytest tests/` green. - [ ] `uv run ruff check backend/ tests/` clean (CI blocks merge on lint fail). - [ ] `bash scripts/lint.sh` clean (encoding / BOM / class-collision). - [ ] **Every backend change adds or updates a test** in `tests/`. Bug fixes MUST ship a regression test. - [ ] Security-relevant changes (auth, path resolution, MCP) extend `test_security.py` / `test_files.py` / `test_mcp_gate.py` as applicable. - [ ] Frontend visual changes documented with a before/after note in the PR (no visual-regression harness yet). - [ ] No secrets; no additions to `.env` / `sessions/`. - [ ] Tests MUST pass against a throwaway workspace — never real personal data. --- ## 7. Scope Boundaries (Non-Goals) muselab is a **self-hosted, workspace-bound Agent workbench**, not a generic hosted AI platform. The following MUST be declined absent an explicit constitution amendment: - A build step of any kind (violates P2). - A document-RAG / crawled-content pipeline over user workspaces (violates P5). - Generic chat-UI features unrelated to workspace execution (hosted accounts, social feeds, etc.). - OpenAI-only protocol providers (violates §3 / A3). - Presetting heavy/developer-only or write/trade-capable MCP servers by default (violates A6 — e.g. GitHub MCP, DB-write, brokerage). - Any feature that requires real personal data to test (violates P6 / §6). --- ## 8. Governance - **This document outranks code, comments, and habit.** A PR that contradicts an invariant here is wrong until either the PR changes or this document is amended in the same PR with reviewer sign-off. - **Specs derive from this constitution.** Each feature/refactor SHOULD carry a short spec (what changes, boundaries, acceptance criteria in EARS form: "When , the system shall "). Specs MUST NOT restate invariants — they reference them. - **Amending the constitution:** bump the version (semver — MAJOR for removing/ redefining an invariant, MINOR for adding one, PATCH for clarifications), update *Last amended*, and note the rationale. Architecture changes (§2) MUST amend before merge, not after. - **Drift check:** when reverse-engineered understanding contradicts this document, treat the contradiction as a finding — fix the code, or amend the doc with rationale. Never silently let them diverge. --- ## 9. Amendment history - **6.0.0 (2026-08-25):** Replaced the always-on in-process question MCP with the SDK-native `AskUserQuestion` browser bridge after upgrading to Agent SDK 0.2.144 / Claude Code CLI 2.1.239 and verifying native answer delivery across default, plan, acceptEdits, and bypass permission modes. The shipped MCP topology is now truly default-zero; connectors remain opt-in. - **5.0.0 (2026-08-15):** Removed the bundled Skill distribution and the runtime-mode Skill activation exception from A9. Skills remain an SDK-native extension mechanism for user, workspace, plugin, reviewed generated, and repository-extension workflows; runtime modes retain only their transport and resource-bound contracts. - **4.0.0 (2026-07-31):** Expanded A4's exact client-pool key with `service_tier` because Fast is fixed in the Gateway header contract at SDK client launch; amended A9 so an explicitly selected runtime mode may activate a bundled Skill through transient `UserPromptSubmit.additionalContext`, while preserving the canonical prompt/transcript and keeping the workflow in `SKILL.md` rather than a custom system prompt. - **3.0.0 (2026-07-31):** Repositioned muselab from its former personal-domain framing to a workspace-bound Agent workbench; removed domain-specific assumptions while retaining local-file, self-hosting, and safety invariants. - **2.0.0 (2026-07-23):** Replaced the single-archive model with registered workspaces; added the real terminal, short-lived real-time tickets, and Activity modules; defined model continuity as frontend fork-by-default with an explicit management-API override; and fixed native instruction ownership across CLAUDE.md, Skills, and tool permissions. --- *A spec tells the AI what to build next. This constitution tells it what must stay true no matter what it builds.*