Daemon/rfc+fix#123
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.