diff --git a/skills/soul-guardian/SKILL.md b/skills/soul-guardian/SKILL.md index 4eb2202..f5a8870 100644 --- a/skills/soul-guardian/SKILL.md +++ b/skills/soul-guardian/SKILL.md @@ -1,7 +1,7 @@ --- name: soul-guardian -version: 0.0.1 -description: Drift detection + baseline integrity guard for an agent workspace's auto-loaded prompt/instruction markdown files (SOUL.md, AGENTS.md, etc.), with per-file policies, tamper-evident audit logging, and optional auto-restore. +version: 0.0.2 +description: Drift detection + baseline integrity guard for agent workspace files with automatic alerting support homepage: https://clawsec.prompt.security metadata: {"openclaw":{"emoji":"šŸ‘»","category":"security"}} clawdis: @@ -10,91 +10,154 @@ clawdis: bins: [python3] --- -# soul-guardian +# soul-guardian šŸ‘» -Use this skill to detect and respond to unexpected edits in the workspace files that the agent auto-loads. +Protects your agent's core files (SOUL.md, AGENTS.md, etc.) from unauthorized changes with automatic detection, restoration, and **user alerting**. -## Installation Options +## Quick Start (3 Steps) -You can get soul-guardian in two ways: +### Step 1: Initialize baselines +```bash +cd ~/.openclaw/workspace +python3 skills/soul-guardian/scripts/soul_guardian.py init --actor setup --note "initial baseline" +``` -### Option A: Bundled with ClawSec Suite (Recommended) +### Step 2: Enable monitoring +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py enable-monitoring +``` +This shows you how to add monitoring to your workflow. -**If you've installed clawsec-suite, you may already have this!** +### Step 3: Add to HEARTBEAT.md (Recommended) -Soul-guardian is bundled alongside ClawSec Suite to provide file integrity and drift detection capabilities. When you install the suite, if you don't already have soul-guardian installed, it will be deployed from the bundled copy. +Add this to your HEARTBEAT.md for automatic monitoring: -**Advantages:** -- Convenient - no separate download needed -- Standard location - installed to `~/.openclaw/skills/soul-guardian/` -- Preserved - if you already have soul-guardian installed, it won't be overwritten -- Single verification - integrity checked as part of suite package +```markdown +## Soul Guardian Check +- Run `python3 skills/soul-guardian/scripts/soul_guardian.py check --actor heartbeat --output-format alert` +- If any output is produced, relay it to the user immediately as a security alert +``` -### Option B: Standalone Installation (This Page) - -Install soul-guardian independently without the full suite. - -**When to use standalone:** -- You only need file integrity monitoring (not other suite components) -- You want to install before installing the suite -- You prefer explicit control over soul-guardian installation - -**Advantages:** -- Lighter weight installation -- Independent from suite -- Direct control over installation process - -Continue below for standalone installation instructions. +That's it! Soul Guardian will now: +- āœ… Detect unauthorized changes to protected files +- āœ… Auto-restore SOUL.md and AGENTS.md to approved baseline +- āœ… Alert you when drift is detected and handled --- ## What it protects (default policy) -- **Auto-restore + alert:** `SOUL.md`, `AGENTS.md` -- **Alert-only:** `USER.md`, `TOOLS.md`, `IDENTITY.md`, `HEARTBEAT.md`, `MEMORY.md` -- **Ignored by default:** `memory/*.md` (daily notes) - -Policy is stored in the guardian state directory as `policy.json`. - -## Quick start (first run) - -Recommended: onboard an **external** state dir, then initialize baselines there. - -```bash -python3 skills/soul-guardian/scripts/onboard_state_dir.py --agent-id -python3 skills/soul-guardian/scripts/soul_guardian.py --state-dir ~/.clawdbot/soul-guardian/ init --actor sam --note "first baseline" -``` - -(Full step-by-step + scheduling options are in `README.md`.) +| File | Mode | Action on drift | +|------|------|-----------------| +| SOUL.md | restore | Auto-restore + alert | +| AGENTS.md | restore | Auto-restore + alert | +| USER.md | alert | Alert only | +| TOOLS.md | alert | Alert only | +| IDENTITY.md | alert | Alert only | +| HEARTBEAT.md | alert | Alert only | +| MEMORY.md | alert | Alert only | +| memory/*.md | ignore | Ignored | ## Commands -Run from the agent workspace root: +### Check for drift (with alert output) +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py check --output-format alert +``` +- Silent if no drift +- Outputs human-readable alert if drift detected +- Perfect for heartbeat integration +### Watch mode (continuous monitoring) +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py watch --interval 30 +``` +Runs continuously, checking every 30 seconds. + +### Approve intentional changes +```bash +python3 skills/soul-guardian/scripts/soul_guardian.py approve --file SOUL.md --actor user --note "intentional update" +``` + +### View status ```bash python3 skills/soul-guardian/scripts/soul_guardian.py status -python3 skills/soul-guardian/scripts/soul_guardian.py check -python3 skills/soul-guardian/scripts/soul_guardian.py check --no-restore -python3 skills/soul-guardian/scripts/soul_guardian.py approve --file SOUL.md -python3 skills/soul-guardian/scripts/soul_guardian.py restore --file SOUL.md +``` + +### Verify audit log integrity +```bash python3 skills/soul-guardian/scripts/soul_guardian.py verify-audit ``` -### State directory +--- -- Default (backward compatible): `memory/soul-guardian/` -- Recommended external override: +## Alert Format -```bash -python3 skills/soul-guardian/scripts/soul_guardian.py --state-dir ~/.clawdbot/soul-guardian/ check +When drift is detected, the `--output-format alert` produces output like: + +``` +================================================== +🚨 SOUL GUARDIAN SECURITY ALERT +================================================== + +šŸ“„ FILE: SOUL.md + Mode: restore + Status: āœ… RESTORED to approved baseline + Expected hash: abc123def456... + Found hash: 789xyz000111... + Diff saved: /path/to/patches/drift.patch + +================================================== +Review changes and investigate the source of drift. +If intentional, run: soul_guardian.py approve --file +================================================== ``` -## Cron pattern +This output is designed to be relayed directly to the user in TUI/chat. -Keep the existing gateway cron pattern: run `check` every N minutes and notify only when drift is detected. +--- -For onboarding/migration to an external state directory, see `README.md` and: +## Security Model + +**What it does:** +- Detects filesystem drift vs approved baseline (sha256) +- Produces unified diffs for review +- Maintains tamper-evident audit log with hash chaining +- Refuses to operate on symlinks +- Uses atomic writes for restores + +**What it doesn't do:** +- Cannot prove WHO made a change (actor is best-effort metadata) +- Cannot protect if attacker controls both workspace AND state directory +- Is not a substitute for backups + +**Recommendation:** Store state directory outside workspace for better resilience. + +--- + +## Demo + +Run the full demo flow to see soul-guardian in action: ```bash -python3 skills/soul-guardian/scripts/onboard_state_dir.py +bash skills/soul-guardian/scripts/demo.sh ``` + +This will: +1. Verify clean state (silent check) +2. Inject malicious content into SOUL.md +3. Run heartbeat check (produces alert) +4. Show SOUL.md was restored + +--- + +## Troubleshooting + +**"Not initialized" error:** +Run `init` first to set up baselines. + +**Drift keeps happening:** +Check what's modifying your files. Review the audit log and patches. + +**Want to approve a change:** +Run `approve --file ` after reviewing the change. diff --git a/skills/soul-guardian/scripts/install_launchd_plist.py b/skills/soul-guardian/scripts/install_launchd_plist.py old mode 100755 new mode 100644 diff --git a/skills/soul-guardian/scripts/soul_guardian.py b/skills/soul-guardian/scripts/soul_guardian.py index 92e17b2..efb3c04 100644 --- a/skills/soul-guardian/scripts/soul_guardian.py +++ b/skills/soul-guardian/scripts/soul_guardian.py @@ -546,7 +546,52 @@ def restore_one(state: GuardianState, relp: str, info: dict[str, Any]) -> dict[s return {"quarantinePath": str(quarantine_path), **info} -def check_cmd(state: GuardianState, actor: str, note: str, *, no_restore: bool = False) -> int: +def format_alert_human(drifted: list[dict[str, Any]]) -> str: + """Format drift results as human-readable alert for TUI notification.""" + lines = [] + lines.append("") + lines.append("=" * 50) + lines.append("🚨 SOUL GUARDIAN SECURITY ALERT") + lines.append("=" * 50) + lines.append("") + + for d in drifted: + path = d.get("path", "unknown") + mode = d.get("mode", "unknown") + restored = d.get("restored", False) + error = d.get("error") + + if error: + lines.append(f"āš ļø ERROR: {path}") + lines.append(f" {error}") + else: + lines.append(f"šŸ“„ FILE: {path}") + lines.append(f" Mode: {mode}") + if restored: + lines.append(f" Status: āœ… RESTORED to approved baseline") + if d.get("quarantinePath"): + lines.append(f" Quarantined: {d.get('quarantinePath')}") + else: + lines.append(f" Status: āš ļø DRIFT DETECTED (not auto-restored)") + + if d.get("approvedSha"): + lines.append(f" Expected hash: {d.get('approvedSha')[:16]}...") + if d.get("currentSha"): + lines.append(f" Found hash: {d.get('currentSha')[:16]}...") + if d.get("patchPath"): + lines.append(f" Diff saved: {d.get('patchPath')}") + lines.append("") + + lines.append("=" * 50) + lines.append("Review changes and investigate the source of drift.") + lines.append("If intentional, run: soul_guardian.py approve --file ") + lines.append("=" * 50) + lines.append("") + + return "\n".join(lines) + + +def check_cmd(state: GuardianState, actor: str, note: str, *, no_restore: bool = False, output_format: str = "json") -> int: state.ensure_dirs() policy = load_policy(state) baselines = load_baselines(state) @@ -611,30 +656,112 @@ def check_cmd(state: GuardianState, actor: str, note: str, *, no_restore: bool = drifted.append(rec) if not drifted: + # Silent on OK for alert format + if output_format != "alert": + pass # Could print "OK" here if desired return 0 - # Single-line summary suitable for cron parsing. - # Keep it small; details are in audit + patch paths. - summary = { - "event": "SOUL_GUARDIAN_DRIFT", - "count": len(drifted), - "files": [ - { - "path": d["path"], - "mode": d.get("mode"), - "restored": d.get("restored"), - "patch": d.get("patchPath"), - "error": d.get("error"), - } - for d in drifted - ], - } - print("SOUL_GUARDIAN_DRIFT " + json.dumps(summary, ensure_ascii=False)) + # Output based on format + if output_format == "alert": + # Human-readable alert suitable for direct relay to user + print(format_alert_human(drifted)) + else: + # JSON format (default) - single-line summary suitable for cron parsing + summary = { + "event": "SOUL_GUARDIAN_DRIFT", + "count": len(drifted), + "files": [ + { + "path": d["path"], + "mode": d.get("mode"), + "restored": d.get("restored"), + "patch": d.get("patchPath"), + "error": d.get("error"), + } + for d in drifted + ], + } + print("SOUL_GUARDIAN_DRIFT " + json.dumps(summary, ensure_ascii=False)) # Drift occurred (even if restored). return 2 +def watch_cmd(state: GuardianState, actor: str, note: str, *, interval: int = 30) -> int: + """Continuous watch mode - checks periodically and alerts on drift.""" + import time + + print(f"šŸ‘» Soul Guardian watching... (interval: {interval}s)") + print("Press Ctrl+C to stop.\n") + + try: + while True: + result = check_cmd(state, actor, note, output_format="alert") + if result == 0: + # No drift - stay silent + pass + # Sleep before next check + time.sleep(interval) + except KeyboardInterrupt: + print("\nšŸ‘» Soul Guardian stopped.") + return 0 + + +def enable_monitoring_cmd(state: GuardianState) -> None: + """Output instructions and HEARTBEAT.md snippet for easy monitoring setup.""" + + heartbeat_snippet = """ +## Soul Guardian Check +- Run `cd {workspace} && python3 skills/soul-guardian/scripts/soul_guardian.py check --actor heartbeat --output-format alert` +- If any output is produced, relay it to the user immediately as a security alert +- This checks for unauthorized changes to SOUL.md, AGENTS.md, and other protected files +""".format(workspace=WORKSPACE_ROOT) + + print(""" +╔══════════════════════════════════════════════════════════════╗ +ā•‘ SOUL GUARDIAN - ENABLE MONITORING ā•‘ +ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• + +To enable automatic drift detection and alerting, you have two options: + +──────────────────────────────────────────────────────────────── +OPTION 1: Heartbeat Integration (Recommended) +──────────────────────────────────────────────────────────────── + +Add the following to your HEARTBEAT.md file: +""") + print(heartbeat_snippet) + print(""" +──────────────────────────────────────────────────────────────── +OPTION 2: Watch Mode (Foreground) +──────────────────────────────────────────────────────────────── + +Run this in a terminal to continuously monitor: + + python3 skills/soul-guardian/scripts/soul_guardian.py watch --interval 30 + +──────────────────────────────────────────────────────────────── +OPTION 3: Manual Check +──────────────────────────────────────────────────────────────── + +Run a one-time check with human-readable output: + + python3 skills/soul-guardian/scripts/soul_guardian.py check --output-format alert + +──────────────────────────────────────────────────────────────── + +The guardian will: +āœ“ Detect unauthorized changes to protected files +āœ“ Auto-restore SOUL.md and AGENTS.md to approved baselines +āœ“ Alert you immediately when drift is detected +āœ“ Save diffs and quarantine modified files for review + +""") + print(f"State directory: {state.state_dir}") + print(f"Workspace: {WORKSPACE_ROOT}") + print() + + def approve_cmd(state: GuardianState, actor: str, note: str, *, files: list[str] | None, all_files: bool = False) -> None: state.ensure_dirs() policy = load_policy(state) @@ -796,7 +923,10 @@ def verify_audit_cmd(state: GuardianState) -> None: def parse_args(argv: list[str]) -> argparse.Namespace: - p = argparse.ArgumentParser() + p = argparse.ArgumentParser( + description="Soul Guardian - Workspace file integrity guard with alerting support.", + epilog="For easy setup, run: soul_guardian.py enable-monitoring" + ) p.add_argument( "--state-dir", default=str(DEFAULT_STATE_DIR), @@ -818,6 +948,8 @@ def parse_args(argv: list[str]) -> argparse.Namespace: sp_check = sub.add_parser("check", help="Check for drift; restore restore-mode by default.") add_common(sp_check) sp_check.add_argument("--no-restore", action="store_true", help="Never restore during check (alert-only run).") + sp_check.add_argument("--output-format", choices=["json", "alert"], default="json", + help="Output format: json (machine-readable) or alert (human-readable for TUI).") sp_approve = sub.add_parser("approve", help="Approve current contents as baselines.") add_common(sp_approve) @@ -830,6 +962,13 @@ def parse_args(argv: list[str]) -> argparse.Namespace: sp_restore.add_argument("--all", action="store_true", help="Restore all restore-mode targets.") sub.add_parser("verify-audit", help="Verify audit log hash chain.") + + # New commands for easier monitoring setup + sp_watch = sub.add_parser("watch", help="Continuous watch mode - monitors and alerts on drift.") + add_common(sp_watch) + sp_watch.add_argument("--interval", type=int, default=30, help="Check interval in seconds (default: 30).") + + sub.add_parser("enable-monitoring", help="Show instructions for enabling automatic monitoring and alerts.") return p.parse_args(argv) @@ -846,7 +985,11 @@ def main(argv: list[str]) -> int: status_cmd(state) return 0 if args.cmd == "check": - return check_cmd(state, args.actor, args.note, no_restore=bool(getattr(args, "no_restore", False))) + return check_cmd( + state, args.actor, args.note, + no_restore=bool(getattr(args, "no_restore", False)), + output_format=getattr(args, "output_format", "json") + ) if args.cmd == "approve": approve_cmd(state, args.actor, args.note, files=getattr(args, "files", None), all_files=bool(getattr(args, "all", False))) return 0 @@ -856,6 +999,11 @@ def main(argv: list[str]) -> int: if args.cmd == "verify-audit": verify_audit_cmd(state) return 0 + if args.cmd == "watch": + return watch_cmd(state, args.actor, args.note, interval=getattr(args, "interval", 30)) + if args.cmd == "enable-monitoring": + enable_monitoring_cmd(state) + return 0 raise RuntimeError(f"Unknown cmd: {args.cmd}") diff --git a/skills/soul-guardian/skill.json b/skills/soul-guardian/skill.json index d7c35b1..bcdba79 100644 --- a/skills/soul-guardian/skill.json +++ b/skills/soul-guardian/skill.json @@ -1,6 +1,6 @@ { "name": "soul-guardian", - "version": "0.0.1", + "version": "0.0.2", "description": "Drift detection and baseline integrity guard for agent workspace prompt files. Auto-restore critical files with tamper-evident audit logging.", "author": "prompt-security", "license": "MIT",