CyClaw is a local RAG / chatbot / research server for your own documents, on
your own hardware: hybrid retrieval over a local Markdown corpus, a local
model answering from it, and safety rules written into the graph that routes
each request — not into a prompt asking a model to behave. It binds to
127.0.0.1:8787, answers locally by default, and treats any paid-provider call
as an exception you confirm per question and can account for afterwards, token
by token.
What that means in practice
- Your data stays put. Once the embedding model is cached, nothing leaves the machine unless you say so on that question.
- Policy is topology. Retrieval is the graph's unconditional entry node, every path converges on the audit logger, and the online-provider gate is a graph edge — checkable in code, not a prompt someone can forget.
- Paid calls are opt-in and ledgered. Three independent conditions must hold before Grok or Claude is called; every billed call appends token counts to a local ledger, with dollars derived at read time so a rate-card fix re-prices history instead of baking in errors.
- Everything else ships off. Per-user auth, memory, guardrails, local-data connectors, the agentic coding loop, and the Telegram/X channels sit behind master switches that ship disabled.
Scope. CyClaw is a trusted-operator, loopback-bound, single-tenant server —
one operator by default, or a small mutually-trusted group once auth.enabled
is on. Not multi-tenant. The full threat model, including what the sandbox
does not cover (no microVM by design), is
docs/THREAT_MODEL.md.
Getting started
- Quick Start
- What It Does
- Architecture
- Installation
- Full Setup Guide — every platform, Docker, every REST endpoint with
curl
Configuration and access
Operating it
- Spend Tracking
- Benchmarks and Evals
- Dropbox Corpus Sync
- macOS launchd & Keychain
- Local Model Fine-Tuning
Optional layers (master switches ship disabled; each section names its enablement gates)
- Optional layers at a glance
- Agentic Layer
- Agentic Coding Loop (GitHub)
- Filesystem, SQL & Passive Network Connectors
- NeMo Guardrails
- Telegram Channel
- OpenTweet Channel
Reference
macOS (Apple Silicon) — the onboarding script handles installation, keys, Ollama, indexing, and startup:
git clone https://github.com/CGFixIT/CyClaw && cd CyClaw
bash macos/setup-cyclaw.shLinux — manual path (Ollama must already be running on :11434):
git clone https://github.com/CGFixIT/CyClaw && cd CyClaw
python3.12 -m venv .venv && source .venv/bin/activate
pip install torch==2.13.0+cpu --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt -r requirements-test.txt -c constraints.txt --ignore-installed PyYAML
ollama pull qwen3.8:27b-mlx
export CYCLAW_API_KEY="$(openssl rand -hex 20)" # needed for the /soul/* and /ops/* endpoints
python -m retrieval.indexer # builds the retrieval index, once
python gate.py # → http://127.0.0.1:8787Windows uses the same dependency pins with PowerShell activation
(py -3.12 -m venv .venv, .\.venv\Scripts\Activate.ps1); see
Windows and
Linux in the setup guide.
Confirm it's alive: curl http://127.0.0.1:8787/health, then open
http://127.0.0.1:8787/ for the browser console.
Manual macOS install differs in one step. The
+cputorch wheel does not exist for macOS, so torch installs plain (torch==2.13.0) from stripped copies ofrequirements.txtandconstraints.txt. Exact commands: macOS (Apple Silicon).
CyClaw answers questions from your documents using a local model reading a local index. What makes the "nothing leaves the machine" claim checkable is where the safety lives: the shape of the graph, not a prompt, system message, or config flag someone could forget to set.
First run is the one exception. If the embedding model isn't already in the Hugging Face cache,
retrieval/embeddings.pyfetches it once — a documented bootstrap, not somethinguser_confirmed_onlinegates. Once cached, a disk-only probe confirms it and every later load passeslocal_files_only=True, so a warm cache never reaches out again.
- Retrieval comes first, unconditionally.
retrieveis the entry node of the 12-node LangGraph state machine ingraph.py; no model call can precede it. Edges enforce retrieval and provider selection, not groundedness —offline_best_effortcan answer from partial context after a vault miss. - Hybrid search over your Markdown corpus. ChromaDB semantic vectors plus
BM25 keyword ranking, fused by RRF (
retrieval.rrf_k), both local and CPU-only. A top hit weaker thanretrieval.min_score(andretrieval.min_semantic_score, when present) routes to a user gate instead of a confident guess. - A local model by default. Ollama serving
models.local_llm.model(shipped:qwen3.8:27b-mlx). Context budget, generation cap, and every timeout areconfig.yamlvalues — nothing tunable is hardcoded. - A governed personality layer.
data/personality/soul.md, SHA-256 drift detection, atomic writes.POST /soul/applyrequires a humanreasonand an enforced injection scan;POST /soul/restorere-adopts vetted.bakcontent under only an advisory scan; a missing file self-heals at boot. Governed — neither frozen nor self-editable. - Online fallback, triple-gated per question. A paid Grok (xAI) or Claude
(Anthropic) call fires only when all three hold:
app.mode: hybridand the provider's ownenabledflag and a per-requestuser_confirmed_online: true— never persisted, never carried forward. The provider is chosen per query viaonline_provider. Both shipenabled: true, so the per-question confirmation is the gate actually holding the line — not a server-enforced challenge (a client can sendtrueon its first call), but something still has to assert it every time. Outbound calls are also capped by the remainingapi.graph_timeout_secbudget. "Triple-gated" below always means this gate. - Two front doors. A FastAPI gateway at
127.0.0.1:8787(browser console at/) and a retrieval-only MCP server (mcp_hybrid_server.py,sampling: None) — search only, no model path. - An audit trail that hashes the question. All eleven upstream paths
converge on
audit_loggerbefore END, writing a SHA-256 query hash plus PII-redacted metadata tologs/audit.jsonl;cyclaw-metricsreads it offline.logging.audit_fields.include_query_hash: falsestores raw query text instead (privacy-affecting, perutils/logger.py's own docstring).
Enforced in four places: graph topology (entry, routing, audit
convergence), gate.py construction (two of the three external-provider
gates — only per-request confirmation is decided in the graph),
utils/personality.py (the soul reason gate and atomic write), and
import structure (module isolation, invariant I6).
python3 .claude/skills/invariant-guard/check_invariants.py asserts all six
invariants plus five guards statically; INVARIANTS.md
records which are code-enforced vs. conventional, and the test pinning each.
Three kinds: route modules on the gateway (per-user auth, memory),
in-path utilities inside the core request path (the Numbat stream and
spend ledger — both in utils/ rather than out-of-band, and both ship on
since neither needs a config edit, adds egress, or does more than write a
local file), and I6-isolated subsystems — everything else, never imported
by gate.py/graph.py/the MCP server, a boundary asserted statically.
| Layer | What it adds | Ships |
|---|---|---|
Per-user authentication (gate_auth.py, utils/authn*) |
scrypt password hashes, session cookie + CSRF, bearer device tokens, three roles (admin/operator/audit), cyclaw-user CLI. With auth.enabled: true, /query requires a session or token |
off |
Facts + episodes memory (gate_memory.py, memory/) |
SQLite + FTS5 store with propose/apply governance (human reason + injection scan) and an optional retrieval-fusion hook |
off |
NeMo Guardrails (guardrails/) |
content-safety input rail + output grounding check, degrading to offline heuristics without nemoguardrails — defense in depth, never a routing authority |
off |
Dropbox corpus sync (sync/) |
an rclone wrapper refreshing data/corpus/ out-of-band, signaling "reindex" by exit code |
CLI only |
Local-data connectors (agentic/fsconnect, sqlconnect, netconnect) |
scoped filesystem reads with gated writes, SELECT-only SQL, and passive LAN inventory | off |
Agentic layer + coding loop (agentic/) |
read-only GitHub context via gh, a governed skills registry, and a real-repo clone → plan → patch → verify → human decides → commit pipeline (push/draft-PR are further decisions) |
off |
| Telegram and OpenTweet channels | a phone remote and a weekly X poster; both reach the pipeline only through loopback POST /query |
off |
Numbat forensic stream (utils/numbat_emitter.py) |
a derived NDJSON projection of the audit trail (logs/numbat-events.ndjsonl) that the pinned Numbat 0.2.0 CLI can score (design note) |
on |
Spend ledger (utils/spend.py) |
token counts per billed call in logs/spend.jsonl, tagged source: query/agentic; dollars derived at read time |
on |
Fine-tune kit (tools/lora_finetune/) |
offline QLoRA kit teaching a local model this codebase; installed by no runtime surface | toolkit |
gate.py (FastAPI on 127.0.0.1:8787) runs the TrustedHostMiddleware Host
allowlist, then the per-IP rate limiter (60 req/min — before the injection
filter), then the config-driven injection filter, then soul init, and hands
a GraphState to the 12-node LangGraph state machine in graph.py. Routing
is graph edges only — retrieve is the unconditional entry, and every path
converges on audit_logger before END. The numbered plain-text version of
this flow is CLAUDE.md §2 "The Map"; the invariants it encodes
are in INVARIANTS.md.
flowchart TD
A(["🌐 Client\nHTTP POST /query"])
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/\noptional rails via guardrail_bridge"]
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
The retrieval-only MCP server calls the retriever directly after sanitization; it never enters this HTTP gateway or generation graph.
What the diagram compresses: HybridRetriever fuses ChromaDB (semantic,
all-MiniLM-L6-v2, 384-dim cosine, CPU-only embeddings) with BM25Okapi
(keyword, Porter stemming) by RRF (k=60, equal weighting), carrying
per-chunk provenance metadata in every result. The telemetry kill block runs
before any SDK import; the MCP server and indexer apply the same block.
Install, first run, and starting the gateway are in
setup-guide.md. macOS installs plain torch==2.13.0;
Windows and Linux use the +cpu wheel. Platform scripts:
macos/README.md and
powershell/README.md. The optional GHCR image is
docs/DOCKER.md.
/soul/*, /ops/*, /memory/*, and /audit/summary require a Bearer
CYCLAW_API_KEY and fail closed (401) when unset. /query and /health
don't use it. What the key gates and the per-platform generate step are in
setup-guide.md.
macOS persist is the Keychain bootstrap:
macos/README.md
(401 recovery). Provider key names
and the ledger are in spend/README.md; Windows
persist is next.
Generate the session value in
setup-guide.md, then persist it —
current user, open a new session before python gate.py:
[System.Environment]::SetEnvironmentVariable("CYCLAW_API_KEY", $env:CYCLAW_API_KEY, "User")cmd.exe: set CYCLAW_API_KEY=<value> for the session, setx CYCLAW_API_KEY "<value>"
to persist. A repo .env is sourced by Invoke-CyClaw.ps1 only when every
Allow ACE is the current user (icacls .env /inheritance:r /grant:r "${env:USERNAME}:(R,W)"). Scheduled-task secrets use Credential Manager
(powershell/README.md).
The operator API key above gates soul and ops routes. Per-user auth is the
account system (gate_auth.py): scrypt passwords, a session cookie plus CSRF
for browsers, named device tokens for scripts, and roles admin, operator,
and audit. It ships with auth.enabled: false — while off, every /auth/*
route returns 503. The design, role table, TLS, and non-loopback bind rule are
in docs/AUTHENTICATION_DESIGN.md.
First-boot curl and cyclaw-user are in
setup-guide.md.
Every triple-gated Grok/Claude call that actually bills appends one line to
logs/spend.jsonl via utils/spend.py. Tokens are the ground truth;
dollars are derived at read time — the ledger never stores a price, so a
rate fix re-prices the entire history instead of leaving wrong numbers baked
in. It never stores query text, prompt content, or API keys, and writes are
best-effort: a full disk logs a warning and drops the row rather than turning
a successful paid answer into a failed request.
source |
Writer | What it covers |
|---|---|---|
query |
llm/client.py |
The /query online fallback — a triple-gated escalation a human confirmed per request |
agentic |
agentic/deepagent_github/chat_client.py |
The out-of-band cloud planner's one-shot plan calls |
eval |
tests/judge_eval.py / judge_calibrate.py |
Opt-in Anthropic-judge evals; routed to logs/evals/spend.jsonl, never the production ledger |
Reading it: python -m metrics (or cyclaw-metrics). Prints today/
last_7d windows: tokens, a derived USD figure, per-provider/source row
counts, and two data-quality counters (usage_missing, rate_unknown). A
vendor-reported cost also shows table_usd/vendor_usd/delta_usd side by
side, so rate-table drift is visible rather than hidden.
Pricing is exact, not approximated: Grok's long-context band (above 200k
tokens, xAI bills the entire request at the long rate) and Claude's
cache-write split by TTL. PRICED_AS_OF flags stale after 30 days.
compare_vendor_cost() verifies against xAI's own ticks (Claude's check is
the Anthropic console total). utils/sequence_detect.py joins the ledger to
logs/audit.jsonl on query_hash to correlate a blocked injection with a
later online escalation.
Live probes (spend real money, opt-in, never collected by pytest):
CYCLAW_SPEND_LIVE=1 python tests/spend_live_probe.py writes to a temp
ledger, deletes it, and asserts no forbidden field reached the row.
Full schema and the live-probe walkthrough: spend/README.md.
Quality is measured on four separate planes, and only the first blocks a
merge. All score the synthetic fixture under tests/fixtures/groundedness/
(eight documents, 52 labeled cases in six categories including
injected_content, where the evidence itself carries instructions); none is a
graph node or a security control.
| Plane | Command | Runs | Measures |
|---|---|---|---|
| Retrieval gate | python -m tests.ci_rag_smoke |
every PR (ci.yml), no LLM |
four data/corpus queries against retrieval.min_score, then hit@5/Recall@5/MRR, plus a check each injected doc's chunk was sanitized to [FILTERED] |
| Local dogfood | CYCLAW_EVAL_DOGFOOD=1 python scripts/cyclaw-eval-dogfood.py |
operator, opt-in | one case per category on the real loopback model, with latency and a sanitizer probe; rows are generated/unverified, never assumed |
| Anthropic judge | CYCLAW_EVAL_LIVE=1 python tests/judge_eval.py (+ key) |
operator, opt-in, spends money | groundedness, completeness, abstention per case, graded by Claude |
| Local judge | same, with evals.local_judge.enabled: true |
operator, opt-in, fully local | same rubric graded by a second loopback model of a different family |
Measured so far. The retrieval gate holds at hit@5 1.0 / Recall@5 1.0 /
MRR 1.0 on CI (20 scored cases, 2026-09-12) and locally after the fixture grew
to 52 (44 scored, 2026-09-16). The dogfood matrix produced five real
generated rows on qwen3.8:27b-mlx on an M5 Pro 48 GB
(dated record).
No judge-plane result has been published yet. Planes, thresholds, and the
not-yet-measured list are in docs/EVALS.md.
An optional, out-of-band rclone-backed pull sync mirrors a Dropbox
corpus into data/corpus/ without touching gate.py, graph.py, or the MCP
path. It carries safety fuses (max_delete, max_transfer), an OS-backed
single-instance lock so a scheduled and a manual run can't race, audit logging
of changed corpus files, an optional reindex trigger, and scheduler glue for
cron / Windows Task Scheduler plus an opt-in Darwin-only launchd backend that
generates the plist and prints the bootstrap command — never loading it.
python -m sync.cli setup # first-run bootstrap; then: test | sync --dry-run | sync | status | schedule | unscheduleThe Sync Console panel drives the same actions via POST /ops/sync
(loopback-only, API-key gated, audited). Full setup and scheduling:
docs/! How-To-Guides/Dropbox_Sync_Guide.md;
module internals: docs/SYNC_README.md.
CyClaw's scheduled/supervised jobs on macOS run through generated launchd
LaunchAgents: 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.
- Secrets never land in a plist. Token-bearing jobs chain
macos/cyclaw-keychain-env.sh, which fetches the secret from the Keychain at process start andexecs the real command — failing closed if the item is missing. Store secrets withmacos/cyclaw-keychain-set.sh, a no-echo prompt so the secret never appears in argv; trust-pinned with-T /usr/bin/security. - Scheduled jobs — Dropbox sync (above), Telegram poll/health, fsconnect
trash emptying, and OpenTweet — each a generate-only
*-plistsubcommand. - Supervised services (highest risk) —
macos/generate_service_plist.pywrites a KeepAlive LaunchAgent forgate.py, refusing to write without--confirmand a non-empty--reasonsince that turns a loopback server into an always-on listener that survives reboot. Windows counterpart:windows/generate_service_task.py. - Uninstall symmetry —
macos/uninstall-cyclaw.shunschedules any registered sync job and removes landed LaunchAgents by label.
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 macos/generate_service_plist.py --service gate \
--reason "keep the RAG server up across reboots" --confirmScript-by-script reference (including 401 / key-drift recovery):
macos/README.md. Design and phase ledger:
docs/work/MACOS_LAUNCHD_INTEGRATION_PLAN.md.
Retrieval tells the local model what this codebase says; a fine-tune teaches
it how this codebase thinks, so an operator model stops re-deriving the same
invariants every question. tools/lora_finetune/ is a QLoRA kit for
models.local_llm.model built on a curated Q&A dataset generated from live
source, each example carrying source_refs back to its file.
It is an operator toolkit outside the runtime install profiles. Its CUDA training install is currently blocked: Unsloth's dependency ranges conflict with the kit's patched Hugging Face pins — do not bypass those; audit the training environment separately (excluded from this repo's OSV walk).
It is not air-gapped, though. finetune_qwen38.py downloads the base
checkpoint from Hugging Face on first run with no local_files_only. Seed the
model/tokenizer caches first on a no-egress machine — "offline" here means
independent of the CyClaw server, not free of network.
python tools/lora_finetune/build_cyclaw_corpus.py # rebuild the dataset from source
python tools/lora_finetune/dryrun_finetune.py # full control flow, mocked, no GPUDataset shape, category counts, the confirmed training-install blocker, and
the pip-audit-on-the-GPU-box step are in
tools/lora_finetune/README.md.
A concise, governed agentic layer for local operator workflows. It is
opt-in, disabled by default, and out-of-band (I6) — never imported by
gate.py, graph.py, or mcp_hybrid_server.py. data/agentic/skills_registry.json
is a governed store that ships empty (apply-skill writes it). Package guide:
agentic/README.md.
What it adds: read-only GitHub context via the gh CLI (argv list, never a
shell; no token stored or forwarded), a governed local skills registry with
explicit human gating, and the operator workflows under .claude/
(.claude/README.md). All reads, refusals, and registry
changes are audit logged.
The GitHub write path (gh pr create --draft) is implemented and its
code-level gates (EXECUTION_ENABLED, mode: "write", writes_enabled) ship
open since 2026-08-07. The layer master switch agentic.enabled still ships
false, so a default checkout can't open a PR — that, plus a per-call
reason and confirm, is what refuses. Rollback:
docs/agentic/GITHUB_WRITE_ENABLEMENT.md.
agentic:
enabled: true # the one edit this block asks you to make
repo: "cgfixit/CyClaw"
mode: "write" # ships open since 2026-08-07
writes_enabled: true # ships open since 2026-08-07
gh_min_version: "2.40.0"
registry_path: "data/agentic/skills_registry.json"python -m agentic.cli status
python -m agentic.cli context --repo # also: --pr 123 / --issue 45
python -m agentic.cli test
python -m agentic.cli propose-skill --name deploy --desc "..." --body-file s.md --reason "draft"
python -m agentic.cli apply-skill --name deploy --desc "..." --body-file s.md --reason "add deploy runbook" --confirmThe Agentic Console panel drives these from the terminal UI via
POST /ops/agentic; skill-Apply is refused under shipped defaults by
agentic.enabled: false, and a per-call reason + --confirm remain
mandatory once it is on.
The real-repo pipeline clones a repo, plans, patches, verifies, and stops for
a human decision before it commits. Pushing the branch and opening a draft PR
are two further decisions. A default checkout holds the run: agentic.enabled,
deepagent_github.enabled, and allow_git_write_tools ship false.
Enablement and commands are in
agentic/README.md and
docs/agentic/AGENTIC_README.md.
Draft-PR arming and the rollback are in
docs/agentic/GITHUB_WRITE_ENABLEMENT.md.
Three connectors extend the agentic layer to local data. All three ship off
and stay outside the request path. fsconnect does scoped filesystem reads,
with writes on a separate gate. sqlconnect is SELECT-only. netconnect is a
passive inventory of local host data and the existing neighbor cache.
Enablement, security notes, and tool lists are in
agentic/README.md:
filesystem,
SQL,
and passive network.
An opt-in content-safety layer in guardrails/
(package README). Absence of the guardrails: block,
or enabled: false (shipped default), is a pure no-op. When enabled,
utils/guardrail_bridge.py wires two graph.py nodes — guardrail_input and
guardrail_output (grounding check, local_llm path only) — still
defense-in-depth only, never a routing authority: the graph's own edges
decide where a blocked query goes. gate.py/graph.py/mcp_hybrid_server.py
never import guardrails directly (I6).
nemoguardrails is an optional dependency: absent, the layer degrades to
offline heuristic rails needing no second LLM call — an input rail
(injection marker scan + soul-mutation intent detection) and an output
rail (token-overlap grounding check, flagging likely-hallucinated answers
below hallucination_threshold). When installed, the same checks back the
live NeMo actions via guardrails/config/rails.co, so heuristics and live
rails never drift. Decisions go to a separate metrics stream
(logs/guardrails.jsonl) storing only SHA-256 hashes.
python -m guardrails.cli status
python -m guardrails.cli check "your query here" # also: metrics | testConfig keys (guardrails.enabled, engine, model, hallucination_threshold,
metrics_path) and their shipped values live in config.yaml; the status
table, phased history, and rail semantics are in
docs/NeMo/README.md.
An optional, out-of-band (I6) channel (telegram/, shipped
enabled: false) giving the single trusted operator a phone-reachable remote
— outbound notifications and, when configured, allowlisted two-way chat.
Inbound chat text only ever reaches the RAG pipeline via loopback
POST /query, never a direct call into graph.py.
Outbound notify (mode: "notify") or long-poll two-way chat (mode: "chat",
still enabled: false; no public webhook listener). allowed_chat_ids is
required non-empty when enabled; the bot token comes only from the env var
named by bot_token_env, never YAML. T3 hybrid-confirm consent
(allow_hybrid_confirm: false by default): the exact command
/online on <grok|claude> is the only way chat text can set
user_confirmed_online, for one message only (hard-capped at 300s) — core's
triple gate remains final authority. T4 media staging (media.enabled: false)
accepts attachments captioned /save --confirm <reason> only through the
existing agentic/fsconnect write path.
python -m telegram.cli status
python -m telegram.cli test
python -m telegram.cli send --chat-id "<id>" --text "..." # T1; add --dry-run to preview
python -m telegram.cli poll # T2; requires telegram.mode: chat
python -m telegram.cli poll-plist / health-plist # Darwin-only; generates, never loadsSee docs/channels/TELEGRAM_DESIGN.md for
architecture, the T0–T4 phase ledger, and threat-model obligations
(docs/THREAT_MODEL.md's seventh amendment), and
telegram/README.md for package internals.
Optional out-of-band (I6) X poster (opentweet/, shipped enabled: false).
Generation is loopback POST /query with user_confirmed_online: false, so
it can never trigger a paid call. The default write is an OpenTweet draft;
scheduled_date is opt-in via opentweet.schedule_enabled. Schedulers never
send publish_now.
python -m opentweet.cli status
python -m opentweet.cli post --topic "..." # add --dry-run to preview
python -m opentweet.cli schedule-plist # Darwin/Windows: schedule-task; generates, never loadsSee docs/channels/OPENTWEET_DESIGN.md
and opentweet/README.md. Keychain/CredMan wrappers
are in macos/README.md and
powershell/README.md.
| Layer | Mechanism |
|---|---|
| Network | Binds 127.0.0.1:8787 — no external exposure by design; _require_loopback_bind refuses a non-loopback api.host outside the documented auth + TLS exception (bind guard) |
| Endpoint trust | utils/endpoint_trust.py allowlists where a generation client may talk, checked in graph.py. Local nodes accept loopback or models.local_llm.trusted_hosts (ships []); online nodes pin Grok to api.x.ai and Claude to api.anthropic.com, so a tampered base_url can't redirect a confirmed call |
| Input | Config-driven injection filter (policy.prompt_filter: 40 banned_patterns, max_input_chars: 4000) |
| Rate limit | 60 req/min per IP, sliding window; in-memory by default, optional SQLite/Postgres persistence |
| Proxy bypass | All httpx clients set trust_env=False — ambient HTTP(S)_PROXY/.netrc can't reroute local traffic or carry API keys |
| Telemetry | Canonical kill maps applied before any SDK import at every chokepoint (invariant-guard G1) and delivered as literal environment at every process boundary. ONNX gets a post-import suppression call; HF Hub calls stop once the embedding model is confirmed cached. Not a network kill switch — see SECURITY.md |
| Audit | All paths log SHA-256 query hash + PII-redacted metadata (What It Does, item 7) |
| Grok / Claude gating | The triple gate (item 5), per provider: mode=hybrid AND <provider>.enabled=true AND user_confirmed_online=true |
| Soul writes | Explicit human reason string + enforced write-boundary scan + atomic write |
| Agentic writes | pr_create implemented; agentic.enabled (ships false) plus per-call reason/confirm is what refuses — see Agentic Layer. Git-level writes additionally need deepagent_github.allow_git_write_tools, ships false |
| Local-data connectors | fsconnect reads scoped/capped with atomic gated writes; sqlconnect is SELECT/WITH-only; netconnect is passive-only — all disabled by default, see Connectors |
| Guardrails | Out-of-band, opt-in defense-in-depth; degrades to offline heuristics without nemoguardrails; never a routing authority — see NeMo Guardrails |
| Telegram / OpenTweet channels | Both out-of-band, ship enabled: false, and reach the pipeline only via loopback POST /query — see their sections above for the T3/T4 gates and draft-only defaults |
| launchd secrets (macOS) | Generated plists never embed tokens — Keychain wrapper injects at exec time, fails closed when missing; supervised-service generators require --confirm + --reason |
/ops/* routes |
Loopback-only, require_api_key gated, rate-limited, every call audited; shells out via subprocess.run([...]) — never imports sync/ or agentic/ |
/auth/* routes |
Per-user auth (gate_auth.py); first-boot GET /auth/setup-status and loopback-only POST /auth/bootstrap-password; session cookie + CSRF for browsers, bearer tokens for scripts; three roles gate /auth/users*, last admin protected; every handler checks auth.enabled first, returns 503. When true, POST /query requires a session or token |
/memory/* + /query/export/html routes |
Optional, default-off; every memory: switch ships false; require_api_key gated, rate-limited; mutating routes require a non-empty reason, injection scan on apply |
| Container | Non-root, no-new-privileges, cap_drop: ALL, read-only rootfs, seccomp, resource limits; optional eBPF/Falco (deploy/falco/, off by default) |
| Dependency risk | Pins tracked in constraints.txt, walked by pip-audit. One accepted risk: chromadb==1.5.9 carries CVE-2026-45829 (critical pre-auth RCE, no patch), accepted only for the embedded PersistentClient mode CyClaw uses. Rationale: SECURITY.md |
Docker / GHCR: published runtime image
ghcr.io/cgfixit/cyclaw(tag-triggered), host publish127.0.0.1only. Guide, Falco opt-in, and explicit non-goals (no microVM):docs/DOCKER.md.
Scope is unchanged from the top of this README — trusted-operator, loopback-bound, single-tenant. LAN/WAN exposure needs an explicit bind exception (auth + TLS), never the default. Full threat model:
docs/THREAT_MODEL.md; design philosophy:docs/security-philosophy/.
CyClaw/
├── gate.py # FastAPI gateway — bind guard, middleware, GraphState hand-off
├── gate_ops.py # /ops/* subprocess shims (sync/agentic/fsconnect/sqlconnect)
├── gate_auth.py # /auth/* — session cookie + CSRF, bearer device tokens
├── gate_memory.py # /memory/* + /query/export/html — default-off memory admin
├── graph.py # the 12-node LangGraph state machine
├── metrics.py # audit.jsonl + spend.jsonl analyzer (cyclaw-metrics)
├── spend/ # token-ledger reference (see Spend Tracking)
├── config.yaml # single source of truth
├── mcp_hybrid_server.py # retrieval-only MCP server
├── memory/ # optional facts + episodes store (default-off)
├── agentic/ # out-of-band GitHub context + governed registry: writer.py
│ (gh pr create --draft), real_repo_loop.py (clone→plan→patch→verify→human
│ decides→commit), executor/ (sandboxed check runner), fsconnect/sqlconnect/
│ netconnect (local FS, read-only SQL, passive LAN), deepagent_github/
├── guardrails/ # opt-in rails; graph nodes via guardrail_bridge
├── telegram/ / opentweet/ # optional channels, out-of-band, shipped enabled: false
├── powershell/ / windows/ / macos/ # per-platform installers, launchd/task glue
├── .claude/ # local operator workflows and prompts (22 project skills)
├── retrieval/ # indexer, hybrid_search (RRF), embeddings, stemmer,
│ # vector_store (pluggable Chroma/pgvector), clear_cache
├── llm/client.py
├── sync/ # optional Dropbox corpus sync
├── utils/ # sanitizer, logger, personality, health, ratelimit,
│ guardrail_bridge (sole bridge to guardrails/), endpoint_trust (destination
│ allowlist), ops_runner, numbat_emitter, spend.py, sequence_detect, authn*
│ (per-user auth stack), gen_cert, telemetry_kill (invariant-guard G1),
│ onnx_telemetry
├── schemas/ # Pydantic API models (extra='forbid', strict)
├── scripts/ # install-githooks.sh, measure_local_llm_throughput.py
├── tools/lora_finetune/ # offline QLoRA kit; installed by no runtime surface
├── deploy/ # apparmor/ falco/ seccomp/ container hardening
├── tests/ / docs/ / static/
├── data/
│ ├── corpus/ / personality/
│ └── agentic/ # skills_registry.json — governed store, ships empty
└── .github/workflows/
Every top-level package carries its own README.md (map + traps + links to
its authoritative doc) — the exception is tools/, whose README lives at
tools/lora_finetune/README.md. The tree omits most files for brevity.
| Read this | When you want |
|---|---|
setup-guide.md |
every install path step by step, Docker, and every REST endpoint with curl |
INVARIANTS.md |
the rules behind the graph, which are enforced by code vs. convention, and the test pinning each |
CLAUDE.md |
the numbered request-path map and the operator conventions AI tooling works under |
SECURITY.md |
egress classification, dependency risk acceptances, disclosure |
docs/THREAT_MODEL.md |
scope, bind exceptions, and the numbered amendments the sections above cite |
docs/AUTHENTICATION_DESIGN.md |
the per-user auth design and rationale |
docs/DOCKER.md |
the GHCR image, compose hardening, Falco opt-in |
docs/EVALS.md |
the four eval planes, thresholds, and what is not yet measured |
spend/README.md |
the ledger schema, Keychain service names, live probes |
docs/security-philosophy/ |
why telemetry is killed and why offline is the default |
macos/README.md / powershell/README.md |
platform scripts, Keychain / Credential Manager, 401 recovery |
Source-available, all rights reserved; personal use is permitted — see
LICENSE for the exact terms.
Designed and built by Chris Grady, with AI tooling used under human review, CI, and the invariant guard.
