Zum Inhalt springen

Einen Headless-Server betreiben

Lass cc_server auf einer Maschine laufen, die an bleibt, und verbinde dich dann vom Desktop, aus einem Browser oder vom Telefon aus. Der Server ist ein natives Binary in reinem Dart, ohne Flutter-Engine, und läuft deshalb headless unter macOS, Linux oder Windows. Ihm gehört die Datenbank, er führt die Agenten aus und tätigt jeden externen API-Aufruf. Die Clients zeichnen nur.

Jedes Release veröffentlicht einen selbst gehosteten Stack aus vier GHCR-Images — das Backend, die beiden statischen Thin Clients und das optionale Kopplungs-Relay — plus ein separates Image cc-server-demo für den öffentlichen Demo-Host. Eine selbst gehostete Installation braucht kein ausgechecktes Quell-Repository. Pinne einen Versions-Tag (ghcr.io/samuelalev/<image>:v0.0.1-rc.1) oder nimm latest.

Image Was es ist Veröffentlicht
ghcr.io/samuelalev/cc-server Das Backend: Datenbank, Agenten, MCP, Webhooks -p 9030:9030
ghcr.io/samuelalev/cc-webapp Der Web-Client (statisches nginx) -p 8080:8080
ghcr.io/samuelalev/cc-remote Die Telefon-Begleit-PWA (statisches nginx) -p 8081:8080
ghcr.io/samuelalev/cc-signaling-server Das WebRTC-Kopplungs-Relay (optional) -p 8788:8788

Der ganze Stack als Compose-Datei:

services:
server:
image: ghcr.io/samuelalev/cc-server:latest
ports: ["9030:9030"]
volumes: ["cc_data:/data"]
restart: unless-stopped
web:
image: ghcr.io/samuelalev/cc-webapp:latest
ports: ["8080:8080"]
restart: unless-stopped
remote:
image: ghcr.io/samuelalev/cc-remote:latest
ports: ["8081:8080"]
restart: unless-stopped
signaling:
image: ghcr.io/samuelalev/cc-signaling-server:latest
ports: ["8788:8788"]
restart: unless-stopped
volumes:
cc_data:

Was du wissen solltest, bevor du davon etwas über Loopback hinaus erreichbar machst:

  • Das Image cc-server bringt CC_SERVER_INSECURE=1 mit (es muss 0.0.0.0 binden, damit Verkehr über veröffentlichte Ports ankommt). Das ist nur hinter einem TLS-terminierenden Reverse-Proxy sicher — Caddy, Traefik, nginx oder ein Cloudflare Tunnel — in einem vertrauenswürdigen Netz. Um TLS stattdessen im Prozess zu terminieren, häng ein Zertifikat ein und setze CC_SERVER_TLS_CERT / CC_SERVER_TLS_KEY (und nimm danach CC_SERVER_INSECURE weg). Alles lässt sich mit docker run -e … oder dem Compose-Block environment: überschreiben.

  • Nur /data hält Zustand. Die SQLite-Datenbanken, die Geheimnisse gekoppelter Geräte, heruntergeladene Modelle und das Audio der Besprechungen liegen alle im Volume. Sichere es, dann ist der Container austauschbar.

  • Kopple einen Client aus dem Image heraus, bevor die erste Verbindung steht. Das ist das Container-Gegenstück zu Schritt 3:

    Terminalfenster
    docker run --rm -it -v cc_data:/data \
    --entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair
  • Die beiden Client-Images sind statische nginx-Hosts. Zur Laufzeit ist nichts zu konfigurieren. Das Verbindungsformular jedes Clients nimmt deine Server-URL, die Geräte-ID und den Kopplungsschlüssel. Das Signalisierungs-Relay braucht ebenfalls nur seinen Port (SIGNALING_HOST / SIGNALING_PORT lassen sich überschreiben). Es ist zustandslos, hält keine Geheimnisse und ist ein Rückfall: Ein Telefon nimmt Loopback, LAN, Tailnet oder einen direkten wss://-Weg, sobald einer erreichbar ist, und fällt nur auf das Relay zurück, wenn keiner es ist. Lass es weg, wenn deine Clients den Server immer direkt erreichen.

Der Rest dieser Anleitung baut den Server aus dem Quellcode. Die Flags und das Verhalten, die sie behandelt, sind dieselben, die die Umgebungsvariablen des Images setzen.

  • Das Dart SDK. Dieses Repo pinnt sein SDK mit fvm, deshalb stellst du jedem Befehl fvm voran.
  • Dieses Repository, ausgecheckt.
  • Die native Toolchain, die das Bereitstellungs-Skript braucht — Rust/cargo, dazu meson, ninja, pkg-config, git und ein C++-Compiler. Jedes Build-Skript nennt, was es nicht findet, und wie du es installierst.
  • Ein erreichbarer Host: Loopback für den lokalen Gebrauch, oder eine LAN- oder öffentliche Adresse mit TLS für entfernte Clients.

Die nativen Bibliotheken sind erforderlich. Einen eingeschränkten Modus gibt es nicht. Baue sie vom Wurzelverzeichnis des Repos aus, bevor du irgendetwas anderes tust:

Terminalfenster
scripts/natives/build_natives.sh

Das baut rift, fff, tree-sitter samt Grammatiken, aec, lame, ccpty, cc_watcher, cc_inference und das SAML-Native nach build/natives/. Der Build-Hook von cc_server schreibt, was dort bereitliegt, erneut ins Bundle. Überspringst du diesen Schritt, schlägt der Build entweder sofort fehl, oder es entsteht — mit dem Ausweg unten — eine Binärdatei, deren Startprüfung den Start verweigert und die fehlende Bibliothek nennt.

Terminalfenster
cd apps/cc_server
fvm dart build cli

Das Bundle legt libsqlite3 und die bereitgestellten nativen Bibliotheken neben die Binärdatei. System-SQLite und eine Flutter-Engine brauchst du nicht. Die ausführbare Datei liegt unter ./build/cli/<os_arch>/bundle/bin/cc_server.

Schritt 3: Einen Client vor dem ersten Start koppeln

Abschnitt betitelt „Schritt 3: Einen Client vor dem ersten Start koppeln“

Ein frisches Datenverzeichnis hat kein gekoppeltes Gerät und keinen vorab geteilten Schlüssel, deshalb lässt sich die Abfrage des Kopplungsschlüssels im Client noch nicht erfüllen. Stell einen mit dem Unterbefehl pair aus. Er schreibt global.db und secrets.json direkt. Ein Server, der dieses Datenverzeichnis schon bedient, nimmt das neue Gerät ohne Neustart auf:

Terminalfenster
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030

Er gibt drei Werte aus, die du in das Verbindungsformular eines Clients einfügst:

Connect a thin client (paste into its connect form):
Server ws://localhost:9030/rpc
Device id web-client
Pairing key <a freshly generated key>

Nützliche Flags: --device (Vorgabe web-client; die ID wählt auch die Geräteklasse — desktop, ios, android, alles andere ist ein Web-Client), --label (Vorgabe ist der Plattformname plus der Hostname dieser Maschine), --host (der Host, der in der URL steht, weil Loopback von einer anderen Maschine aus nicht erreichbar ist) und --client-url. Ein erneutes pair rotiert den Schlüssel dieses Geräts und beendet seine laufenden Sitzungen. Die Kopplung legt nie einen Arbeitsbereich an. Das Onboarding des Clients, der sich verbindet, benennt den ersten und legt ihn an.

Mit --client-url gibt er zusätzlich einen QR aus, den du im Terminal scannen kannst. Er öffnet den Web-Client schon ausgefüllt:

Terminalfenster
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030 \
--bind any --host 192.168.1.42 --client-url https://app.usectrl.dev

Eine Zugangsberechtigung von cc_server pair hat kein Ablaufdatum. Eine in der App ausgestellte läuft immer nach 30 Tagen ab. Widerrufe ein über die CLI ausgestelltes Gerät unter Einstellungen → Du → Deine Geräte, sobald du es nicht mehr brauchst.

Auf Loopback, der Vorgabe und allem, was ein Client auf derselben Maschine braucht:

Terminalfenster
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030

Um ihn von einer anderen Maschine zu erreichen, binde jede Schnittstelle. Ein Bind außerhalb von Loopback ohne TLS wird beim Start abgelehnt (Refusing to bind non-loopback address … without TLS). Gib ihm deshalb ein Zertifikat:

Terminalfenster
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem \
--public-url wss://server.example:9030/rpc

Oder terminiere TLS in einem Reverse-Proxy in einem vertrauenswürdigen privaten Netz und gib dahinter einen Klartext-Bind frei:

Terminalfenster
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --insecure --public-url wss://server.example/rpc

--insecure wird ignoriert, wenn --tls-cert und --tls-key beide gesetzt sind. TLS gewinnt immer. Der Server gibt den Klartext-Zustand als insecure: true in GET /healthz bekannt, und Clients kennzeichnen so eine Verbindung mit Unsicher.

Der Server läuft, bis SIGINT oder SIGTERM kommt, und fährt dann sauber herunter.

Richte einen Client auf die Server-URL und den Kopplungsschlüssel aus Schritt 3 aus:

  • Desktop-App: Auf dem Bildschirm beim ersten Start (Wie soll Control Center laufen?) füllst du die Verbindungsfelder aus, statt In dieser App ausführen zu wählen, oder du fügst den Server später unter Einstellungen → Server → Verbindung und Status hinzu. Siehe Mit einem Remote-Server verbinden.
  • Web-Client: Öffne app.usectrl.dev (oder deine eigene Kopie des Web-Builds) und gib Server-URL, Geräte-ID und Kopplungsschlüssel ein.
  • Telefon: Kopple die App Remote von einem laufenden Client aus. Siehe Ein Gerät koppeln.

Schritt 6: Einen Google Calendar verbinden (optional)

Abschnitt betitelt „Schritt 6: Einen Google Calendar verbinden (optional)“

Um den Kalender eines Arbeitsbereichs ohne angeschlossene Oberfläche zu synchronisieren, nutze den Gerätecode-Ablauf:

Terminalfenster
GOOGLE_OAUTH_CLIENT_ID=<id> GOOGLE_OAUTH_CLIENT_SECRET=<secret> \
./build/cli/<os_arch>/bundle/bin/cc_server calendar connect \
--data-dir ./data --workspace <workspace-id>

Er gibt einen Code und eine URL aus, die du auf einem anderen Gerät genehmigst, speichert das Refresh-Token serverseitig und beendet sich. Ein Build aus dem Quellcode bringt keinen Google-Client mit, deshalb sind die Umgebungsvariablen für ID und Secret erforderlich. Ein Flag --google-client-id gibt es nicht. Siehe Einen Google Calendar verbinden.

Die Flags oben sind die, die eine erste Einrichtung braucht. Die vollständige Oberfläche, 23 Paare aus Flag und Umgebungsvariable, die vier Unterbefehle und der Aufbau des Datenverzeichnisses, steht in der cc_server-CLI-Referenz.

Die vier, die du am ehesten brauchst:

Flag Vorgabe Bedeutung
--data-dir das anwendungsbezogene Datenverzeichnis des Betriebssystems je Benutzer Datenbanken, Geheimnisse, Modelle und zwischengespeicherte Medien
--port 9030 TCP-Port (0 = ephemer)
--bind loopback loopback, oder any für jede Schnittstelle (braucht TLS oder --insecure)
--public-url aus dem Bind abgeleitet Die RPC-URL, die gekoppelten Clients bekannt gegeben wird. Setze sie hinter einem Proxy, NAT oder Tunnel ausdrücklich

Das Vorgabe-Datenverzeichnis ist ~/Library/Application Support/control-center unter macOS, %APPDATA%\control-center unter Windows und $XDG_DATA_HOME/control-center (sonst ~/.local/share/control-center) auf den übrigen Systemen. Ein auf das Arbeitsverzeichnis bezogenes .cc_server wird nur genutzt, wenn sich kein Home- oder Anwendungsdatenverzeichnis auflösen lässt.

cc_server ist die ganze Produktoberfläche: der Datenweg für Repo-RPC und die Abonnements, der MCP-Werkzeug-Endpunkt unter POST /mcp und GET /sse auf demselben Port, eingehende Webhooks, die Proxys für Medien und Schriften, die Callbacks für Single Sign-on, das Lease-Protokoll der Fleet, serverseitiges Abrufen von RSS im 30-Minuten-Takt und die Vektorsuche (die Erweiterung sqlite_vector ist als prozessweite Auto-Extension registriert und fällt nur dort auf reine Volltextsuche zurück, wo die Erweiterung fehlt).

Der Dispatcher ist zustandslos. Eine Sitzung bindet keinen Arbeitsbereich. Jeder arbeitsbereichsbezogene Aufruf trägt seine eigene workspace_id, und über den Zugriff entscheiden Mitgliedschaft und Rolle des Aufrufers im Arbeitsbereich. Ein Kopplungsschlüssel allein reicht dafür nicht.

Zwei Dinge, bevor du den Port über Loopback hinaus öffnest:

  • MCP bleibt außerhalb des Hosts fail-closed. Eine Oberfläche ohne Token antwortet nur auf Loopback. any zu binden erweitert sie nicht. Eine Anfrage von außerhalb des Hosts wird abgelehnt mit “MCP requires a bearer token to answer non-loopback clients”, bis du eines konfigurierst (mcp.setToken oder Einstellungen → Server → MCP-Server).
  • GET /healthz ist absichtlich ohne Authentifizierung, mit Wildcard-CORS, und veröffentlicht ID, Name, Identitätsfingerabdruck und Build-Version des Servers. Clients brauchen den Fingerabdruck, um den Server zu pinnen. Das ist so gewollt, und jeder erreichbare cc_server gibt genau das jedem, der fragt.