CyClaw: Local AI you can Trust.
Table of Contents
- What It Does
- Architecture
- API Key Setup (Soul Mutations)
- Quick Start
- Docker / GHCR
- Full Setup Guide
- Project Structure
- Dropbox Corpus Sync
- macOS launchd & Keychain
- Agentic Layer
- Filesystem & SQL Connectors
- NeMo Guardrails
- Agentic Harness Scaffold
- Coding Harness Console
- GitHub Agentic Coding Harness
- Telegram Channel
- Security Model
- Remaining Work
- Archive & Roadmap
What It Does
CyClaw is a personal RAG (Retrieval-Augmented Generation) backend that:
- Answers questions exclusively from your local Markdown corpus — no internet by default
- Enforces every safety invariant via LangGraph topology — not prompts, not config flags, not discipline
- Maintains a persistent soul/personality layer (
soul.md) with SHA-256 drift detection, atomic evolution writes, and user-gated modification - 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
- Exposes both a FastAPI HTTP gateway and an MCP server for Claude Desktop / Copilot Studio integration
- 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 - 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 - Adds an optional NeMo Guardrails content-safety layer (v1.8,
guardrails/) that soft-importsnemoguardrailsand degrades to offline heuristic rails — defense-in-depth only, never a routing authority. Whenguardrails.enabledis the literaltrue,utils/guardrail_bridge.pywires visibleguardrail_input/guardrail_outputnodes; seeguardrails/README.md - 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, leavingagentic.enabledas the master switch that still shipsfalse - Ships a local coding-harness console (v1.9,
harness/+powershell//macos/) — a grok-build-style slash-command console on127.0.0.1:8790chatting with the local model over the OpenAI-compatible endpoint, with per-session token tallies,/goal+ human-gated/loop, wired/skillsand/toolsdiagrams, 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. Seeharness/README.md - 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 aclaude/*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: falseis 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, plusallow_git_write_tools: falsefor push - 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 thecyclaw-userconsole script for account/token management. Beyond login/logout/whoami,gate_auth.pyalso carries a role-based admin surface (§12 "Roles" — three roles,admin/operator/audit, with the last enabledadminprotected from disable/delete/role-change):/auth/userslist/create,/auth/passwordself-service,/auth/users/{username}/password|role|disable|enable,DELETE /auth/users/{username}, and/auth/audit/summary. Every/auth/*route exists regardless ofauth.enabledand returns 503 (not 404) when it's off, so route presence never discloses whether the feature is enabled. Whenauth.enabledis true,POST /queryand the console require a session or named device token. The shipped default leaves/queryopen. - Adds an optional facts + episodes memory store (
gate_memory.py+memory/, packagememory/README.md, plan indocs/memory/README.md) — SQLite+FTS5-backed, with propose/apply governance (a non-empty humanreasonplus an injection scan on apply, parallel to soul's I5) and an optional retrieval-fusion hook. Everymemory:switch shipsfalse; mutating routes require the same BearerCYCLAW_API_KEYas the other admin endpoints. Notdocs/memories/(sandbox notes) - Ships an optional Telegram channel (v1.9,
telegram/, shippedenabled: false) — an out-of-band phone remote. Shipped YAML ismode: "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 loopbackPOST /query— never a direct call intograph.py. T3 hybrid-confirm (allow_hybrid_confirm, default off) is the only way chat text can setuser_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 existingagentic/fsconnectpath. Seedocs/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 — includingGET /soul— require a validAuthorization: Bearer <key>token. Only/health,/query,POST /auth/login(issues the session itself; 503 whenauth.enabledis false), and the console pages (GET /,/static/*) are unauthenticated.
Opting out entirely:
config.yaml'ssecurity.api_key_optional(defaultfalse) removes theCYCLAW_API_KEYrequirement 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 insecurity.allowed_hostsdo not change that: that list filters requestHostheaders and opens no listening socket. What would matter is the bind itself —gate.pyrefuses to start with a non-loopbackapi.hostwhile the flag istrue, andconfig-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 (setCYCLAW_API_KEYin 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
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_KEY —
llm/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 (
.envis 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_frequencydaily/weekly/monthly) that generates the plist and prints thelaunchctl bootstrapcommand, 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, andexecs the real command — failing closed (nothing launched) if the item is missing or empty. Store secrets first withmacos/cyclaw-keychain-set.sh: an interactive no-echo prompt driven bysecurityitself, 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.pywrites a KeepAlive LaunchAgent forgate.pyor 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--confirmand a non-empty--reason— the same reason-required idiom soul mutations use. Restart-on-crash only (KeepAlive: {SuccessfulExit: false}); a cleanlaunchctl stopstays stopped - Uninstall symmetry —
macos/uninstall-cyclaw.shunschedules 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

No comments yet
Be the first to share your take.