mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
chore(soul-guardian): bump version to 0.0.2
This commit is contained in:
+122
-59
@@ -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 <agentId>
|
||||
python3 skills/soul-guardian/scripts/soul_guardian.py --state-dir ~/.clawdbot/soul-guardian/<agentId> 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/<agentId> 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 <path>
|
||||
==================================================
|
||||
```
|
||||
|
||||
## 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 <path>` after reviewing the change.
|
||||
|
||||
Executable → Regular
@@ -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 <path>")
|
||||
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}")
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user