CyClaw: Local AI you can Trust.

Python 3.12 FastAPI LangGraph CyClaw CI/CD testing

Screenshots: local AI

Table of Contents


What It Does

CyClaw is a personal RAG (Retrieval-Augmented Generation) backend that:

  1. Answers questions exclusively from your local Markdown corpus — no internet by default
  2. Enforces every safety invariant via LangGraph topology — not prompts, not config flags, not discipline
  3. Maintains a persistent soul/personality layer (soul.md) with SHA-256 drift detection, atomic evolution writes, and user-gated modification
  4. Falls back to an external LLM only with explicit user confirmation in hybrid mode — Grok (xAI) or Claude (Anthropic), selected per-query, each independently triple-gated at config, env, and per-query level
  5. Exposes both a FastAPI HTTP gateway and an MCP server for Claude Desktop / Copilot Studio integration
  6. Ships optional, out-of-band operator layers for Dropbox corpus sync (sync/) and agentic GitHub context / governed local workflows (agentic/, .claude/) — never imported into the request path, now also drivable from the browser terminal via governed Sync and Agentic consoles
  7. Extends the agentic layer to local data (v1.8) with an opt-in filesystem connector (agentic/fsconnect/ — scoped held-handle reads + gated writes over local/SMB shares) and a read-only SQL connector (agentic/sqlconnect/ — SELECT-only Postgres/MSSQL scaffold) — both disabled by default and out-of-band
  8. Adds an optional NeMo Guardrails content-safety layer (v1.8, guardrails/) that soft-imports nemoguardrails and degrades to offline heuristic rails — defense-in-depth only, never a routing authority. When guardrails.enabled is the literal true, utils/guardrail_bridge.py wires visible guardrail_input / guardrail_output nodes; see guardrails/README.md
  9. Scaffolds an optional LangChain Deep Agents / governed harness-optimizer layer (v1.9, agentic/deepagent_github/ + agentic/harness_optimizer/) — opt-in, disabled by default, and out-of-band like every other agentic feature above; phases 0-9 are implemented and tested — phases 0-5 (config, workspace tools, mock scoring/acceptance gate) plus phases 6-9 (real subagent wiring, fixture-based GitHub coding evaluator, governed propose/apply), which landed in PR #515 (2026-07-13). Superseded by item 11 below: P10 has since landed a real draft-PR write path and a sandboxed verification executor — the write path's own flag was armed on 2026-08-07, leaving agentic.enabled as the master switch that still ships false
  10. Ships a local coding-harness console (v1.9, harness/ + powershell/ / macos/) — a grok-build-style slash-command console on 127.0.0.1:8790 chatting with the local model over the OpenAI-compatible endpoint, with per-session token tallies, /goal + human-gated /loop, wired /skills and /tools diagrams, and allowlist-only /web (off by default). Home layout under %USERPROFILE%\.CyClaw (Windows) or ~/.CyClaw (macOS/Linux). Same I6 isolation as every other out-of-band layer. See harness/README.md
  11. Adds a real-repo GitHub agentic coding harness (v1.9, agentic/real_repo_loop.py + agentic/executor/) — clone → plan → patch → verify → human decides → commit, with pushing a claude/* branch and opening a draft PR as two further separate decisions; a diff-scope gate refuses candidates that rewrite the tests judging them, verification runs as sandboxed argv-list subprocesses, and the layer ships off — agentic.enabled: false is the master switch, and since the operator enablement of 2026-08-07 it (plus per-call reason/confirm) is what holds the draft-PR step, plus allow_git_write_tools: false for push
  12. Adds an optional per-user authentication layer (gate_auth.py + utils/authn*, docs/AUTHENTICATION_DESIGN.md) — scrypt password hashes, session cookie + CSRF for browsers, bearer device tokens for programmatic clients, and the cyclaw-user console script for account/token management. Beyond login/logout/whoami, gate_auth.py also carries a role-based admin surface (§12 "Roles" — three roles, admin/operator/audit, with the last enabled admin protected from disable/delete/role-change): /auth/users list/create, /auth/password self-service, /auth/users/{username}/password|role|disable|enable, DELETE /auth/users/{username}, and /auth/audit/summary. Every /auth/* route exists regardless of auth.enabled and returns 503 (not 404) when it's off, so route presence never discloses whether the feature is enabled. When auth.enabled is true, POST /query and the console require a session or named device token. The shipped default leaves /query open.
  13. Adds an optional facts + episodes memory store (gate_memory.py + memory/, package memory/README.md, plan in docs/memory/README.md) — SQLite+FTS5-backed, with propose/apply governance (a non-empty human reason plus an injection scan on apply, parallel to soul's I5) and an optional retrieval-fusion hook. Every memory: switch ships false; mutating routes require the same Bearer CYCLAW_API_KEY as the other admin endpoints. Not docs/memories/ (sandbox notes)
  14. Ships an optional Telegram channel (v1.9, telegram/, shipped enabled: false) — an out-of-band phone remote. Shipped YAML is mode: "chat" (allowlisted long-poll); mode: "notify" remains the T1 outbound-only option. Operator advice is still T1-first before leaving a poller up. Inbound text only ever reaches the RAG pipeline through loopback POST /query — never a direct call into graph.py. T3 hybrid-confirm (allow_hybrid_confirm, default off) is the only way chat text can set user_confirmed_online — one-shot, via the exact private-chat command /online on <grok|claude> — and T4 media staging (media.enabled, default off) writes only through the existing agentic/fsconnect path. See docs/channels/TELEGRAM_DESIGN.md

Architecture

User Query (HTTP POST /query or MCP tool call)
         │
         ▼
    ┌─────────────────────────────────────────────────────┐
    │  gate.py  (FastAPI, 127.0.0.1:8787)                 │
    │  • Rate limit (60 req/min per IP — RUNS FIRST)      │
    │  • Injection filter (sanitizer.py, config-driven)   │
    │  • Soul init (PersonalityManager closure)           │
    │  • Telemetry kill block (before any SDK import;     │
    │    shared — MCP + indexer apply the same block)     │
    └──────────────────┬──────────────────────────────────┘
                       │
                       ▼
    ┌─────────────────────────────────────────────────────┐
    │  graph.py  (LangGraph 12-node State Machine)        │
    │                                                     │
    │  [ENTRY]                                            │
    │     ↓                                               │
    │  1. retrieve  (Chroma + BM25 + RRF fusion)          │
    │     ↓                                               │
    │  2. route_by_score  (top_score >= 0.028 RRF?)       │
    │     ├─ YES ──→ 3. guardrail_input (offline rail;    │
    │     │           opt-in, pass-through when disabled) │
    │     │           blocked ──→ 12. audit_logger        │
    │     │           passed  ──→ 4. local_llm            │
    │     │                        (Ollama :11434)        │
    │     └─ NO  ──→ 5. user_gate (needs_confirm=true)    │
    │                    ├─ not yet answered ──→          │
    │                    │     12. audit_logger           │
    │                    ├─ confirmed + hybrid ──→        │
    │                    │      6. pre_action_hook_grok   │
    │                    │      7. grok_fallback OR       │
    │                    │      8. pre_action_hook_claude │
    │                    │      9. claude_fallback        │
    │                    └─ declined / offline ──→        │
    │                       3. guardrail_input (again)    │
    │                           blocked ──→ 12. audit_logger│
    │                           passed  ──→               │
    │                           10. offline_best_effort   │
    │     ↓ (all four answer nodes converge)              │
    │  11. guardrail_output (offline output rail; opt-in; │
    │     grounding check applies to local_llm answer only)│
    │     ↓                                               │
    │  12. audit_logger (SHA-256 + PII redact → jsonl)    │
    │     ↓                                               │
    │  [END]                                              │
    └─────────────────────────────────────────────────────┘
                       │
                       ▼
    ┌─────────────────────────────────────────────────────┐
    │  HybridRetriever  (retrieval/hybrid_search.py)      │
    │  • ChromaDB  (semantic, all-MiniLM-L6-v2, 384d)    │
    │  • BM25Okapi (keyword, Porter stemming)             │
    │  • RRF fusion (k=60, equal 1.0/1.0 weighting)      │
    │  • Per-chunk provenance metadata in every result    │
    └─────────────────────────────────────────────────────┘

LangGraph Topology (rendered)

flowchart TD
    A(["🌐 Client\nHTTP POST /query\nor MCP tool call"])
    A --> B

    subgraph GATEWAY ["gate.py — FastAPI 127.0.0.1:8787"]
        B["TrustedHostMiddleware\nHost header allowlist"]
        B --> C["Rate Limiter\n60 req/min per IP"]
        C --> D["Prompt Injection Filter\n40 patterns · config-driven · lru_cache"]
        D --> E["Build GraphState\nquery + user_confirmed_online"]
    end

    E --> F

    subgraph GRAPH ["graph.py — LangGraph 12-node State Machine"]
        F(["① retrieve\nChroma + BM25 + RRF"])
        F --> G["② route_by_score\ntop_score ≥ 0.028?"]
        G -->|"YES — local context"| X["③ guardrail_input\noffline rail · opt-in\npass-through when disabled"]
        X -->|"blocked"| L
        X -->|"passed · high score"| H["④ local_llm\nOllama :11434\nqwen3.8:27b-mlx"]
        G -->|"NO — vault miss"| I["⑤ user_gate\nneeds_confirm = true"]
        I -->|"confirmed=true + hybrid\n+ grok.enabled + provider=grok"| PG["⑥ pre_action_hook_grok\nsync · disabled=pass-through\nexit 2 → deny"]
        PG -->|"exit 0 → allow"| J["⑦ grok_fallback\nxAI grok-4.5\ntriple-gated · not railed"]
        I -->|"confirmed=true + hybrid\n+ claude.enabled + provider=claude"| PC["⑧ pre_action_hook_claude\nsync · disabled=pass-through\nexit 2 → deny"]
        PC -->|"exit 0 → allow"| W["⑨ claude_fallback\nAnthropic claude-sonnet-5\ntriple-gated · not railed"]
        I -->|"confirmed=false\nor offline mode"| X
        X -->|"passed · vault miss"| K["⑩ offline_best_effort\nlocal LLM · no RAG gate"]
        I -->|"confirmed=None — PAUSE\nreturn needs_confirm to the client"| L
        H --> Y["⑪ guardrail_output\noffline rail · opt-in\ngrounding check: local_llm only"]
        J --> Y
        W --> Y
        K --> Y
        Y --> L
        PG -.->|"deny"| L
        PC -.->|"deny"| L
        L(["⑫ audit_logger\nSHA-256 hash · PII redact\n→ logs/audit.jsonl"])
    end

    L --> M(["📤 QueryResponse\nanswer · sources · model_used\nretrieval_mode · needs_confirm"])

    subgraph RETRIEVAL ["retrieval/hybrid_search.py"]
        N["ChromaDB\nsemantic · 384-dim cosine"]
        O["BM25Okapi\nkeyword · Porter stemming"]
        P["RRF fusion\nk=60 · equal weighting"]
        N --> P
        O --> P
    end

    F <-->|"hybrid search"| P

    subgraph SOUL ["utils/personality.py"]
        Q["soul.md\nSHA-256 drift detection"]
        R["SQLite / Postgres\nversion history · TTL prune"]
        Q <--> R
    end

    H <-->|"soul preamble\n≤ 8000 chars"| Q
    K <-->|"soul preamble"| Q

    subgraph OOB ["Out-of-band — never imported by gate/graph/MCP"]
        S["agentic/cli.py\nGitHub read ops"]
        T["agentic/fsconnect/\nscoped FS read/write"]
        U["sync/cli.py\nDropbox corpus pull"]
        V["guardrails/\nNeMo rails skeleton"]
    end

    style GATEWAY fill:#1a3a5c,color:#ffffff,stroke:#4a90d9
    style GRAPH fill:#1a3a2a,color:#ffffff,stroke:#4a9d5a
    style RETRIEVAL fill:#3a2a1a,color:#ffffff,stroke:#d9904a
    style SOUL fill:#3a1a3a,color:#ffffff,stroke:#d94ad9
    style OOB fill:#2a2a2a,color:#aaaaaa,stroke:#666666,stroke-dasharray:5 5
    style J fill:#5c1a1a,color:#ffffff
    style W fill:#5c1a1a,color:#ffffff
    style L fill:#1a1a3a,color:#ffffff

API Key Setup (Soul Mutations)

CyClaw's soul mutation endpoints (/soul/propose, /soul/apply, /soul/reload, /soul/restore) require a Bearer API key. Without it they return HTTP 401 immediately — intentional fail-closed behavior.

All /soul/* endpoints — including GET /soul — require a valid Authorization: Bearer <key> token. Only /health, /query, POST /auth/login (issues the session itself; 503 when auth.enabled is false), and the console pages (GET /, /static/*) are unauthenticated.

Opting out entirely: config.yaml's security.api_key_optional (default false) removes the CYCLAW_API_KEY requirement from every route above and the harness console's guarded routes (agent run/push/publish included), for both apps at once — but only for requests arriving from this machine. The bypass is granted on the socket peer, so a remote caller still needs the real key no matter how the process was launched. Entries in security.allowed_hosts do not change that: that list filters request Host headers and opens no listening socket. What would matter is the bind itself — gate.py refuses to start with a non-loopback api.host while the flag is true, and config-guard's C13 warns on that pair. Note it also does nothing under Docker: NAT rewrites the source address, so the container sees the bridge gateway rather than loopback and the routes stay key-gated (set CYCLAW_API_KEY in the container instead).

macOS — zsh (the default shell) or bash

Set for the current Terminal tab. Generate a real value instead of typing one — openssl ships with macOS:

export CYCLAW_API_KEY="$(openssl rand -hex 20)"
echo "$CYCLAW_API_KEY"        # copy it; you paste this into the console UI
uvicorn gate:app --host 127.0.0.1 --port 8787

Persist it. macOS has defaulted to zsh since Catalina, so that means ~/.zshrc unless you switched — check with echo $SHELL first:

echo 'export CYCLAW_API_KEY="your-strong-local-secret"' >> ~/.zshrc
source ~/.zshrc
echo "$CYCLAW_API_KEY"        # confirm it survived
uvicorn gate:app --host 127.0.0.1 --port 8787

On bash, append it to the first existing login file in this order: ~/.bash_profile, ~/.bash_login, ~/.profile. Create ~/.bash_profile only when none exists; macOS bash login shells do not read ~/.bashrc.

Full macOS walkthrough — including launching the harness console beside the gateway and exercising every REST endpoint with curl — is in setup-guide.md.

Linux — bash / zsh

Set for the current session:

export CYCLAW_API_KEY="your-strong-local-secret"
uvicorn gate:app --host 127.0.0.1 --port 8787

Persist in your shell profile (~/.bashrc, ~/.zshrc, or ~/.profile):

echo 'export CYCLAW_API_KEY="your-strong-local-secret"' >> ~/.bashrc
source ~/.bashrc
uvicorn gate:app --host 127.0.0.1 --port 8787

Windows — PowerShell

(Windows is the fallback path; CyClaw is developed and verified on macOS first. Everything below still works and is CI-covered on windows-latest.)

Set for the current session only (cleared on terminal close):

$env:CYCLAW_API_KEY = "your-strong-local-secret"
uvicorn gate:app --host 127.0.0.1 --port 8787

Persist API key across sessions (writes to the current user's environment permanently):

[System.Environment]::SetEnvironmentVariable(
    "CYCLAW_API_KEY",
    "your-strong-local-secret",
    [System.EnvironmentVariableTarget]::User
)
# Restart your terminal, then launch normally:
uvicorn gate:app --host 127.0.0.1 --port 8787

Verify it is set before launching:

echo $env:CYCLAW_API_KEY

https://cyclaw-keygen.grok.me

Windows — Command Prompt (cmd.exe)

set CYCLAW_API_KEY=your-strong-local-secret
uvicorn gate:app --host 127.0.0.1 --port 8787

Persist permanently (takes effect in new sessions):

setx CYCLAW_API_KEY "your-strong-local-secret"

Windows Server 2022 — System-wide (all users, requires admin)

[System.Environment]::SetEnvironmentVariable(
    "CYCLAW_API_KEY",
    "your-strong-local-secret",
    [System.EnvironmentVariableTarget]::Machine
)

Or via GUI: System Properties → Advanced → Environment Variables → System variables → New.

All platforms — .env file (already in .gitignore)

Create .env in the repo root:

# Keys live here, never in config.yaml — config.yaml only names which
# provider is enabled; the key itself is read from the environment.
CYCLAW_API_KEY=your-strong-local-secret
GROK_API_KEY=your-xai-key-or-dummy-when-offline
ANTHROPIC_API_KEY=your-anthropic-key

The Claude variable is ANTHROPIC_API_KEY, not CLAUDE_API_KEYllm/client.py and agentic/config.py both read the former, and nothing in the codebase reads the latter. Setting the wrong name is silent: Claude simply reports unavailable and the query falls back to a local answer.

Load it before launching:

# Bash / Zsh
export $(grep -v '^#' .env | xargs)
uvicorn gate:app --host 127.0.0.1 --port 8787
# PowerShell
Get-Content .env | ForEach-Object {
    if ($_ -match '^([^#=][^=]*)=(.*)$') {
        [System.Environment]::SetEnvironmentVariable($Matches[1].Trim(), $Matches[2].Trim())
    }
}
uvicorn gate:app --host 127.0.0.1 --port 8787

Choosing an API key value

CyClaw is loopback-only (127.0.0.1:8787) — the key never crosses a network. Still:

  • Use at least 20 random characters: openssl rand -hex 20 (Linux/macOS) or [System.Web.Security.Membership]::GeneratePassword(24,4) (PowerShell)
  • Do not reuse a password from elsewhere
  • Do not commit the key to Git (.env is already in .gitignore)
  • Don't forget to set the api key via terminal on Mac or env var in Windows or the web app will not recognize it.

Quick Start

Prerequisites

Requirement Version Notes
Python 3.12 Primary supported runtime
Ollama Any Must be running on localhost:11434
Model pulled in Ollama qwen3.8:27b-mlx (default), mistral:7b, or any chat model
macOS (primary) 14 Sonoma+ Apple Silicon only. An Intel Mac cannot install this repo's pinned torch at all — no x86_64 wheel is published at that pin
Windows / Linux (fallback) Both fully supported and CI-covered; they share the +cpu torch path below

Optional local-backend failover. Ollama is the primary local backend; CyClaw can also fail over to LM Studio (or any other OpenAI-compatible loopback server) if Ollama isn't reachable. It's off by default — set models.local_llm.fallback.enabled: true in config.yaml and fill in your LM Studio model id (fallback.model; LM Studio ids don't carry the Ollama-style name:tag colon, so don't just reuse qwen3.8:27b-mlx). When enabled, a short probe (fallback.probe_timeout_sec, default 1.5s) tries Ollama first and LM Studio second, both LocalLLMClient and /health share the same choice, and it re-checks automatically if neither backend was reachable the first time. See llm/client.py's resolve_local_backend for the full resolution order.

Docker (optional runtime image)

Prefer GHCR when you want a prebuilt linux/amd64 runtime without a local pip install. Pull ghcr.io/cgfixit/cyclaw and run with the existing compose hardening (loopback publish, read-only rootfs, seccomp builtin). Full operator guide: docs/DOCKER.md.

export CYCLAW_IMAGE_TAG=1.9.0
docker compose pull && docker compose up -d
curl -sS http://127.0.0.1:8787/health

Native install (below) remains the primary path for Apple Silicon and for the coding harness.

Install — macOS (Apple Silicon)

macOS is the primary supported platform, and it needs a different torch step than Windows/Linux: the +cpu local-version wheel does not exist for macOS, and both manifests hardcode that pin, so the generic block fails twice on a Mac.

git clone https://github.com/CGFixIT/CyClaw
cd CyClaw
python3.12 -m venv .venv
source .venv/bin/activate

# 1) torch FIRST, and PLAIN — no +cpu suffix, no --index-url override.
#    Apple Silicon has one arm64 wheel; there is no CPU/CUDA build to pick between.
pip install "torch==2.13.0"

# 2) Everything else, from copies of both manifests with the torch and
#    PyTorch-index lines stripped out. Same thing CI's macos-latest leg runs.
grep -v -e '^torch==' -e '^--extra-index-url https://download.pytorch.org' \
    requirements.txt > /tmp/requirements-macos.txt
grep -v '^torch==' constraints.txt > /tmp/constraints-macos.txt
pip install -r /tmp/requirements-macos.txt -c /tmp/constraints-macos.txt \
    --ignore-installed PyYAML

Just cloned on Apple Silicon? bash ./macos/setup-from-clone.sh is the one-shot path: installer + Keychain keys (Telegram / Claude / Grok / GitHub)

  • Ollama check + retrieval index + both servers. It chains the existing macos/ scripts rather than reimplementing them. Flags and privacy notes: macos/README.md.

Prefer the installer only? bash ./macos/install-cyclaw.sh branches on uname -s and handles the torch difference for you — it prepares the harness console, and the installed cyclaw command also boots the RAG gateway on :8787, which stays degraded (503 on /query) until you do the Ollama / index / API-key steps yourself — the installer skips all three. The tradeoffs are tabulated in setup-guide.md.

Install — Windows / Linux (fallback)

git clone https://github.com/CGFixIT/CyClaw
cd CyClaw
python3.12 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

# 1) Install CPU-only torch first (CVE-2025-32434 fixed in 2.6.0; 2.13.0 is within the patched range)
pip install torch==2.13.0+cpu --index-url https://download.pytorch.org/whl/cpu

# 2) Install the rest, pinned to the verified transitive tree.
#    --ignore-installed PyYAML avoids a resolver conflict with a system-level
#    PyYAML some platforms preinstall outside pip's own tracking.
pip install -r requirements.txt -c constraints.txt --ignore-installed PyYAML

Every optional feature in one environment (any platform)

For a from-scratch dev box or a full manual smoke test — Postgres/pgvector, NeMo Guardrails, dev/test tools, and both cloud providers — substitute step 2 with:

pip install -e ".[all]" -c constraints.txt

Required local prep

mkdir -p index logs   # optional — gate.py/the retriever/logger self-create these on first run
export GROK_API_KEY=dummy

data/personality/soul.md ships committed to git with CyClaw's real personality already in place — do not recreate it from a placeholder on a fresh clone. If it's ever deleted, PersonalityManager self-heals with a generic default, but that's a recovery path, not the normal first-run state.

Run

CyClaw ships two independent local web apps. Neither starts the other; run whichever you need, or both in separate terminal tabs.

# The RAG gateway — serves static/terminal.html at / plus the whole REST API
python -m retrieval.indexer                          # once, before the first /query
uvicorn gate:app --host 127.0.0.1 --port 8787        # → http://127.0.0.1:8787

# The coding-harness console — serves static/harness.html
python -m harness.server                             # → http://127.0.0.1:8790

The cyclaw-* short names need a self-install. cyclaw-server, cyclaw-harness, cyclaw-index, cyclaw-mcp, cyclaw-metrics, and cyclaw-clear-cache are [project.scripts] console scripts, and pip writes those shims only when the CyClaw project itself is installed. The install steps above install requirements.txt — a third-party pin list with no self-install line — so those names are command not found after them. Add pip install -e . -c constraints.txt if you want them; otherwise use the python -m … forms, which always work and are what both shipped launchers use.

Override the harness port with CYCLAW_HARNESS_PORT=8795 python -m harness.server, not a CLI flag; it refuses to bind a non-loopback address.

uvicorn harness.server:app also works, via a lazy module-level app (harness/server.py builds it on first attribute access rather than at import, so merely importing the module never touches ~/.CyClaw). Prefer the -m form anyway: the uvicorn form bypasses the bind-address guard, so --host 0.0.0.0 opens a public socket that python -m harness.server would have refused. TrustedHostMiddleware still rejects any non-loopback Host header either way, so the containment holds — but one layer fewer.

Open / for the terminal UI and /health for readiness. The terminal exposes five operator consoles — Soul, Sync, Agentic, Filesystem, and SQL — the latter four calling POST /ops/sync, /ops/agentic, /ops/fsconnect, and /ops/sqlconnect (API-key gated, rate-limited, audited).

Every gateway route with a copy-pasteable curl invocation, plus what each status code means, is in setup-guide.md.


Project Structure

CyClaw/
├── gate.py
├── gate_ops.py                 # /ops/* endpoints (sync/agentic/fsconnect/sqlconnect subprocess shims)
├── gate_auth.py                # /auth/* endpoints — session cookie + CSRF, bearer device tokens
├── gate_memory.py              # /memory/* + /query/export/html — optional, default-off memory admin surface
├── graph.py
├── metrics.py                  # audit.jsonl analyzer (cyclaw-metrics)
├── config.yaml                 # single source of truth
├── README.md
├── mcp_hybrid_server.py        # retrieval-only MCP server
├── memory/                     # optional facts + episodes store (default-off)
│   ├── README.md               # package pointer (not docs/memories/)
│   ├── store.py                # SQLite + FTS5 backend for facts/episodes
│   ├── policy.py               # propose/apply governance (reason required, injection scan)
│   ├── retrieval_adapter.py    # optional fusion hook into hybrid retrieval
│   ├── mirror.py               # episode staging (lazy, non-fatal)
│   ├── consolidation.py        # stub — stay false in v1
│   └── models.py               # typed request/response shapes
├── agentic/                    # out-of-band GitHub context + governed registry (see README.md)
│   ├── cli.py
│   ├── context.py
│   ├── gh_client.py
│   ├── registry.py
│   ├── writer.py               # gh pr create --draft; armed flag, held by agentic.enabled
│   ├── real_repo_loop.py       # (v1.9 P10) clone → plan → patch → verify → human decides → commit
│   ├── executor/               # sandboxed argv-list check runner; soft sandbox, not a kernel boundary
│   ├── fsconnect/              # (v1.8) local/SMB filesystem connector
│   │   ├── cli.py
│   │   ├── client.py           # scoped reads (fs_list/stat/read/grep)
│   │   ├── pathsafe.py         # held-handle containment core (POSIX + Windows reads)
│   │   ├── writer.py           # gated, atomic writes (default-disabled)
│   │   └── indexer.py          # toggleable RAG-corpus indexing of the share
│   ├── sqlconnect/             # (v1.8) read-only SQL scaffold (Postgres/MSSQL)
│   │   ├── cli.py
│   │   └── client.py           # SELECT-only query guard, env-only DSN
│   ├── harness_optimizer/      # (v1.9) governed better-harness-style optimizer scaffold
│   │   ├── core.py             # Experiment/Surface/RunReport/CandidateDecision models
│   │   ├── proposer.py         # scoped train/holdout workspace builder
│   │   ├── mcp/tools.py        # audited, symlink-hardened proposer workspace tools
│   │   └── governance.py       # visible-case-hardcoding + governance-finding gates
│   └── deepagent_github/       # (v1.9) workspace tools + cloud planner; DeepAgents subgraph retired
│       ├── repo_workspace.py   # live: jailed workspace tools (clone/read/write/commit/push) used by real_repo_loop
│       ├── chat_client.py      # live: cloud-provider planner adapter (Grok/Claude)
│       ├── builder.py          # retired DeepAgents subgraph (2026-07-31) — kept, not deleted
│       ├── permissions.py      # phase-5 no-write policy refusal
│       └── subagents.py        # validated SubAgent specs, no bare-string tools
├── guardrails/                 # (v1.8) opt-in rails; graph nodes via guardrail_bridge
│   ├── README.md
│   ├── cli.py
│   ├── config.py
│   ├── integration.py          # soft-imports nemoguardrails; degrades gracefully
│   ├── rails.py                # offline heuristic rails (injection/soul/grounding)
│   ├── metrics.py              # separate logs/guardrails.jsonl stream (hashes only)
│   └── config/                 # NeMo config.yml + rails.co (Colang flows)
├── harness/                    # (v1.9) coding console on 127.0.0.1:8790 (see harness/README.md)
│   ├── README.md               # slash-command usage (/goal /loop /skills /tools /web)
│   ├── server.py               # FastAPI control plane (cyclaw-harness)
│   ├── sessions.py             # JSON session store with per-session token tallies + /goal
│   ├── ollama.py               # loopback-only OpenAI-compatible /v1 chat client
│   ├── config.py               # ~/.CyClaw (or %USERPROFILE%\.CyClaw) home layout
│   ├── prompts.py              # ponytail + karpathy (+ optional soul, /goal, /web extract)
│   ├── registry_view.py        # merged catalog (AST-parses MCP tools; I6)
│   ├── tools_view.py           # /tools wiring diagram (live routes vs MCP catalog)
│   ├── skills_view.py          # /skills wiring diagram (prompt + agent-check vs catalog)
│   ├── web_search.py           # allowlist-only GET; off by default; no search engine
│   ├── agent_policy.py         # check-profile allowlist — console sends profile names, never argv
│   └── schemas.py              # request models
├── telegram/                   # (v1.9) optional Telegram channel (out-of-band), shipped enabled: false
│   ├── cli.py
│   ├── client.py               # Bot API client — outbound notify + long-poll inbound chat
│   ├── config.py               # loads config.yaml's `telegram:` block
│   ├── runner.py                # long-poll loop; answers via loopback POST /query only
│   ├── state.py                 # T3 hybrid-confirm consent state (default off)
│   ├── media.py                 # T4 attachment staging via agentic/fsconnect (default off)
│   └── ratelimit.py
├── powershell/                 # Windows installer/launcher for the harness
│   ├── Install-CyClaw.ps1      # home + venv + PATH shim + profile function
│   ├── Invoke-CyClaw.ps1
│   └── Uninstall-CyClaw.ps1
├── macos/                      # macOS/Linux installer/launchd glue (see macos/README.md)
│   ├── setup-from-clone.sh     # one-shot after git clone (Apple Silicon)
│   ├── install-cyclaw.sh
│   ├── uninstall-cyclaw.sh
│   ├── invoke-cyclaw.sh        # gate :8787 + harness :8790
│   ├── setup-cyclaw-keys.sh    # Keychain + ~/.CyClaw/.env (never config.yaml)
│   ├── setup-fsconnect.sh      # confined ~/CyClaw-FS list/stat/read
│   ├── cyclaw-keychain-*.sh    # Keychain inject/store for launchd jobs
│   ├── generate_service_plist.py  # supervised gate/harness LaunchAgent — requires --confirm + --reason, never loads
│   └── LaunchAgents/           # templates only — never auto-loaded
├── .claude/                    # local operator workflows and prompts
│   ├── commands/
│   ├── hooks/
│   ├── memory/
│   ├── patterns/
│   ├── rules/
│   ├── skills/
│   ├── tools/
│   └── utility-prompts/
├── retrieval/
│   ├── indexer.py
│   ├── hybrid_search.py
│   ├── embeddings.py
│   ├── stemmer.py
│   ├── vector_store.py         # pluggable: embedded ChromaDB (default) or pgvector; sole Chroma chokepoint
│   └── clear_cache.py          # dry-run-by-default embedding-cache cleaner (cyclaw-clear-cache)
├── llm/
│   └── client.py
├── sync/                       # optional Dropbox corpus sync
│   ├── cli.py
│   ├── runner.py
│   └── scheduler.py
├── utils/
│   ├── sanitizer.py
│   ├── logger.py
│   ├── personality.py
│   ├── health.py
│   ├── ratelimit.py
│   ├── launchd_plist.py        # stdlib-only plist builder used by every launchd generator
│   ├── guardrail_bridge.py     # only bridge from graph.py to guardrails/ (never a direct import)
│   └── telemetry_kill.py       # shared kill block — applied by gate.py, mcp_hybrid_server.py, retrieval/vector_store.py
├── schemas/                    # Pydantic API models (api.py; extra='forbid', strict)
├── scripts/                    # install-githooks.sh, check-pr-template.sh
├── deploy/                     # apparmor/ falco/ seccomp/ container-hardening profiles (all opt-in)
├── tests/
├── docs/
├── static/
├── data/
│   ├── corpus/
│   ├── personality/
│   └── agentic/                # skills_registry.json — governed store, ships empty
└── .github/workflows/

Every top-level package and directory above now carries its own README.md (map + traps + links to its authoritative doc); the tree omits most of them for brevity.


Dropbox Corpus Sync

CyClaw includes an optional, out-of-band Dropbox sync layer that mirrors a Dropbox corpus into data/corpus/ without touching gate.py, graph.py, or the MCP request path.

Key capabilities

  • rclone-backed pull sync with safety fuses (max_delete, max_transfer)
  • crash-safe single-instance locking — an OS-backed lock (fcntl.flock / msvcrt.locking) prevents a scheduled run and a manual run from racing, and releases automatically even if the process dies
  • audit logging for changed corpus files
  • optional scheduler integration for Linux, macOS, and Windows — cron / Windows Task Scheduler by default, plus an opt-in Darwin-only launchd backend (sync.scheduler_backend: "launchd", schedule_frequency daily/weekly/monthly) that generates the plist and prints the launchctl bootstrap command, never loading it itself
  • optional reindex trigger when corpus changes

Core commands

python -m sync.cli setup          # first-run bootstrap
python -m sync.cli test
python -m sync.cli sync --dry-run
python -m sync.cli sync
python -m sync.cli status
python -m sync.cli schedule
python -m sync.cli unschedule

The same actions are available from the Sync Console panel in the terminal UI via POST /ops/sync (loopback-only, API-key gated, audited).

See docs/! How-To-Guides/Dropbox_Sync_Guide.md for full setup and scheduling details, and docs/SYNC_README.md for module internals (lock lifecycle, exit codes, error taxonomy).


macOS launchd & Keychain (v1.9)

CyClaw's scheduled and supervised jobs on macOS run through generated launchd LaunchAgents with one uniform posture: every generator writes a plist from real resolved install paths and prints the exact launchctl bootstrap command — none of them ever loads the agent itself. Loading a background job is always a separate, explicit operator action.

Key capabilities

  • Secrets never land in a plist. Token-bearing jobs chain macos/cyclaw-keychain-env.sh, which fetches the secret from the macOS Keychain at process start, exports it, and execs the real command — failing closed (nothing launched) if the item is missing or empty. Store secrets first with macos/cyclaw-keychain-set.sh: an interactive no-echo prompt driven by security itself, so the secret never appears in any process's argv, and the item is trust-pinned with -T /usr/bin/security
  • Scheduled jobs — Dropbox sync (opt-in launchd backend, see Dropbox Corpus Sync), Telegram poll/health (python -m telegram.cli poll-plist / health-plist), and fsconnect trash emptying (python -m agentic.fsconnect.cli trash-empty-plist)
  • Supervised services (highest risk, extra-gated) — macos/generate_service_plist.py writes a KeepAlive LaunchAgent for gate.py or the harness console. Because that turns a loopback server into an always-on, auto-restarting listener that survives reboot, it refuses to write anything without --confirm and a non-empty --reason — the same reason-required idiom soul mutations use. Restart-on-crash only (KeepAlive: {SuccessfulExit: false}); a clean launchctl stop stays stopped
  • Uninstall symmetrymacos/uninstall-cyclaw.sh unschedules any registered sync job and boots out + removes landed CyClaw LaunchAgents by label, so no background job outlives the install

Core commands

bash macos/cyclaw-keychain-set.sh com.cgfixit.cyclaw.telegram-bot-token   # store a secret (TTY prompt)
python -m telegram.cli poll-plist                                        # Darwin-only; generates, never loads
python -m telegram.cli health-plist
python -m agentic.fsconnect.cli trash-empty-plist
python macos/generate_service_plist.py --service gate \
    --reason "keep the RAG server up across reboots" --confirm
python macos/generate_service_plist.py --service harness