cgfixit/CyClaw

CyClaw is a Local, offline-first local AI agent - https://cglocalai.grok.me/

Python

6

3,667 commits

updated Oct 15, 2026

See the code

README

CyClaw

Python 3.12 FastAPI LangGraph CyClaw CI/CD testing

CyClaw console answering from the local library

CyClaw answers questions from your documents on your hardware. Its 12-node LangGraph starts with retrieval, ends every path in the audit log, and requires per-question consent for paid Grok or Claude fallback. The server binds to 127.0.0.1:8787.

  • Embeddings, BM25, and the cross-encoder run locally on CPU; cache both retrieval models before going offline.
  • Soul, ops, memory, and audit routes require operator access, which fails closed without CYCLAW_API_KEY (an enabled admin's login also works when per-user auth is on).
  • Audit records hash questions by default; the spend ledger records billed tokens; cyclaw-metrics reads both offline.
  • Guardrails (NeMo in every base install), the Numbat stream, and the spend ledger ship on. Auth, memory, connectors, the agentic loop, and Telegram/X channels ship off.

CyClaw serves one trusted operator, or a mutually trusted group with auth enabled; it provides neither tenant isolation nor a microVM (threat model).

Step-by-step installs and every REST call: Full Setup Guide. Contributor rules for people and agents: CLAUDE.md, AGENTS.md, and the tracked git-hook gate in docs/GITHOOKS.md.

Security review #1526's residual decision register separates implemented controls, accepted limits (native Windows verification stays disabled; supported confinement still permits host reads), and optional research.

Quick Start

macOS (Apple Silicon) — install, keys, Ollama, index, and startup:

git clone https://github.com/cgfixit/CyClaw && cd CyClaw
bash macos/setup-cyclaw.sh

Linux — Ollama already running on 127.0.0.1:11434:

git clone https://github.com/cgfixit/CyClaw && cd CyClaw
python3.12 -m venv .venv && source .venv/bin/activate
pip install --require-hashes --no-deps -r locks/requirements-torch-lock-linux.txt --index-url https://download.pytorch.org/whl/cpu
pip install --require-hashes -r locks/requirements-lock-linux.txt
pip install --require-hashes -r locks/requirements-test-lock-linux.txt
ollama pull qwen3.8:27b-mlx
export CYCLAW_API_KEY="$(openssl rand -hex 20)"  # operator routes; /query uses sessions/tokens when auth is on
python -m retrieval.indexer                      # once; without this, /query is 503
python gate.py                                   # http://127.0.0.1:8787

CI checks every lock group against recorded inputs and hashes, and checks wheel dependency metadata. Refresh on arm64 macOS with the maintained lock scripts; generation failures leave tracked locks unchanged.

Windows uses the same pins with the -windows lock files (py -3.12 -m venv .venv, .\.venv\Scripts\Activate.ps1): Windows, Linux.

Confirm: curl http://127.0.0.1:8787/health, then open http://127.0.0.1:8787/. A manual macOS install uses plain torch==2.13.0 (no +cpu wheel) then the hashed macOS lock (macOS (Apple Silicon)); updating an existing environment ends with the NeMo verification.


For newcomers

Python and install paths. Use Python 3.12 (>=3.12,<3.13), not an unversioned python3 that may resolve to 3.11; .venv needs no admin rights. Choose native macOS, Windows, the Linux Quick Start, Docker (linux/amd64, published on 127.0.0.1 only), or conda (environment.yml). The constrained editable install in the setup guide adds the cyclaw-* commands; python -m … always works.

Secrets. gate.py reads environment variables, never dotenv files (Invoke-CyClaw.ps1 reads only non-secret settings from an owner-only %USERPROFILE%\.CyClaw\.env or checkout dotenv).

Secret persistence.

PlatformSetupOperationDefault secret storePlaintext opt-in
macOSmacos/setup-cyclaw-keys.shSet up or preserve gateway keyKeychain--write-env-file
Windowspowershell/Install-CyClaw.ps1Migrate existing plaintext secretsCredential Manager-WriteEnvFile

On Linux, macos/invoke-cyclaw.sh stores the gateway key in libsecret or an owner-only file (macos/cyclaw-linux-key.sh). Services read secrets at execution through macos/cyclaw-keychain-env.sh or powershell/CyClaw-CredMan-Env.ps1, never from dotenv, plist, task XML, config.yaml, shell rc files, or argv; a configured keystore lookup fails closed when its item is missing. A missing provider key leaves that provider unavailable, not the server down (provider keys).

Offline and hybrid. Shipped app.mode: hybrid permits a paid call only when the selected provider is enabled, available, and confirmed for that one request; confirmation is never persisted. Set app.mode: offline or decline to stay local. Model downloads are separate from that consent: models.embeddings.offline_after_index: true forces local files after indexing, and the ~91 MB reranker loads on the first query, so cache it first.

Ports. Gateway 127.0.0.1:8787 (api.port, launcher override CYCLAW_GATE_PORT), Ollama 127.0.0.1:11434. The old :8790 coding console is now the separate CG-agent-harness.

First-run checks and limits.

  • Without an index, /query returns 503 INDEX_NOT_FOUND. Run python -m retrieval.indexer, or click Build my library in the console (POST /index/build, loopback + same-origin + no forwarding headers, then GET /index/status); once CYCLAW_API_KEY is set a locked build opens the unlock dialog and retries after the unlock.
  • The console header shows the mode in plain words ("Cloud fallback · ask first" or "Offline only") and Library/Engine health chips. /health reporting degraded usually means Ollama is down; TELEMETRY KILL at startup is expected.
  • The 60/min per-IP rate limit resets on restart unless api.rate_limit.persist_path or a Postgres DSN is set. /health and /index/status are public and not rate-limited. Request bodies above security.max_request_body_bytes (1 MiB) get 413.
  • Browser CORS allows http://127.0.0.1:8787 and http://localhost:8787; /query always rejects cross-site requests.
  • Restart the gateway after editing sanitizer config (it is cached). CYCLAW_EMBED_CACHE_SIZE (default 2048) is an env var, not a YAML key.
  • Offline checks: python .claude/skills/invariant-guard/check_invariants.py and GROK_API_KEY=dummy python -m pytest tests/ -q --tb=short (no live provider).

Local records. Paths are relative to the repository.

PathGit statusContents and behavior
logs/audit.jsonlignoredAuthoritative audit, written synchronously on the request thread. Questions use persistent HMAC-SHA256 fingerprints by default, including when logging.audit_fields is absent or empty. Explicit include_query_hash: false stores redacted query text
logs/spend.jsonlignoredTokens for billed Grok/Claude calls, without query text or prices
logs/numbat-events.ndjsonlignoredDerived redacted audit and out-of-band events; observation only
logs/cyclaw.logignoredApplication log; a bounded writer drops records rather than holding a request on stalled I/O
logs/evals/ignoredOpt-in dogfood and judge output, separate from production billing
index/ignoredChroma and bm25.json; rebuild after corpus changes
data/personality/soul.mdtrackedChanges appear in git status and broad staging
data/personality/cyclaw_soul.dbignoredVersion DB; not yet generated in a clean checkout
data/personality/soul.md.bakignoredVetted backup; not yet generated in a clean checkout

Day-two:

python -m metrics                           # spend, audit aggregates, sequences
python -m retrieval.clear_cache             # dry-run; --apply deletes .emb_cache
python -m retrieval.indexer                 # rebuild after editing data/corpus/
curl -s http://127.0.0.1:8787/index/status   # build progress

What It Does

CyClaw retrieves Markdown and .txt documents before generation. Output rails enforce a token-overlap floor on retrieved local answers; that heuristic does not verify individual claims, which the evals measure.

  1. Retrieval first. retrieve starts the 12-node graph; a miss can still reach offline_best_effort with partial context.
  2. Hybrid search. ChromaDB and BM25 fuse through RRF (retrieval.rrf_k). A vault hit needs the best cosine to clear retrieval.min_semantic_score (without cosines, the fused score must clear the RRF-scale retrieval.min_score); the cross-encoder runs in shadow mode (retrieval.min_rerank_score: null). Retrieval reference.
  3. Local generation. Ollama runs models.local_llm.model, shipped as qwen3.8:27b-mlx. Tunables live in config.yaml. Optionally, build a local tag that stores num_ctx on the model itself (derived tag). macos/Modelfile.cg builds the author's qwen3.8:27b-mlx-cg, which is not a published model. Use your own tag name, but keep a suffix so it never overwrites the registry tag qwen3.8:27b-mlx. Set num_ctx and the sampling parameters to fit your Ollama max context, unified memory, and model.
  4. Governed soul. data/personality/soul.md has SHA-256 drift detection and atomic writes; POST /soul/apply requires a human reason and an enforced injection scan. Restore, /soul/reload, and startup drift recovery are the documented unscanned or advisory exceptions, and a missing soul self-initializes (Soul invariants, Rules 4–5).
  5. Confirmed external fallback. Grok (grok-4.5, api.x.ai) or Claude (claude-sonnet-5, api.anthropic.com) needs hybrid mode, that provider enabled and selected, user_confirmed_online: true on the request, and a key. Calls share the remaining api.graph_timeout_sec budget, and utils/endpoint_trust.py rejects rewritten provider URLs.
  6. HTTP and MCP. FastAPI serves the browser at /; the separate MCP server exposes sanitized retrieval only (sampling: None, no generation).
  7. Audit convergence. Every path converges at audit_logger before END; cyclaw-metrics reads the audit offline.

The six invariants pin these guarantees plus import isolation of the core modules; INVARIANTS.md states Rules 1–9 and the test behind each.

Storage and console. indexing.vector_backend defaults to embedded chroma (optional pgvector); BM25 is JSON, never pickle. static/terminal.html provides queries, the first-run build panel, health chips, Soul, Sync, Agentic, Filesystem, and SQL panels, plus Users and Audit when auth is on.

Optional layers at a glance

LayerPurposeShips
Per-user authPasswords, sessions, device tokens, rolesoff
MemorySQLite/FTS5 facts and episodes, propose/apply, optional retrieval fusion and HTML exportoff
NeMo GuardrailsDeny-only input/output checks, deterministic fallback on NeMo failureon
Dropbox syncOut-of-band rclone corpus pullCLI
ConnectorsScoped filesystem, SELECT-only SQL, passive LAN inventoryoff
Agentic loopGitHub context, skills, clone/plan/patch/verify, human decisionsoff
Telegram / OpenTweetPhone remote and X drafts through loopback /queryoff
Numbat stream / pre-action hookDerived NDJSON, observation only; deny-only gate before a confirmed external callon / off
Spend ledgerBilled token countson
Fine-tune kitSeparate QLoRA toolkittoolkit

Architecture

gate.py initializes the soul at startup; requests pass the body cap, Host allowlist, any required authentication, the 60/min per-IP limit, and the injection filter (in that order), then enter the graph as GraphState.

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"]
        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\nbest cosine ≥ 0.30?\n(RRF ≥ 0.028 if no cosine)"]
        G -->|"YES — local context"| X["guardrail_input\noffline rail · default on"]
        X -->|"blocked"| L
        X -->|"passed · high score"| H["local_llm\nOllama :11434"]
        G -->|"NO — vault miss"| I["user_gate\nneeds_confirm = true"]
        I -->|"confirmed + hybrid\n+ grok.enabled + provider=grok"| PG["pre_action_hook_grok\ndeny-only · off = pass-through"]
        PG -->|"allow"| J["grok_fallback\ngrok-4.5 · triple-gated"]
        I -->|"confirmed + hybrid\n+ claude.enabled + provider=claude"| PC["pre_action_hook_claude\ndeny-only · off = pass-through"]
        PC -->|"allow"| W["claude_fallback\nclaude-sonnet-5 · triple-gated"]
        I -->|"declined or offline"| X
        X -->|"passed · vault miss"| K["offline_best_effort\nlocal LLM · no RAG gate"]
        I -->|"confirmed=None — pause"| L
        H --> Y["guardrail_output\noffline rail · default on\ngrounding: local_llm only"]
        J --> Y
        W --> Y
        K --> Y
        Y --> L
        PG -.->|"deny"| L
        PC -.->|"deny"| L
        L(["audit_logger\nHMAC fingerprint · PII redact\nlogs/audit.jsonl\n+ derived Numbat stream"])
    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"]
        Q <--> R
    end

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

The MCP server calls the retriever directly after sanitization and never enters this gateway or graph. The diagram omits the per-answer NeMo check() and the post-graph CEL monitor (Optional layers).


API Key Setup (Soul Mutations)

/soul/*, /ops/*, /memory/*, /query/export/html, and /audit/summary require operator access. /health is public; /query requires a session or device token when auth.enabled is true. Operator credentials are:

  • Bearer CYCLAW_API_KEY, for HTTP clients and scripts (compared with hmac.compare_digest).
  • The console cookie. "Unlock operator tools" trades the key once for an HttpOnly, SameSite=Strict cookie (security.console_session_ttl_sec, 1 h shipped); "Lock" deletes it with matching TLS attributes, and rotating the key revokes every cookie.
  • An admin login, when auth.enabled is on (no key needed); operator and audit accounts do not get operator access.

Cookie writes need CSRF tokens, cookies reject cross-site requests, console tokens bind the request origin, and TLS uses __Host- cookies. Cookies are not port-scoped, so give CyClaw its own hostname plus TLS when other services on the host are untrusted. Without CYCLAW_API_KEY, Bearer and cookie access fail closed.

macos/invoke-cyclaw.sh and powershell\Invoke-CyClaw.ps1 generate a missing key into the OS keystore and open a single-use #pair=... unlock link (5 minutes). To unlock by hand, copy the key into the dialog:

# macOS
security find-generic-password -a "$(whoami)" -s com.cgfixit.cyclaw.api-key -w | pbcopy
# Windows, from the CyClaw folder
. .\powershell\CyClaw-SecretStore.ps1; (Read-CyclawCredential com.cgfixit.cyclaw.api-key).Secret | Set-Clipboard

Do not persist the key with setx or a user environment variable; the launchers read it from the keystore into the gateway process only (powershell/README.md). Rotation, the macOS bootstrap, and 401 recovery: macos/README.md.

security.api_key_optional ships false; when set, a request skips the key only if the socket peer is loopback, no forwarding header is present, and the request is not cross-site (inert under Docker NAT). Full boundary: INVARIANTS.md Rule 6 and the threat model (fourteenth amendment).

Per-User Authentication

Separate from the operator API key. gate_auth.py: scrypt passwords, a session cookie plus CSRF for browsers, named device tokens for scripts, roles admin, operator, and audit. Ships auth.enabled: false; while off, every /auth/* route returns 503. Design, TLS, and the non-loopback bind rule: docs/AUTHENTICATION_DESIGN.md; first-boot curl and cyclaw-user: setup-guide.md. cyclaw-gen-cert writes the self-signed cert for the auth + TLS bind exception.


Spend Tracking

Billed Grok/Claude calls append tokens to logs/spend.jsonl; dollars are computed at read time, so rate-card fixes re-price history. Rows exclude queries, prompts, and keys, and a failed write drops the row, not the answer. Core Grok/Claude requests also reserve a durable call allowance before each POST attempt, retries included: 100 per UTC day and 1,000 per UTC month by default (zero denies all). Optional agentic providers are excluded, and dollar spend is not enforced.

sourceWriterCovers
queryllm/client.pyConfirmed /query fallback
agenticagentic/deepagent_github/chat_client.pyOut-of-band planner calls
evaltests/judge_eval.pyOpt-in judge runs, kept separately under logs/evals/

python -m metrics reports today and last_7d tokens and USD by provider and source, and its forensic Sequences section flags a blocked injection followed within 15 minutes by a paid call (no policy enforced). tests/spend_live_probe.py spends real money and is opt-in only. Ledger schema, rate bands, and probes.


Benchmarks and Evals

Only the retrieval gate blocks merges; none of these four paths is a graph node or a security control. Thresholds and limits.

EvaluationCommandScope
Retrieval gatepython -m tests.ci_rag_smokeEvery PR, no LLM. Corpus probes through route_by_score_node, then hit@5, Recall@5, and MRR on the 52-case fixture (44 scored, 8 out-of-corpus skipped). Metric floors and [FILTERED] injection-chunk assertions block CI
Local dogfoodCYCLAW_EVAL_DOGFOOD=1 python scripts/cyclaw-eval-dogfood.pyOpt-in real loopback model, one case per category (recipe)
Anthropic judgeCYCLAW_EVAL_LIVE=1 python tests/judge_eval.pyPaid opt-in Claude grades groundedness, completeness, abstention
Local judgeSame command with evals.local_judge.enabled: trueSame rubric on a second loopback model from another family

Fixture retrieval scores 1.0 on all three metrics (no claim about arbitrary corpora). The reranker bake-off found no passing threshold, so vetoing stays off. The only published local run is the Qwen dogfood audit; no judge result is published yet.


Current development

Recent work: the tracked git-hook secrets gate (docs/GITHOOKS.md), sandboxed agentic verification, hashed installs, credential and request-body hardening, and console first-run fixes. The merged PRs and docs/changelog.txt are the record.


Optional layers

Dropbox sync. Generated schedules retrieve the encrypted-config password from macOS Keychain or Windows Credential Manager at run time; setup and limits. rclone pulls into data/corpus/ outside the request path, bounded by max_delete, max_transfer, and a single-instance lock; exit code 10 signals a reindex. python -m sync.cli setup, then test, sync --dry-run, sync, status, schedule, or unschedule (guide, CLI).

Native scheduling. Generators write plist/task files and print load commands; token-bearing jobs use the keystore wrappers, and a supervised gateway (macos/generate_service_plist.py, windows/generate_service_task.py) requires --confirm and a non-empty --reason (macOS operations, launchd design).

Fine-tuning. The QLoRA kit is excluded from runtime installs and audited separately; finetune_qwen38.py may download its base checkpoint, so seed caches before a no-egress run. GPU-free dry run: python tools/lora_finetune/build_cyclaw_corpus.py, then python tools/lora_finetune/dryrun_finetune.py (toolkit).

Agentic coding. agentic.enabled: false makes the out-of-band CLI a no-op; writes also need a per-call reason and confirm, and allow_git_write_tools ships false. python -m agentic.cli real-repo-run clones into a jail, plans, patches, and verifies, then waits for a human decision before committing; push and draft PR are separate decisions. Checks run in fresh gitless copies under Linux bubblewrap or macOS Seatbelt and fail closed without them (Windows refuses); no platform uses a microVM (threat model). real-repo-run* is CLI-only (POST /ops/agentic returns 422). Git helper containment, identity pinning, and orphan cleanup: workspace controls.

python -m agentic.cli status
python -m agentic.cli context --repo          # also --pr 123 / --issue 45

Real-repo loop, governed harness, and write rollback.

Connectors. All three ship off, outside the request path: fsconnect (bounded reads; writes gated, refused on Windows), sqlconnect (SELECT/WITH only), and netconnect (passive, explicit RFC1918/loopback CIDRs). Filesystem, SQL, and passive network.

NeMo Guardrails

guardrails.enabled: true ships in config.yaml; explicit false or an absent block disables it. utils/guardrail_bridge.py is the graph's only path to guardrails/.

GuardScope and refusal
Offline graph inputLocal and best-effort paths; returns block_message through audit_logger without a model call
Offline graph outputlocal_llm only; replaces answers below hallucination_threshold (0.18) or matching soul-leak markers
NeMo check()Wraps all four answer nodes (nemoguardrails==0.24.0); input refusal skips generation, output refusal replaces the answer
Broker fallbackMissing, failed, or unsupported live verdicts run deterministic input and soul-leak checks on every answer route; grounding stays local_llm only

Active rails are Python checks and add no model calls; NVIDIA's model-assisted self_check_* rails are inactive. NeMo failures audit guardrail_degraded. Verify an install with python -m pip check and python -m guardrails.verify_install (setup guide, package guide, NeMo reference, Track B record).

Numbat

CyClaw calls the external CLI pinned at 0.2.0 (schema 0.3.0); it never vendors or imports it.

PieceSwitchDefault and behavior
Streamnumbat.enabledOn. utils/numbat_emitter.py writes redacted audit/out-of-band events to logs/numbat-events.ndjsonl (rolls at 50 MiB); its bounded writer cannot hold requests
Pre-action hookpolicy.fallback.pre_action_hook.enabledOff. Prepared with engine: numbat and monitor-only rules in config/numbat/gate/; after consent, rules test --no-builtin-rules denies enforcing matches and every engine failure
CEL monitornumbat.cel.enabled, extra numbat-celOff. Two structured-field rules observe weak-retrieval cloud answers and hook/guardrail refusals after /query; never blocks
Offline scoringNoneOperator CLI and numbat-rules.yml; fixture, stream-contract, and CEL checks block CI

Nothing scores the live file during a request. The CLI and CEL extra are not in standard installs: keep their switches off until installed (an enabled hook without the binary denies every confirmed external call), trial rules before promoting any to enforce: true, and never use numbat hook as the command engine (it exits 0 on errors). Once enabled, /health reports hook and CEL readiness and /audit/summary shows pre_action_hook_last_verdict. Pre-action gate, stream, phase status, and the Track A acceptance record.

Telegram and OpenTweet

Both ship off and call loopback POST /query outside the core graph imports, reading credentials from the environment variable named in config. Telegram offers outbound notify or long-poll chat (no public webhooks), requires non-empty allowed_chat_ids, and can confirm one paid call only through the exact /online on <grok|claude> command with allow_hybrid_confirm on (default off); the triple gate still applies. OpenTweet forces user_confirmed_online: false and writes drafts; scheduled_date needs opentweet.schedule_enabled, and schedulers never send publish_now. Check either with python -m telegram.cli status / python -m opentweet.cli status. Telegram design and operations; OpenTweet design and operations.


Security Model

LayerMechanism
NetworkBinds 127.0.0.1:8787; request bodies capped at 1 MiB (security.max_request_body_bytes). A non-loopback api.host is refused except the documented auth + TLS path, or CYCLAW_ALLOW_NON_LOOPBACK_BIND (bind guard)
Endpoint trustLocal nodes: loopback or an exact host in models.local_llm.trusted_hosts (ships []). Online nodes: api.x.ai and api.anthropic.com only
Inputpolicy.prompt_filter: 40 banned_patterns, max_input_chars; same filter on MCP search
Rate limit60 req/min per IP, after same-origin rejection and before the filter; in-memory unless SQLite or Postgres is set
Proxy bypasshttpx clients set trust_env=False
TelemetryKill maps before any SDK import (invariant-guard G1), plus ONNX's post-import call; not a network firewall (SECURITY.md)
AuditHMAC-SHA256 query fingerprint + redacted metadata in logs/audit.jsonl, then the derived Numbat stream
Grok / ClaudeTriple gate item 5, then the opt-in deny-only pre-action hook
Soul writesHuman reason + enforced scan + atomic replace on POST /soul/apply only (What It Does item 4 lists the exceptions)
API keyFail closed: Bearer key, CSRF-guarded console cookie, or an admin login; the opt-in loopback bypass is peer-bound (API Key Setup)
GuardrailsOn by default, deny-only, deterministic on NeMo failure (NeMo Guardrails)
Optional surfacesAgentic writes, connectors, channels, and /memory/* ship off; /ops/* needs operator access and runs subprocess argv lists; no tokens in plist or task XML
/auth/*Present either way; 503 while auth is off. When on, /query needs a session or device token and refuses audit accounts; the last admin cannot be removed
ContainerNon-root, no-new-privileges, dropped caps, read-only rootfs; optional Falco (deploy/falco/) ships off
Dependency riskchromadb==1.5.9 carries CVE-2026-45829, accepted only for embedded PersistentClient. SECURITY.md
Commit-time gateTracked .githooks/ refuse secrets and private data at commit and push, protected-path edits at commit (reminder only at push), and main/force pushes without an operator override (docs/GITHOOKS.md)

Full threat model: docs/THREAT_MODEL.md. Design notes: docs/security-philosophy/.


Project Structure

CyClaw/
├── gate.py  gate_ops.py  gate_auth.py  gate_memory.py   # gateway, /ops, /auth, /memory
├── graph.py                # 12-node LangGraph
├── metrics.py              # cyclaw-metrics: audit + spend + sequences
├── mcp_hybrid_server.py    # retrieval-only MCP
├── config.yaml             # tunables
├── retrieval/  llm/        # hybrid search, embeddings, rerank; local, Grok, Claude clients
├── utils/                  # sanitizer, logger, personality, spend, numbat_*, endpoint_trust, auth
├── guardrails/             # default-on rails, reached only via guardrail_bridge
├── memory/  agentic/  sync/  telegram/  opentweet/   # optional layers, off or CLI-only
├── macos/  powershell/  windows/                     # native installers and schedulers
├── spend/  schemas/  static/  tests/  docs/  deploy/
├── tools/lora_finetune/    # operator QLoRA kit, not a runtime extra
└── .github/workflows/

data/corpus/ is the sample corpus, data/personality/soul.md the live soul; data/agentic/skills_registry.json ships empty.


Documentation Map

Docs each section above links are not repeated here.

Read thisWhen you want
setup-guide.mdInstall paths, Docker, conda, and every REST call
INVARIANTS.mdRules 1–9, code vs convention, and the test that pins each
CLAUDE.md / AGENTS.md / docs/GITHOOKS.mdRequest-path map, agent rules, and the commit/push security gate
SECURITY.md / docs/THREAT_MODEL.mdEgress, accepted CVEs, disclosure; scope, bind exceptions, amendments
docs/AUTHENTICATION_DESIGN.md / docs/DOCKER.mdPer-user auth; GHCR image and compose hardening
docs/security-philosophy/Telemetry suppression, offline operation, and Numbat designs
macos/README.md / powershell/README.mdKeychain, Credential Manager, 401 recovery

License

Source-available, all rights reserved; personal use is permitted. See LICENSE.

Designed and built by Chris Grady, with AI tooling used under human review, CI, and the invariant guard.

cyclaw
langchain
langgraph
mcp
mcp-server
offline-first
own-your-data
personal
python
security-awareness
telemetry-data

cgfixit/CyClaw

CyClaw is a Local, offline-first local AI agent - https://cglocalai.grok.me/

Python

6

3,667 commits

updated Oct 15, 2026

See the code

README

CyClaw

Python 3.12 FastAPI LangGraph CyClaw CI/CD testing

CyClaw console answering from the local library

CyClaw answers questions from your documents on your hardware. Its 12-node LangGraph starts with retrieval, ends every path in the audit log, and requires per-question consent for paid Grok or Claude fallback. The server binds to 127.0.0.1:8787.

  • Embeddings, BM25, and the cross-encoder run locally on CPU; cache both retrieval models before going offline.
  • Soul, ops, memory, and audit routes require operator access, which fails closed without CYCLAW_API_KEY (an enabled admin's login also works when per-user auth is on).
  • Audit records hash questions by default; the spend ledger records billed tokens; cyclaw-metrics reads both offline.
  • Guardrails (NeMo in every base install), the Numbat stream, and the spend ledger ship on. Auth, memory, connectors, the agentic loop, and Telegram/X channels ship off.

CyClaw serves one trusted operator, or a mutually trusted group with auth enabled; it provides neither tenant isolation nor a microVM (threat model).

Step-by-step installs and every REST call: Full Setup Guide. Contributor rules for people and agents: CLAUDE.md, AGENTS.md, and the tracked git-hook gate in docs/GITHOOKS.md.

Security review #1526's residual decision register separates implemented controls, accepted limits (native Windows verification stays disabled; supported confinement still permits host reads), and optional research.

Quick Start

macOS (Apple Silicon) — install, keys, Ollama, index, and startup:

git clone https://github.com/cgfixit/CyClaw && cd CyClaw
bash macos/setup-cyclaw.sh

Linux — Ollama already running on 127.0.0.1:11434:

git clone https://github.com/cgfixit/CyClaw && cd CyClaw
python3.12 -m venv .venv && source .venv/bin/activate
pip install --require-hashes --no-deps -r locks/requirements-torch-lock-linux.txt --index-url https://download.pytorch.org/whl/cpu
pip install --require-hashes -r locks/requirements-lock-linux.txt
pip install --require-hashes -r locks/requirements-test-lock-linux.txt
ollama pull qwen3.8:27b-mlx
export CYCLAW_API_KEY="$(openssl rand -hex 20)"  # operator routes; /query uses sessions/tokens when auth is on
python -m retrieval.indexer                      # once; without this, /query is 503
python gate.py                                   # http://127.0.0.1:8787

CI checks every lock group against recorded inputs and hashes, and checks wheel dependency metadata. Refresh on arm64 macOS with the maintained lock scripts; generation failures leave tracked locks unchanged.

Windows uses the same pins with the -windows lock files (py -3.12 -m venv .venv, .\.venv\Scripts\Activate.ps1): Windows, Linux.

Confirm: curl http://127.0.0.1:8787/health, then open http://127.0.0.1:8787/. A manual macOS install uses plain torch==2.13.0 (no +cpu wheel) then the hashed macOS lock (macOS (Apple Silicon)); updating an existing environment ends with the NeMo verification.


For newcomers

Python and install paths. Use Python 3.12 (>=3.12,<3.13), not an unversioned python3 that may resolve to 3.11; .venv needs no admin rights. Choose native macOS, Windows, the Linux Quick Start, Docker (linux/amd64, published on 127.0.0.1 only), or conda (environment.yml). The constrained editable install in the setup guide adds the cyclaw-* commands; python -m … always works.

Secrets. gate.py reads environment variables, never dotenv files (Invoke-CyClaw.ps1 reads only non-secret settings from an owner-only %USERPROFILE%\.CyClaw\.env or checkout dotenv).

Secret persistence.

PlatformSetupOperationDefault secret storePlaintext opt-in
macOSmacos/setup-cyclaw-keys.shSet up or preserve gateway keyKeychain--write-env-file
Windowspowershell/Install-CyClaw.ps1Migrate existing plaintext secretsCredential Manager-WriteEnvFile

On Linux, macos/invoke-cyclaw.sh stores the gateway key in libsecret or an owner-only file (macos/cyclaw-linux-key.sh). Services read secrets at execution through macos/cyclaw-keychain-env.sh or powershell/CyClaw-CredMan-Env.ps1, never from dotenv, plist, task XML, config.yaml, shell rc files, or argv; a configured keystore lookup fails closed when its item is missing. A missing provider key leaves that provider unavailable, not the server down (provider keys).

Offline and hybrid. Shipped app.mode: hybrid permits a paid call only when the selected provider is enabled, available, and confirmed for that one request; confirmation is never persisted. Set app.mode: offline or decline to stay local. Model downloads are separate from that consent: models.embeddings.offline_after_index: true forces local files after indexing, and the ~91 MB reranker loads on the first query, so cache it first.

Ports. Gateway 127.0.0.1:8787 (api.port, launcher override CYCLAW_GATE_PORT), Ollama 127.0.0.1:11434. The old :8790 coding console is now the separate CG-agent-harness.

First-run checks and limits.

  • Without an index, /query returns 503 INDEX_NOT_FOUND. Run python -m retrieval.indexer, or click Build my library in the console (POST /index/build, loopback + same-origin + no forwarding headers, then GET /index/status); once CYCLAW_API_KEY is set a locked build opens the unlock dialog and retries after the unlock.
  • The console header shows the mode in plain words ("Cloud fallback · ask first" or "Offline only") and Library/Engine health chips. /health reporting degraded usually means Ollama is down; TELEMETRY KILL at startup is expected.
  • The 60/min per-IP rate limit resets on restart unless api.rate_limit.persist_path or a Postgres DSN is set. /health and /index/status are public and not rate-limited. Request bodies above security.max_request_body_bytes (1 MiB) get 413.
  • Browser CORS allows http://127.0.0.1:8787 and http://localhost:8787; /query always rejects cross-site requests.
  • Restart the gateway after editing sanitizer config (it is cached). CYCLAW_EMBED_CACHE_SIZE (default 2048) is an env var, not a YAML key.
  • Offline checks: python .claude/skills/invariant-guard/check_invariants.py and GROK_API_KEY=dummy python -m pytest tests/ -q --tb=short (no live provider).

Local records. Paths are relative to the repository.

PathGit statusContents and behavior
logs/audit.jsonlignoredAuthoritative audit, written synchronously on the request thread. Questions use persistent HMAC-SHA256 fingerprints by default, including when logging.audit_fields is absent or empty. Explicit include_query_hash: false stores redacted query text
logs/spend.jsonlignoredTokens for billed Grok/Claude calls, without query text or prices
logs/numbat-events.ndjsonlignoredDerived redacted audit and out-of-band events; observation only
logs/cyclaw.logignoredApplication log; a bounded writer drops records rather than holding a request on stalled I/O
logs/evals/ignoredOpt-in dogfood and judge output, separate from production billing
index/ignoredChroma and bm25.json; rebuild after corpus changes
data/personality/soul.mdtrackedChanges appear in git status and broad staging
data/personality/cyclaw_soul.dbignoredVersion DB; not yet generated in a clean checkout
data/personality/soul.md.bakignoredVetted backup; not yet generated in a clean checkout

Day-two:

python -m metrics                           # spend, audit aggregates, sequences
python -m retrieval.clear_cache             # dry-run; --apply deletes .emb_cache
python -m retrieval.indexer                 # rebuild after editing data/corpus/
curl -s http://127.0.0.1:8787/index/status   # build progress

What It Does

CyClaw retrieves Markdown and .txt documents before generation. Output rails enforce a token-overlap floor on retrieved local answers; that heuristic does not verify individual claims, which the evals measure.

  1. Retrieval first. retrieve starts the 12-node graph; a miss can still reach offline_best_effort with partial context.
  2. Hybrid search. ChromaDB and BM25 fuse through RRF (retrieval.rrf_k). A vault hit needs the best cosine to clear retrieval.min_semantic_score (without cosines, the fused score must clear the RRF-scale retrieval.min_score); the cross-encoder runs in shadow mode (retrieval.min_rerank_score: null). Retrieval reference.
  3. Local generation. Ollama runs models.local_llm.model, shipped as qwen3.8:27b-mlx. Tunables live in config.yaml. Optionally, build a local tag that stores num_ctx on the model itself (derived tag). macos/Modelfile.cg builds the author's qwen3.8:27b-mlx-cg, which is not a published model. Use your own tag name, but keep a suffix so it never overwrites the registry tag qwen3.8:27b-mlx. Set num_ctx and the sampling parameters to fit your Ollama max context, unified memory, and model.
  4. Governed soul. data/personality/soul.md has SHA-256 drift detection and atomic writes; POST /soul/apply requires a human reason and an enforced injection scan. Restore, /soul/reload, and startup drift recovery are the documented unscanned or advisory exceptions, and a missing soul self-initializes (Soul invariants, Rules 4–5).
  5. Confirmed external fallback. Grok (grok-4.5, api.x.ai) or Claude (claude-sonnet-5, api.anthropic.com) needs hybrid mode, that provider enabled and selected, user_confirmed_online: true on the request, and a key. Calls share the remaining api.graph_timeout_sec budget, and utils/endpoint_trust.py rejects rewritten provider URLs.
  6. HTTP and MCP. FastAPI serves the browser at /; the separate MCP server exposes sanitized retrieval only (sampling: None, no generation).
  7. Audit convergence. Every path converges at audit_logger before END; cyclaw-metrics reads the audit offline.

The six invariants pin these guarantees plus import isolation of the core modules; INVARIANTS.md states Rules 1–9 and the test behind each.

Storage and console. indexing.vector_backend defaults to embedded chroma (optional pgvector); BM25 is JSON, never pickle. static/terminal.html provides queries, the first-run build panel, health chips, Soul, Sync, Agentic, Filesystem, and SQL panels, plus Users and Audit when auth is on.

Optional layers at a glance

LayerPurposeShips
Per-user authPasswords, sessions, device tokens, rolesoff
MemorySQLite/FTS5 facts and episodes, propose/apply, optional retrieval fusion and HTML exportoff
NeMo GuardrailsDeny-only input/output checks, deterministic fallback on NeMo failureon
Dropbox syncOut-of-band rclone corpus pullCLI
ConnectorsScoped filesystem, SELECT-only SQL, passive LAN inventoryoff
Agentic loopGitHub context, skills, clone/plan/patch/verify, human decisionsoff
Telegram / OpenTweetPhone remote and X drafts through loopback /queryoff
Numbat stream / pre-action hookDerived NDJSON, observation only; deny-only gate before a confirmed external callon / off
Spend ledgerBilled token countson
Fine-tune kitSeparate QLoRA toolkittoolkit

Architecture

gate.py initializes the soul at startup; requests pass the body cap, Host allowlist, any required authentication, the 60/min per-IP limit, and the injection filter (in that order), then enter the graph as GraphState.

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"]
        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\nbest cosine ≥ 0.30?\n(RRF ≥ 0.028 if no cosine)"]
        G -->|"YES — local context"| X["guardrail_input\noffline rail · default on"]
        X -->|"blocked"| L
        X -->|"passed · high score"| H["local_llm\nOllama :11434"]
        G -->|"NO — vault miss"| I["user_gate\nneeds_confirm = true"]
        I -->|"confirmed + hybrid\n+ grok.enabled + provider=grok"| PG["pre_action_hook_grok\ndeny-only · off = pass-through"]
        PG -->|"allow"| J["grok_fallback\ngrok-4.5 · triple-gated"]
        I -->|"confirmed + hybrid\n+ claude.enabled + provider=claude"| PC["pre_action_hook_claude\ndeny-only · off = pass-through"]
        PC -->|"allow"| W["claude_fallback\nclaude-sonnet-5 · triple-gated"]
        I -->|"declined or offline"| X
        X -->|"passed · vault miss"| K["offline_best_effort\nlocal LLM · no RAG gate"]
        I -->|"confirmed=None — pause"| L
        H --> Y["guardrail_output\noffline rail · default on\ngrounding: local_llm only"]
        J --> Y
        W --> Y
        K --> Y
        Y --> L
        PG -.->|"deny"| L
        PC -.->|"deny"| L
        L(["audit_logger\nHMAC fingerprint · PII redact\nlogs/audit.jsonl\n+ derived Numbat stream"])
    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"]
        Q <--> R
    end

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

The MCP server calls the retriever directly after sanitization and never enters this gateway or graph. The diagram omits the per-answer NeMo check() and the post-graph CEL monitor (Optional layers).


API Key Setup (Soul Mutations)

/soul/*, /ops/*, /memory/*, /query/export/html, and /audit/summary require operator access. /health is public; /query requires a session or device token when auth.enabled is true. Operator credentials are:

  • Bearer CYCLAW_API_KEY, for HTTP clients and scripts (compared with hmac.compare_digest).
  • The console cookie. "Unlock operator tools" trades the key once for an HttpOnly, SameSite=Strict cookie (security.console_session_ttl_sec, 1 h shipped); "Lock" deletes it with matching TLS attributes, and rotating the key revokes every cookie.
  • An admin login, when auth.enabled is on (no key needed); operator and audit accounts do not get operator access.

Cookie writes need CSRF tokens, cookies reject cross-site requests, console tokens bind the request origin, and TLS uses __Host- cookies. Cookies are not port-scoped, so give CyClaw its own hostname plus TLS when other services on the host are untrusted. Without CYCLAW_API_KEY, Bearer and cookie access fail closed.

macos/invoke-cyclaw.sh and powershell\Invoke-CyClaw.ps1 generate a missing key into the OS keystore and open a single-use #pair=... unlock link (5 minutes). To unlock by hand, copy the key into the dialog:

# macOS
security find-generic-password -a "$(whoami)" -s com.cgfixit.cyclaw.api-key -w | pbcopy
# Windows, from the CyClaw folder
. .\powershell\CyClaw-SecretStore.ps1; (Read-CyclawCredential com.cgfixit.cyclaw.api-key).Secret | Set-Clipboard

Do not persist the key with setx or a user environment variable; the launchers read it from the keystore into the gateway process only (powershell/README.md). Rotation, the macOS bootstrap, and 401 recovery: macos/README.md.

security.api_key_optional ships false; when set, a request skips the key only if the socket peer is loopback, no forwarding header is present, and the request is not cross-site (inert under Docker NAT). Full boundary: INVARIANTS.md Rule 6 and the threat model (fourteenth amendment).

Per-User Authentication

Separate from the operator API key. gate_auth.py: scrypt passwords, a session cookie plus CSRF for browsers, named device tokens for scripts, roles admin, operator, and audit. Ships auth.enabled: false; while off, every /auth/* route returns 503. Design, TLS, and the non-loopback bind rule: docs/AUTHENTICATION_DESIGN.md; first-boot curl and cyclaw-user: setup-guide.md. cyclaw-gen-cert writes the self-signed cert for the auth + TLS bind exception.


Spend Tracking

Billed Grok/Claude calls append tokens to logs/spend.jsonl; dollars are computed at read time, so rate-card fixes re-price history. Rows exclude queries, prompts, and keys, and a failed write drops the row, not the answer. Core Grok/Claude requests also reserve a durable call allowance before each POST attempt, retries included: 100 per UTC day and 1,000 per UTC month by default (zero denies all). Optional agentic providers are excluded, and dollar spend is not enforced.

sourceWriterCovers
queryllm/client.pyConfirmed /query fallback
agenticagentic/deepagent_github/chat_client.pyOut-of-band planner calls
evaltests/judge_eval.pyOpt-in judge runs, kept separately under logs/evals/

python -m metrics reports today and last_7d tokens and USD by provider and source, and its forensic Sequences section flags a blocked injection followed within 15 minutes by a paid call (no policy enforced). tests/spend_live_probe.py spends real money and is opt-in only. Ledger schema, rate bands, and probes.


Benchmarks and Evals

Only the retrieval gate blocks merges; none of these four paths is a graph node or a security control. Thresholds and limits.

EvaluationCommandScope
Retrieval gatepython -m tests.ci_rag_smokeEvery PR, no LLM. Corpus probes through route_by_score_node, then hit@5, Recall@5, and MRR on the 52-case fixture (44 scored, 8 out-of-corpus skipped). Metric floors and [FILTERED] injection-chunk assertions block CI
Local dogfoodCYCLAW_EVAL_DOGFOOD=1 python scripts/cyclaw-eval-dogfood.pyOpt-in real loopback model, one case per category (recipe)
Anthropic judgeCYCLAW_EVAL_LIVE=1 python tests/judge_eval.pyPaid opt-in Claude grades groundedness, completeness, abstention
Local judgeSame command with evals.local_judge.enabled: trueSame rubric on a second loopback model from another family

Fixture retrieval scores 1.0 on all three metrics (no claim about arbitrary corpora). The reranker bake-off found no passing threshold, so vetoing stays off. The only published local run is the Qwen dogfood audit; no judge result is published yet.


Current development

Recent work: the tracked git-hook secrets gate (docs/GITHOOKS.md), sandboxed agentic verification, hashed installs, credential and request-body hardening, and console first-run fixes. The merged PRs and docs/changelog.txt are the record.


Optional layers

Dropbox sync. Generated schedules retrieve the encrypted-config password from macOS Keychain or Windows Credential Manager at run time; setup and limits. rclone pulls into data/corpus/ outside the request path, bounded by max_delete, max_transfer, and a single-instance lock; exit code 10 signals a reindex. python -m sync.cli setup, then test, sync --dry-run, sync, status, schedule, or unschedule (guide, CLI).

Native scheduling. Generators write plist/task files and print load commands; token-bearing jobs use the keystore wrappers, and a supervised gateway (macos/generate_service_plist.py, windows/generate_service_task.py) requires --confirm and a non-empty --reason (macOS operations, launchd design).

Fine-tuning. The QLoRA kit is excluded from runtime installs and audited separately; finetune_qwen38.py may download its base checkpoint, so seed caches before a no-egress run. GPU-free dry run: python tools/lora_finetune/build_cyclaw_corpus.py, then python tools/lora_finetune/dryrun_finetune.py (toolkit).

Agentic coding. agentic.enabled: false makes the out-of-band CLI a no-op; writes also need a per-call reason and confirm, and allow_git_write_tools ships false. python -m agentic.cli real-repo-run clones into a jail, plans, patches, and verifies, then waits for a human decision before committing; push and draft PR are separate decisions. Checks run in fresh gitless copies under Linux bubblewrap or macOS Seatbelt and fail closed without them (Windows refuses); no platform uses a microVM (threat model). real-repo-run* is CLI-only (POST /ops/agentic returns 422). Git helper containment, identity pinning, and orphan cleanup: workspace controls.

python -m agentic.cli status
python -m agentic.cli context --repo          # also --pr 123 / --issue 45

Real-repo loop, governed harness, and write rollback.

Connectors. All three ship off, outside the request path: fsconnect (bounded reads; writes gated, refused on Windows), sqlconnect (SELECT/WITH only), and netconnect (passive, explicit RFC1918/loopback CIDRs). Filesystem, SQL, and passive network.

NeMo Guardrails

guardrails.enabled: true ships in config.yaml; explicit false or an absent block disables it. utils/guardrail_bridge.py is the graph's only path to guardrails/.

GuardScope and refusal
Offline graph inputLocal and best-effort paths; returns block_message through audit_logger without a model call
Offline graph outputlocal_llm only; replaces answers below hallucination_threshold (0.18) or matching soul-leak markers
NeMo check()Wraps all four answer nodes (nemoguardrails==0.24.0); input refusal skips generation, output refusal replaces the answer
Broker fallbackMissing, failed, or unsupported live verdicts run deterministic input and soul-leak checks on every answer route; grounding stays local_llm only

Active rails are Python checks and add no model calls; NVIDIA's model-assisted self_check_* rails are inactive. NeMo failures audit guardrail_degraded. Verify an install with python -m pip check and python -m guardrails.verify_install (setup guide, package guide, NeMo reference, Track B record).

Numbat

CyClaw calls the external CLI pinned at 0.2.0 (schema 0.3.0); it never vendors or imports it.

PieceSwitchDefault and behavior
Streamnumbat.enabledOn. utils/numbat_emitter.py writes redacted audit/out-of-band events to logs/numbat-events.ndjsonl (rolls at 50 MiB); its bounded writer cannot hold requests
Pre-action hookpolicy.fallback.pre_action_hook.enabledOff. Prepared with engine: numbat and monitor-only rules in config/numbat/gate/; after consent, rules test --no-builtin-rules denies enforcing matches and every engine failure
CEL monitornumbat.cel.enabled, extra numbat-celOff. Two structured-field rules observe weak-retrieval cloud answers and hook/guardrail refusals after /query; never blocks
Offline scoringNoneOperator CLI and numbat-rules.yml; fixture, stream-contract, and CEL checks block CI

Nothing scores the live file during a request. The CLI and CEL extra are not in standard installs: keep their switches off until installed (an enabled hook without the binary denies every confirmed external call), trial rules before promoting any to enforce: true, and never use numbat hook as the command engine (it exits 0 on errors). Once enabled, /health reports hook and CEL readiness and /audit/summary shows pre_action_hook_last_verdict. Pre-action gate, stream, phase status, and the Track A acceptance record.

Telegram and OpenTweet

Both ship off and call loopback POST /query outside the core graph imports, reading credentials from the environment variable named in config. Telegram offers outbound notify or long-poll chat (no public webhooks), requires non-empty allowed_chat_ids, and can confirm one paid call only through the exact /online on <grok|claude> command with allow_hybrid_confirm on (default off); the triple gate still applies. OpenTweet forces user_confirmed_online: false and writes drafts; scheduled_date needs opentweet.schedule_enabled, and schedulers never send publish_now. Check either with python -m telegram.cli status / python -m opentweet.cli status. Telegram design and operations; OpenTweet design and operations.


Security Model

LayerMechanism
NetworkBinds 127.0.0.1:8787; request bodies capped at 1 MiB (security.max_request_body_bytes). A non-loopback api.host is refused except the documented auth + TLS path, or CYCLAW_ALLOW_NON_LOOPBACK_BIND (bind guard)
Endpoint trustLocal nodes: loopback or an exact host in models.local_llm.trusted_hosts (ships []). Online nodes: api.x.ai and api.anthropic.com only
Inputpolicy.prompt_filter: 40 banned_patterns, max_input_chars; same filter on MCP search
Rate limit60 req/min per IP, after same-origin rejection and before the filter; in-memory unless SQLite or Postgres is set
Proxy bypasshttpx clients set trust_env=False
TelemetryKill maps before any SDK import (invariant-guard G1), plus ONNX's post-import call; not a network firewall (SECURITY.md)
AuditHMAC-SHA256 query fingerprint + redacted metadata in logs/audit.jsonl, then the derived Numbat stream
Grok / ClaudeTriple gate item 5, then the opt-in deny-only pre-action hook
Soul writesHuman reason + enforced scan + atomic replace on POST /soul/apply only (What It Does item 4 lists the exceptions)
API keyFail closed: Bearer key, CSRF-guarded console cookie, or an admin login; the opt-in loopback bypass is peer-bound (API Key Setup)
GuardrailsOn by default, deny-only, deterministic on NeMo failure (NeMo Guardrails)
Optional surfacesAgentic writes, connectors, channels, and /memory/* ship off; /ops/* needs operator access and runs subprocess argv lists; no tokens in plist or task XML
/auth/*Present either way; 503 while auth is off. When on, /query needs a session or device token and refuses audit accounts; the last admin cannot be removed
ContainerNon-root, no-new-privileges, dropped caps, read-only rootfs; optional Falco (deploy/falco/) ships off
Dependency riskchromadb==1.5.9 carries CVE-2026-45829, accepted only for embedded PersistentClient. SECURITY.md
Commit-time gateTracked .githooks/ refuse secrets and private data at commit and push, protected-path edits at commit (reminder only at push), and main/force pushes without an operator override (docs/GITHOOKS.md)

Full threat model: docs/THREAT_MODEL.md. Design notes: docs/security-philosophy/.


Project Structure

CyClaw/
├── gate.py  gate_ops.py  gate_auth.py  gate_memory.py   # gateway, /ops, /auth, /memory
├── graph.py                # 12-node LangGraph
├── metrics.py              # cyclaw-metrics: audit + spend + sequences
├── mcp_hybrid_server.py    # retrieval-only MCP
├── config.yaml             # tunables
├── retrieval/  llm/        # hybrid search, embeddings, rerank; local, Grok, Claude clients
├── utils/                  # sanitizer, logger, personality, spend, numbat_*, endpoint_trust, auth
├── guardrails/             # default-on rails, reached only via guardrail_bridge
├── memory/  agentic/  sync/  telegram/  opentweet/   # optional layers, off or CLI-only
├── macos/  powershell/  windows/                     # native installers and schedulers
├── spend/  schemas/  static/  tests/  docs/  deploy/
├── tools/lora_finetune/    # operator QLoRA kit, not a runtime extra
└── .github/workflows/

data/corpus/ is the sample corpus, data/personality/soul.md the live soul; data/agentic/skills_registry.json ships empty.


Documentation Map

Docs each section above links are not repeated here.

Read thisWhen you want
setup-guide.mdInstall paths, Docker, conda, and every REST call
INVARIANTS.mdRules 1–9, code vs convention, and the test that pins each
CLAUDE.md / AGENTS.md / docs/GITHOOKS.mdRequest-path map, agent rules, and the commit/push security gate
SECURITY.md / docs/THREAT_MODEL.mdEgress, accepted CVEs, disclosure; scope, bind exceptions, amendments
docs/AUTHENTICATION_DESIGN.md / docs/DOCKER.mdPer-user auth; GHCR image and compose hardening
docs/security-philosophy/Telemetry suppression, offline operation, and Numbat designs
macos/README.md / powershell/README.mdKeychain, Credential Manager, 401 recovery

License

Source-available, all rights reserved; personal use is permitted. See LICENSE.

Designed and built by Chris Grady, with AI tooling used under human review, CI, and the invariant guard.

cyclaw
langchain
langgraph
mcp
mcp-server
offline-first
own-your-data
personal
python
security-awareness
telemetry-data