Skip to content

Daemon/rfc+fix#123

Merged
chauncygu merged 2 commits into
mainfrom
daemon/RFC+fix
May 13, 2026
Merged

Daemon/rfc+fix#123
chauncygu merged 2 commits into
mainfrom
daemon/RFC+fix

Conversation

@chauncygu
Copy link
Copy Markdown
Contributor

No description provided.

chauncygu and others added 2 commits May 12, 2026 20:02
Closes the remaining four scope items in RFC 0002 end-to-end
(~1500 LoC of code + ~900 LoC of tests + docs). Drilldown:

- F-4 #2 — Bridge notify forwarding: subprocess-runner's notify IPC
  now routes through cc_daemon.bridge_supervisor.notify(kind, text).
  Runner can target a specific bridge or "*" broadcast.
- F-4 #3 — Restart policy: RestartPolicy(mode, max_restarts,
  backoff_base/cap/jitter) with on-crash exponential-backoff respawn,
  Timer-based scheduling, identity-checked _unregister to defeat
  stop()/restart races.
- F-6 / F-7 / F-8 Phase 1 — Telegram / Slack / WeChat poll loops
  lifted into a single cc_daemon/bridge_supervisor.py worker,
  feature-flagged (CHEETAHCLAWS_ENABLE_F6/7/8). bridges SQLite
  persistence + bridge.{start,stop,list,send,status} RPCs.
- F-6 Phase 2 — Inbound refactor: session.send / session.reply /
  session.list_recent RPCs publish on the SSE bus; slim daemon-driven
  worker (daemon_phase2=True) replaces the REPL-shaped supervisor.
- F-9 — Cost-guardrail defaults under `cheetahclaws serve` + per-
  runner quota-pause hook (paused_budget IPC → quota_warn event →
  blocks on _resume_event → resume IPC unblocks). system.status +
  agent.resume(budget_overrides, name?) RPCs.

Audit also fixed 5 real bugs in the new code (WeChat field names,
Slack cursor seeding, Telegram long-poll responsiveness, stop()/
restart Timer race in _unregister, broader secret redaction in
_safe_cfg).

cheetahclaws.py picks up F-5's _proactive_foreign_daemon_running
helper so the REPL's proactive watcher steps aside when an external
cc_daemon owns the discovery file. (Also folds in the REPL '!command'
NUL/control-char/length sanity guard from the security hardening
sweep that's in the follow-up commit — same scope as the proactive
edits, kept here to avoid splitting the file.)

agent_runner.py gets the F-9 quota-pause _PipeAgentRunner overrides
plus a defensive err_msg='' init that the follow-up security commit
relies on for its iteration-loop failure-signature path.

Full suite: 2347 passing, 3 skipped (env-gated live LLM tests),
0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dboxing

Two rounds of fixes (CRITICAL + HIGH) from the in-repo code review.
All 2347 tests still green, zero regressions.

Bot tokens off argv / readline history:
- bridges/__init__.py: resolve_bridge_token() (env > REPL > config)
  + scrub_token_from_history() (walks readline.get_history_item
  backwards, removes any entry that embeds the token).
- bridges/telegram.py, bridges/slack.py: support single-arg form
  /telegram <chat_id> / /slack <channel_id> with $TELEGRAM_BOT_TOKEN
  / $SLACK_BOT_TOKEN; legacy two-arg form still works but prints a
  deprecation warning + auto-scrubs.
- bridges/slack.py: _slack_start_bridge gains explicit token/channel
  kwargs so env-sourced tokens never get persisted to config.json.

Web UI CSRF (double-submit cookie):
- web/server.py: mints ccsrf=<24B>; SameSite=Strict; Max-Age=86400
  on connections without one. Gates POST/PUT/PATCH/DELETE on
  matching X-CSRF-Token header. Exempt: /api/auth/{bootstrap,
  register,login,logout,api/auth}.
- web/static/js/csrf.js (new): monkey-patches window.fetch so every
  state-changing call carries the header automatically.
- web/chat.html, web/lab.html, _build_html: load csrf.js first.
- tests/test_web_api.py: httpx event_hook mirrors the browser
  behaviour for the regression suite.

Terminal session ownership (web/server.py):
- _PtySession(owner_uid=...) tags the JWT sub on creation.
- _check_pty_owner() refuses /api/{stream,input,resize} from anyone
  else with 403. Password-only mode (no JWT) keeps owner_uid=None
  and preserves the shared-secret model.

Bash hard-denylist (tools/shell.py):
- 8 regexes refuse rm -rf /, fork bomb, mkfs.*, dd of=/dev/sd…,
  > /dev/sd…, chmod -R 777 /, chown -R / regardless of
  permission_mode. NUL bytes / control chars / >64KB length rejected.
- Same denylist applied to bridges' !cmd (bridges/terminal_runner.py,
  bridges/interactive_session.py) and the REPL '!command' escape.

Filesystem credential denylist (tools/security.py):
- Default deny: SSH private keys (~/.ssh/id_*), ~/.aws, ~/.gnupg,
  ~/.kube, ~/.docker, ~/.netrc, ~/.pgpass, /etc/shadow,
  /etc/sudoers*, /root. Public-by-convention SSH files
  (config, known_hosts, authorized_keys) remain readable.
  CHEETAHCLAWS_FS_NO_SANDBOX=1 to bypass.

Plugin loader (plugin/loader.py):
- New CHEETAHCLAWS_DISABLE_PLUGINS=1 kill switch +
  CHEETAHCLAWS_PLUGIN_ALLOWLIST=a,b,c whitelist.
- Module paths confined to install_dir (no ../../etc traversal).
- EXTERNAL-scope plugins print a one-time stderr warning.

MCP env sanitisation (cc_mcp/client.py):
- _sanitized_mcp_env strips LD_PRELOAD, LD_LIBRARY_PATH, LD_AUDIT,
  DYLD_*, PYTHONPATH, PYTHONSTARTUP, PYTHONHOME, PYTHONEXECUTABLE,
  NODE_OPTIONS, NODE_PATH, BASH_ENV, ENV from server-config env
  maps. CHEETAHCLAWS_MCP_TRUST_ENV=1 to allow.
- Reader loop dict.pop() instead of `in`+index to drop late
  responses after timeout cleanly.

macOS daemon peer-cred (cc_daemon/auth.py):
- ctypes-loaded getpeereid(2) for darwin/*bsd. Linux SO_PEERCRED
  path unchanged.

Web JWT secret (web/auth.py):
- O_CREAT | O_EXCL + 0o600 + post-write mode verification. Refuses
  to read a world-readable secret file with a clear chmod hint.
  Override with CHEETAHCLAWS_WEB_SECRET (recommended for production).

Smaller fixes folded in:
- web/server.py: terminal one-time password 6 → 32 chars (~190 bits
  of entropy).
- cc_config.py: save_config strips permission_mode=accept-all before
  persisting — session-scoped, no longer outlives launches.
- session_store.py: save_session wrapped in module-level Lock +
  BEGIN IMMEDIATE / ROLLBACK so concurrent same-id writes can't
  silently drop changes; LIKE fallback in search_sessions escapes
  %/_/\\.
- compaction.py: compact_messages wraps stream_auxiliary in try/
  except + falls back to original messages.
- providers.py: _recover_args_from_text caps scan window to last
  32KB of accumulated text.
- context.py: get_git_info / get_claude_md gain TTL caches
  (30s/10s, keyed by cwd).
- tool_registry.py: _cache_key adds session_id dimension so reads
  cached for one session don't leak to another.
- tools/shell.py: _bash_hard_denied helper + early NUL/length/
  denylist gate exposed for the bridge runners to import.
- tmux_tools.py: _run rewritten to take argv list + shell=False
  instead of f-string + shell=True.
- web/static/js/settings.js: _renderModels switches to
  data-model + delegated click handler so server-supplied model
  names can't break out of the onclick attr (deep-trust XSS hole).

Frontend XSS audit confirmed _esc (textContent→innerHTML) +
_renderMd (HTML-tag-strip → marked) cover all user/model content
paths; only the settings.js hole above needed a fix.

New CHEETAHCLAWS_* env vars (all documented in
docs/guides/security.md):

  TELEGRAM_BOT_TOKEN, SLACK_BOT_TOKEN
  CHEETAHCLAWS_BRIDGE_TERMINAL   (default 1; 0 = hard-disable)
  CHEETAHCLAWS_FS_NO_SANDBOX     (default 0)
  CHEETAHCLAWS_DISABLE_PLUGINS   (default 0)
  CHEETAHCLAWS_PLUGIN_ALLOWLIST  (default unset = all)
  CHEETAHCLAWS_MCP_TRUST_ENV     (default 0)
  CHEETAHCLAWS_WEB_SECRET        (default = file on disk)

Docs:
- New docs/guides/security.md is the single reference for the
  threat model + every env var + every defence.
- docs/guides/bridges.md updated for env-token-first setup +
  deprecation warning + new 'Remote !shell-command' section.
- docs/guides/web-ui.md gains CSRF + terminal-session-ownership
  sections.
- docs/news.md + README.md News carry both this hardening entry
  and the F-1..F-9 daemon roadmap announcement (which previously
  lived only in working-tree docs and lands properly here).
- docs/README.md + README.md Content+Documentation tables link
  to the new security guide.
- .env.example covers every new env var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chauncygu chauncygu merged commit 07f0e58 into main May 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant