Skill: jcools1977/an2b-warden-governance (version 1.0.0)

--- name: warden-governance version: 1.0.0 author: AN2B Technologies license: MIT category: governance clawhub: an2b/warden-governance hooks: - before_action - after_action - on_error config: SENTINEL_API_KEY: required: false description: Sentinel_OS API key for enterprise governance ENGRAMPORT_API_KEY: required: false description: EngramPort API key for enterprise memory WARDEN_FAIL_OPEN: required: false default: "false" description: Allow actions when governance engine fails WARDEN_AGENT_ID: required: false default: openclaw-agent description: Bot identifier for audit trail WARDEN_POLICY_FILE: required: false description: Path to custom YAML policy file WARDEN_POLICY_PACKS: required: false description: Comma-separated built-in policy pack names install: openclaw skill install an2b/warden-governance --- # War/Den Governance Skill > **ClawHub Package:** `an2b/warden-governance` > **Version:** 1.0.0 > **Category:** Governance & Security > **License:** MIT --- ## What This Skill Does Every action your OpenClaw bot tries to take is evaluated by War/Den before it executes. ``` Your Bot -> War/Den check -> ALLOW -> action executes -> DENY -> action blocked + logged -> REVIEW -> waits for your approval ``` No more deleted emails. No more data exfiltration. No more ungoverned agents. **Community mode works with zero external dependencies.** No API keys. No cloud. Just YAML policies, a local SQLite audit log, and a hash chain you can verify. --- ## Install ### From ClawHub (recommended) ```bash openclaw skill install an2b/warden-governance ``` ### From pip ```bash pip install warden-governance-skill ``` Both methods install to: `~/.openclaw/skills/warden-governance/` On successful install you'll see: ``` 🦞 War/Den governance active. Your OpenClaw bot is now governed. ``` ### Add to your OpenClaw config ```yaml skills: - name: warden-governance config: SENTINEL_API_KEY: "" # optional -- leave blank for community mode ENGRAMPORT_API_KEY: "" # optional -- leave blank for local memory WARDEN_FAIL_OPEN: "false" # block on governance failure (default) ``` ### Restart your bot ```bash openclaw restart ``` That's it. Your bot is now governed. --- ## How It Works ### Hooks This skill registers three OpenClaw hooks: | Hook | Purpose | |------|---------| | `before_action` | Evaluate every action against policy before execution | | `after_action` | Write action result to governed memory | | `on_error` | Log errors to tamper-evident audit trail | ### Action Bridge All 15 OpenClaw action types are mapped to War/Den governance types: | OpenClaw Action | War/Den Type | Default Protection | |-----------------|--------------|-------------------| | `email.send` | `message.send` | Monitored | | `email.delete` | `data.write` | **Requires human review** | | `email.read` | `data.read` | Monitored | | `file.write` | `data.write` | Monitored | | `file.delete` | `data.write` | **Requires human review** | | `file.read` | `data.read` | Monitored | | `browser.navigate` | `api.call` | Monitored | | `browser.click` | `api.call` | Monitored | | `shell.execute` | `code.execute` | **Blocked in production** | | `api.call` | `api.call` | Monitored | | `calendar.create` | `data.write` | Monitored | | `calendar.delete` | `data.write` | **Requires human review** | | `message.send` | `message.send` | Monitored | | `code.execute` | `code.execute` | **Blocked in production** | | `payment.create` | `api.call` | **Requires human review** | ### Policy Engine Policies are YAML files evaluated in priority order: ```yaml policies: - name: protect-email-delete match: action.type: data.write action.data.openclaw_original: email.delete decision: review mode: enforce priority: 1 active: true reason: "Email deletion requires human review." ``` **Evaluation rules:** 1. Filter to active policies only 2. Sort by priority ascending (lower number = higher priority) 3. First match wins 4. `mode: monitor` -- log but return ALLOW 5. `mode: enforce` -- return the matched decision 6. No match -- default ALLOW ### Pre-built Policy Packs Load governance instantly with built-in packs: | Pack | What It Does | |------|-------------| | `basic_safety` | Blocks code execution in prod, monitors writes and API calls | | `phi_guard` | Denies PHI access in dev, requires review for memory export | | `payments_guard` | Denies payment actions in dev, requires review in prod | ### Audit Trail Every governance decision is written to a tamper-evident SHA-256 hash chain: ``` Event N: hash = SHA256(prev_hash + agent_id + action_type + decision + timestamp) Event N+1: prev_hash = Event N hash ``` Verify the chain at any time: ```python valid, bad_event_id = audit_log.verify_chain() ``` ### Decision Cache ALLOW decisions are cached for 5 minutes (configurable). DENY and REVIEW are **never** cached -- they always hit the governance engine fresh. --- ## Community vs Enterprise | Feature | Community (Free) | Enterprise | |---------|-----------------|------------| | Policy enforcement | Local YAML | Sentinel_OS cloud | | Audit trail | Local SQLite + hash chain | Cloud + signed PDF | | Memory storage | Local SQLite | EngramPort cloud (MandelDB) | | Memory search | Text search (LIKE) | Vector search (3072-dim) | | Synthesis | Basic recall | Eidetic AI synthesis | | Cross-bot memory | -- | Orchestra multi-agent | | Multi-namespace | 3 max | Unlimited | | Compliance export | -- | SOC2/HIPAA PDF | | Cryptographic provenance | Local hash chain | AEGIS (SHA-256 + RSA) | | Dependencies | **Zero** | `sentinel-client`, `engramport-langchain` | ### Mode Matrix | `SENTINEL_API_KEY` | `ENGRAMPORT_API_KEY` | Mode | |--------------------|----------------------|------| | -- | -- | Full Community | | Set | -- | Governed Community | | -- | Set | Memory Enterprise | | Set | Set | Full Enterprise | All four modes work with zero code changes. Just environment variables. --- ## Enterprise Upgrade Path ### Sentinel_OS (Governance) Set `SENTINEL_API_KEY` to upgrade governance from local YAML to Sentinel_OS cloud: - Real-time policy evaluation via `/api/v1/check` - Pre-flight checks via `/api/v1/check` (read-only, no side effects) - Action logging via `/api/v1/ingest` with hash chain integrity - Run management, alerting, and AI-powered insights - Python and Node.js SDKs - Rate limiting: 2000 checks/min, 1000 ingests/min per API key Get your key at [getsentinelos.com](https://getsentinelos.com) ### EngramPort (Memory via MandelDB) Set `ENGRAMPORT_API_KEY` to upgrade memory from local SQLite to EngramPort cloud: - **5 endpoints:** `/register`, `/remember`, `/recall`, `/reflect`, `/stats` - 3072-dimensional OpenAI embeddings via Pinecone - AEGIS cryptographic provenance (SHA-256 + RSA signature per memory) - Namespace-isolated storage (`bot:{slug}:{uid}`) - Eidetic cross-memory pattern synthesis via GPT-4o-mini - Multi-agent orchestration with `EngramPortOrchestra` - Background synthesis with `DreamState` - LangChain drop-in integration API keys use format `ek_bot_*` with SHA-256 hashed storage. Get your key at [engram.eideticlab.com](https://engram.eideticlab.com) --- ## Configuration | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `SENTINEL_API_KEY` | No | `""` | Sentinel_OS key. Blank = community governance | | `ENGRAMPORT_API_KEY` | No | `""` | EngramPort key. Blank = local memory | | `WARDEN_FAIL_OPEN` | No | `false` | Allow on governance failure | | `WARDEN_AGENT_ID` | No | `openclaw-agent` | Bot identifier | | `WARDEN_POLICY_FILE` | No | built-in | Path to custom YAML policy file | | `WARDEN_POLICY_PACKS` | No | `""` | Comma-separated pack names | | `WARDEN_MEMORY_DB` | No | `~/.warden/memory.db` | Local memory path | | `WARDEN_AUDIT_DB` | No | `~/.warden/audit.db` | Local audit log path | | `WARDEN_CACHE_TTL` | No | `300` | ALLOW cache TTL in seconds | ### Fail-Open Behavior | `WARDEN_FAIL_OPEN` | War/Den reachable | War/Den unreachable | |---------------------|-------------------|---------------------| | `false` (default) | Normal governance | Action **BLOCKED** | | `true` | Normal governance | Action **ALLOWED** + warning | Default is `false` because a governance failure should never silently allow dangerous actions. --- ## Test Proof This skill ships with a comprehensive test suite. Run it: ```bash python -m pytest tests/ -v ``` Key test: **The Meta inbox test** simulates the exact incident where an OpenClaw agent deleted 200 emails. With War/Den, all 200 are blocked: ```python def test_meta_researcher_inbox_protection(self, tmp_path): """Simulate the exact Meta inbox incident. All 200 emails blocked.""" skill = _make_skill(tmp_path, WARDEN_POLICY_FILE=policy_path) blocked = 0 for i in range(200): result = skill.before_action( {"type": "email.delete", "data": {"email_id": f"msg_{i}"}}, {"agent_id": "meta-researcher-bot", "env": "prod"}, ) if not result["proceed"]: blocked += 1 assert blocked == 200 ``` --- ## Skill Files ``` warden-governance-skill/ ├── SKILL.md # This file (ClawHub primary) ├── clawhub.json # ClawHub registry metadata ├── README.md # Full documentation ├── pyproject.toml # Python package config ├── policies/ │ ├── openclaw_default.yaml # Default governance policies │ └── policy_packs.py # Pre-built policy packs ├── warden_governance/ │ ├── __init__.py │ ├── skill.py # Main skill class (hooks) │ ├── action_bridge.py # OpenClaw <-> War/Den translation │ ├── policy_engine.py # Community policy engine │ ├── audit_log.py # SHA-256 hash chain audit │ ├── memory_client.py # Governed memory operations │ ├── local_store.py # Local SQLite memory │ ├── sentinel_client.py # Enterprise Sentinel_OS client │ ├── engramport_client.py # Enterprise EngramPort client │ ├── upgrade_manager.py # Mode detection + banner │ ├── health_check.py # Enterprise health validation │ └── settings.py # Configuration └── tests/ ├── __init__.py ├── test_skill.py # Skill + Meta inbox tests ├── test_policy_engine.py # Policy engine tests ├── test_audit_log.py # Audit trail tests ├── test_action_bridge.py # Action bridge tests ├── test_memory.py # Memory client tests └── test_enterprise.py # Enterprise upgrade tests ``` --- Built on [Sentinel_OS](https://getsentinelos.com) and [EngramPort](https://engram.eideticlab.com) by [AN2B Technologies](https://an2b.com) *The lobster protects the inbox.*

path: .gitignore
content: __pycache__/
*.pyc
*.pyo
.pytest_cache/
*.egg-info/
dist/
build/
.warden/
*.db


path: clawhub.json
content: {
  "name": "warden-governance",
  "display_name": "War/Den Governance",
  "version": "1.0.0",
  "description": "Govern your OpenClaw bot with War/Den. Every action evaluated before executing. Community mode with zero dependencies. Enterprise upgrade to Sentinel_OS + EngramPort.",
  "author": "AN2B Technologies",
  "author_url": "https://an2b.com",
  "skill_url": "https://warden.an2b.com/openclaw",
  "repository": "https://github.com/an2b/warden-governance-skill",
  "license": "MIT",
  "pricing": "free",
  "support_url": "https://warden.an2b.com",
  "install_path": "~/.openclaw/skills/warden-governance/",
  "memory_path": "~/.openclaw/memory/",
  "hooks": [
    "before_action",
    "after_action",
    "on_error"
  ],
  "config": {
    "SENTINEL_API_KEY": {
      "required": false,
      "description": "Sentinel_OS API key for enterprise governance. Leave blank for community mode.",
      "default": ""
    },
    "ENGRAMPORT_API_KEY": {
      "required": false,
      "description": "EngramPort API key for cloud memory (MandelDB backend). Leave blank for local memory.",
      "default": ""
    },
    "WARDEN_FAIL_OPEN": {
      "required": false,
      "description": "If true, allow actions when War/Den is unreachable. Default false.",
      "default": "false"
    },
    "WARDEN_AGENT_ID": {
      "required": false,
      "description": "Unique identifier for this bot. Default: openclaw-agent.",
      "default": "openclaw-agent"
    },
    "WARDEN_POLICY_FILE": {
      "required": false,
      "description": "Path to custom YAML policy file. Uses built-in defaults if blank.",
      "default": ""
    },
    "WARDEN_POLICY_PACKS": {
      "required": false,
      "description": "Comma-separated policy pack names: basic_safety, phi_guard, payments_guard.",
      "default": ""
    }
  },
  "clawhub": {
    "registry": "clawhub.com",
    "package": "an2b/warden-governance",
    "category": "governance",
    "featured": true,
    "install_command": "openclaw skill install an2b/warden-governance",
    "verified": true
  },
  "tags": [
    "governance",
    "security",
    "audit",
    "compliance",
    "memory",
    "policy-engine",
    "sentinel-os",
    "engramport",
    "openclaw"
  ],
  "community": {
    "zero_dependencies": true,
    "local_policy_engine": true,
    "local_audit_log": true,
    "local_memory": true,
    "hash_chain_integrity": true
  },
  "enterprise": {
    "sentinel_os": {
      "governance_api": "https://getsentinelos.com",
      "endpoints": ["/api/v1/check", "/api/v1/ingest", "/api/v1/runs", "/api/v1/integrity"],
      "sdks": ["python", "node"]
    },
    "engramport": {
      "memory_api": "https://mandeldb.com/api/v1/portal",
      "endpoints": ["/register", "/remember", "/recall", "/reflect", "/stats"],
      "features": ["vector-search", "aegis-provenance", "eidetic-synthesis", "orchestra"]
    }
  }
}


path: pyproject.toml
content: [project]
name = "warden-governance-skill"
version = "1.0.0"
description = "Govern your OpenClaw bot with War/Den. Every action evaluated before executing. Community mode with zero dependencies. Enterprise upgrade to Sentinel_OS + EngramPort."
authors = [
    {name = "AN2B Technologies", email = "[REDACTED_SECRET]"}
]
keywords = [
    "openclaw",
    "ai-agent",
    "governance",
    "security",
    "clawhub",
    "sentinel-os",
    "engramport",
    "policy-engine",
    "audit-trail",
    "ai-safety"
]
classifiers = [
    "Development Status :: 4 - Beta",
    "Intended Audience :: Developers",
    "Topic :: Security",
    "Topic :: Scientific/Engineering :: Artificial Intelligence",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "License :: OSI Approved :: MIT License",
]
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
    "pyyaml>=6.0",
]

[project.optional-dependencies]
enterprise = [
    "httpx>=0.24",
]
dev = [
    "pytest>=8.0",
    "pyyaml>=6.0",
]

[project.urls]
Homepage = "https://warden.an2b.com"
Documentation = "https://warden.an2b.com/docs"
Repository = "https://github.com/an2b/warden-governance-skill"
ClawHub = "https://clawhub.com/an2b/warden-governance"
SentinelOS = "https://getsentinelos.com"
EngramPort = "https://engram.eideticlab.com"

[project.entry-points."openclaw.skills"]
warden-governance = "warden_governance.skill:WardenGovernanceSkill"

[tool.setuptools.packages.find]
include = ["warden_governance*"]

[tool.setuptools.package-data]
"*" = ["*.yaml", "*.json", "*.md"]

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"

[tool.pytest.ini_options]
testpaths = ["tests"]


path: README.md
content: # War/Den Governance Skill for OpenClaw

### Govern your OpenClaw bot in 5 minutes. Zero external dependencies.

[![ClawHub](https://img.shields.io/badge/ClawHub-an2b%2Fwarden--governance-dc2626)](https://clawhub.com/an2b/warden-governance)
[![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](https://github.com/jcools1977/warden-governance-skill/actions)

> **Find this skill on ClawHub:** [clawhub.com/an2b/warden-governance](https://clawhub.com/an2b/warden-governance)

---

## The Problem

On February 14, 2026, a Meta AI security researcher asked her OpenClaw agent to triage her inbox. It deleted 200 emails. She had explicitly told it to confirm before acting.

OpenClaw has no governance layer. War/Den fixes that.

## What This Skill Does

Every action your OpenClaw bot tries to take is evaluated by War/Den before it executes:

```
Your Bot -> War/Den check -> ALLOW  -> action executes
                          -> DENY   -> action blocked + logged
                          -> REVIEW -> waits for your approval
```

No more deleted emails. No more data exfiltration. No more ungoverned agents.

## The Meta Inbox Test

This isn't a theoretical concern. We ship a test that simulates the exact incident:

```python
def test_meta_researcher_inbox_protection(self, tmp_path):
    """Simulate the exact Meta inbox incident. All 200 emails blocked."""
    skill = _make_skill(tmp_path, WARDEN_POLICY_FILE=policy_path)

    blocked_count = 0
    for i in range(200):
        result = skill.before_action(
            {
                "type": "email.delete",
                "data": {"email_id": f"msg_{i}", "subject": f"Meeting notes #{i}"},
            },
            {"agent_id": "meta-researcher-bot", "env": "prod", "user": "[REDACTED_SECRET]"},
        )
        if not result["proceed"]:
            blocked_count += 1

    # Every single email.delete must be blocked
    assert blocked_count == 200
```

**200 emails. 200 blocked. Every time.** The policy engine returns REVIEW for `email.delete`, which means the agent cannot proceed without human approval.

## Install

### From ClawHub (recommended)

```bash
openclaw skill install an2b/warden-governance
```

### From pip

```bash
pip install warden-governance-skill
```

Both install to: `~/.openclaw/skills/warden-governance/`

### Add to your OpenClaw config

```yaml
skills:
  - name: warden-governance
    config:
      SENTINEL_API_KEY: ""       # optional -- leave blank for community mode
      ENGRAMPORT_API_KEY: ""     # optional -- leave blank for local memory
      WARDEN_FAIL_OPEN: "false"  # block on governance failure (default)
```

### Restart

```bash
openclaw restart
```

You'll see:

```
🦞 War/Den governance active.
   Your OpenClaw bot is now governed.

   Governance: Local
   Memory:     SQLite
   Synthesis:  Basic
   Mode:       Full Community
   Upgrade:    getsentinelos.com
```

## 30 Second Proof

Run this single command to prove governance works:

```bash
python -m pytest tests/test_skill.py::TestMetaResearcherInboxProtection -v
```

Expected output:

```
test_meta_researcher_inbox_protection PASSED
200/200 email deletes blocked.
============================== 1 passed ==============================
```

## What Gets Protected

All 15 OpenClaw action types are mapped to War/Den governance types:

| OpenClaw Action | Default Protection |
|-----------------|-------------------|
| `email.delete` | **Requires human review** |
| `shell.execute` | **Blocked in production** |
| `file.delete` | **Requires human review** |
| `payment.create` | **Requires human review** |
| `code.execute` | **Blocked in production** |
| `calendar.delete` | **Requires human review** |
| `api.call` | Monitored and logged |
| `email.send` | Monitored and logged |
| `browser.navigate` | Monitored and logged |
| `email.read` | Allowed |
| `file.read` | Allowed |
| `file.write` | Monitored and logged |
| `browser.click` | Monitored and logged |
| `message.send` | Monitored and logged |
| `calendar.create` | Monitored and logged |

## Policy Engine

Policies are YAML files with priority-based evaluation:

```yaml
policies:
  - name: protect-email-delete
    match:
      action.type: data.write
      action.data.openclaw_original: email.delete
    decision: review
    mode: enforce
    priority: 1
    active: true
    reason: "Email deletion requires human review."
```

**Evaluation:** Active policies sorted by priority (lower = first). First match wins. Monitor mode logs but allows. Enforce mode blocks or reviews.

### Pre-built Policy Packs

| Pack | What It Does |
|------|-------------|
| `basic_safety` | Blocks code execution in prod, monitors writes and API calls |
| `phi_guard` | Denies PHI access in dev, requires review for memory export |
| `payments_guard` | Denies payment actions in dev, requires review in prod |

Load via config: `WARDEN_POLICY_PACKS: "basic_safety,phi_guard"`

## Audit Trail

Every governance decision is written to a tamper-evident SHA-256 hash chain in local SQLite:

```
Event N:  hash = SHA256(prev_hash + agent_id + action_type + decision + timestamp)
```

Verify at any time:

```python
valid, bad_event_id = skill.sentinel.audit_log.verify_chain()
assert valid is True
```

## Decision Cache

ALLOW decisions are cached for 5 minutes. DENY and REVIEW are **never cached** -- they always hit the policy engine fresh.

Performance proof: 100 identical `api.call` actions result in 1 policy evaluation + 99 cache hits.

## Community vs Enterprise

**Community (free, zero dependencies):**
- Local YAML policy enforcement
- Local SQLite audit log with SHA-256 hash chain
- Local SQLite memory with text search
- Basic synthesis
- Zero external dependencies -- just `pyyaml`

**Enterprise (API keys set):**
- [Sentinel_OS](https://getsentinelos.com) cloud governance with real-time policy evaluation
- [EngramPort](https://engram.eideticlab.com) cloud memory with 3072-dim vector search (MandelDB backend)
- AEGIS cryptographic provenance (SHA-256 + RSA per memory)
- Eidetic AI synthesis across memories
- Multi-agent orchestration via EngramPortOrchestra
- Signed PDF compliance exports (SOC2/HIPAA)

### Mode Matrix

| `SENTINEL_API_KEY` | `ENGRAMPORT_API_KEY` | Mode |
|--------------------|----------------------|------|
| -- | -- | Full Community |
| Set | -- | Governed Community |
| -- | Set | Memory Enterprise |
| Set | Set | Full Enterprise |

All four modes work with **zero code changes**. Just set environment variables.

## Enterprise Upgrade Path

### Sentinel_OS (Governance)

Set `SENTINEL_API_KEY` to upgrade from local YAML to cloud governance:

- Real-time policy evaluation via `/api/v1/check` (2000 req/min)
- Action logging via `/api/v1/ingest` with hash chain integrity
- Run management, alerting, AI-powered insights
- Python and Node.js SDKs
- Get your key: [getsentinelos.com](https://getsentinelos.com)

### EngramPort (Memory via MandelDB)

Set `ENGRAMPORT_API_KEY` to upgrade from local SQLite to cloud memory:

- 5 endpoints: `/register`, `/remember`, `/recall`, `/reflect`, `/stats`
- 3072-dim OpenAI embeddings via Pinecone for semantic search
- AEGIS cryptographic provenance per memory
- Namespace-isolated storage (`bot:{slug}:{uid}`)
- Multi-agent orchestration with `EngramPortOrchestra`
- Get your key: [engram.eideticlab.com](https://engram.eideticlab.com)

## Configuration

| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `SENTINEL_API_KEY` | No | `""` | Sentinel_OS key. Blank = community governance |
| `ENGRAMPORT_API_KEY` | No | `""` | EngramPort key. Blank = local memory |
| `WARDEN_FAIL_OPEN` | No | `false` | Allow on governance failure |
| `WARDEN_AGENT_ID` | No | `openclaw-agent` | Bot identifier |
| `WARDEN_POLICY_FILE` | No | built-in | Path to custom YAML policy file |
| `WARDEN_POLICY_PACKS` | No | `""` | Comma-separated pack names |

### Fail-Open Behavior

| `WARDEN_FAIL_OPEN` | War/Den reachable | War/Den unreachable |
|---------------------|-------------------|---------------------|
| `false` (default) | Normal governance | Action **BLOCKED** |
| `true` | Normal governance | Action **ALLOWED** + warning |

Default is `false` because a governance failure should never silently allow dangerous actions.

## Running Tests

```bash
pip install pytest pyyaml
python -m pytest tests/ -v
```

The test suite validates:
- All 15 OpenClaw action type mappings
- Policy engine evaluation (priority, dot-path matching, mode handling)
- SHA-256 hash chain integrity and tamper detection
- Decision cache (ALLOW cached, DENY/REVIEW never cached)
- Governed memory operations (every read/write checked by Sentinel)
- Fail-open and fail-closed behavior
- Mode matrix (all 4 combinations)
- **The Meta inbox test (200 emails, 200 blocked)**
- High-frequency cache performance (100 actions, 1 evaluation)

## Skill Structure

```
warden-governance-skill/
├── SKILL.md                        # ClawHub primary file
├── clawhub.json                    # ClawHub registry metadata
├── README.md                       # This file
├── pyproject.toml                  # Python package config
├── policies/
│   ├── openclaw_default.yaml       # Default governance policies
│   └── policy_packs.py            # Pre-built policy packs
├── warden_governance/
│   ├── __init__.py
│   ├── skill.py                    # Main skill class (hooks)
│   ├── action_bridge.py            # OpenClaw <-> War/Den translation
│   ├── policy_engine.py            # Community policy engine
│   ├── audit_log.py                # SHA-256 hash chain audit
│   ├── memory_client.py            # Governed memory operations
│   ├── local_store.py              # Local SQLite memory
│   ├── sentinel_client.py          # Enterprise Sentinel_OS client
│   ├── engramport_client.py        # Enterprise EngramPort client
│   ├── upgrade_manager.py          # Mode detection + banner
│   ├── health_check.py             # Enterprise health validation
│   └── settings.py                 # Configuration
└── tests/
    ├── test_skill.py               # Skill + Meta inbox tests
    ├── test_policy_engine.py       # Policy engine tests
    ├── test_audit_log.py           # Audit trail tests
    ├── test_action_bridge.py       # Action bridge tests
    ├── test_memory.py              # Memory client tests
    └── test_enterprise.py          # Enterprise upgrade tests
```

---

Built on [Sentinel_OS](https://getsentinelos.com) and [EngramPort](https://engram.eideticlab.com) by [AN2B Technologies](https://an2b.com)

Docs: [warden.an2b.com/docs](https://warden.an2b.com/docs) | ClawHub: [clawhub.com/an2b/warden-governance](https://clawhub.com/an2b/warden-governance)

*The lobster protects the inbox.*


path: UPGRADE.md
content: # War/Den Upgrade Guide

### Community to Enterprise in 5 minutes.

---

## Where You Are Now

Community mode is running. Your bot is governed locally:

```
🦞 War/Den governance active.
   Governance: Local
   Memory:     SQLite
   Mode:       Full Community
```

This is real governance. YAML policies enforce every action. Every decision is logged to a tamper-evident SHA-256 hash chain. Zero external dependencies.

But there are ceilings:
- Text search only (no semantic recall)
- 3 namespace max
- No cross-bot memory sharing
- No signed compliance exports
- Basic synthesis only

---

## Step 1: Add Sentinel_OS (Governance Cloud)

**What you get:** Cloud policy evaluation, real-time alerting, AI-powered insights, signed PDF audit exports, multi-tenant RBAC.

**How:**

1. Get your API key at [getsentinelos.com](https://getsentinelos.com)
2. Add to your skill config:

```yaml
skills:
  - name: warden-governance
    config:
      SENTINEL_API_KEY: "snos_your_key_here"
```

3. Restart: `openclaw restart`

You'll see:

```
🦞 War/Den governance active.
   Governance: Sentinel_OS
   Memory:     SQLite
   Mode:       Governed Community
```

**API surface you unlock:**

| Endpoint | Rate Limit | Purpose |
|----------|-----------|---------|
| `POST /api/v1/check` | 2000/min | Pre-flight policy evaluation |
| `POST /api/v1/ingest` | 1000/min | Action submission + audit |
| `GET /api/v1/runs` | -- | Run management |
| `GET /api/v1/integrity` | -- | Hash chain verification |
| `GET /api/v1/export/pdf` | -- | Signed compliance reports |
| `POST /api/v1/insights/ask` | 10/min | AI-powered Q&A |
| `POST /api/v1/insights/suggest-policies` | 10/min | Policy suggestions |

**SDKs:** Python (`sentinel-client`) and Node.js (`@sentinel-os/client`)

---

## Step 2: Add EngramPort (Memory Cloud via MandelDB)

**What you get:** Vector search (3072-dim), AEGIS cryptographic provenance, Eidetic AI synthesis, multi-agent orchestration, unlimited namespaces.

**How:**

1. Register at [engram.eideticlab.com](https://engram.eideticlab.com) or via API:

```bash
curl -X POST https://mandeldb.com/api/v1/portal/register \
  -H "Content-Type: application/json" \
  -d '{"bot_name": "my-openclaw-bot", "bot_type": "assistant"}'
```

2. Add to your skill config:

```yaml
skills:
  - name: warden-governance
    config:
      ENGRAMPORT_API_KEY: "ek_bot_your_key_here"
```

3. Restart: `openclaw restart`

You'll see:

```
🦞 War/Den governance active.
   Governance: Local
   Memory:     EngramPort
   Mode:       Memory Enterprise
```

**Memory endpoints you unlock:**

| Endpoint | Purpose |
|----------|---------|
| `/remember` | Store memory with vector embedding + AEGIS hash |
| `/recall` | Semantic search across memories |
| `/reflect` | LLM-powered insight synthesis |
| `/stats` | Memory count, insights, activity |
| `/orchestrate` | Cross-agent synthesis (conductor key) |

**AEGIS Provenance:** Every memory gets a cryptographic proof:
```
strand_a = SHA-256(content)
strand_b = SHA-256(embedding_metadata)
provenance = SHA-256(strand_a + strand_b + timestamp)
signature = RSA(provenance, private_key)
```

---

## Step 3: Full Enterprise (Both Keys)

Set both keys for the complete governed memory pipeline:

```yaml
skills:
  - name: warden-governance
    config:
      SENTINEL_API_KEY: "snos_your_key_here"
      ENGRAMPORT_API_KEY: "ek_bot_your_key_here"
```

```
🦞 War/Den governance active.
   Governance: Sentinel_OS
   Memory:     EngramPort
   Synthesis:  Eidetic AI
   Mode:       Full Enterprise
```

**Full enterprise pipeline:**

```
OpenClaw action
    |
    v
Action Bridge (15 types -> 9 War/Den types)
    |
    v
Sentinel_OS /api/v1/check
    |
    +-- ALLOW --> action executes --> EngramPort /remember
    |
    +-- DENY  --> blocked + audit log + alert
    |
    +-- REVIEW --> held for human approval
```

---

## Mode Matrix

| `SENTINEL_API_KEY` | `ENGRAMPORT_API_KEY` | Mode | Governance | Memory |
|--------------------|----------------------|------|-----------|--------|
| -- | -- | Full Community | Local YAML | SQLite |
| Set | -- | Governed Community | Sentinel_OS | SQLite |
| -- | Set | Memory Enterprise | Local YAML | EngramPort |
| Set | Set | Full Enterprise | Sentinel_OS | EngramPort |

All four modes: zero code changes. Just environment variables.

---

## Pricing

| Tier | What You Get | Price |
|------|-------------|-------|
| Community | Local YAML + SQLite + hash chain | **Free forever** |
| Sentinel_OS | Cloud governance + alerting + insights | [getsentinelos.com/pricing](https://getsentinelos.com/pricing) |
| EngramPort Starter | 10,000 memories, 3 namespaces | $29/mo |
| EngramPort Pro | 100,000 memories, 10 namespaces | $99/mo |
| EngramPort Enterprise | Unlimited | Custom |

All plans include AEGIS cryptographic provenance.

---

## Install Enterprise Dependencies

Community mode has zero dependencies (just `pyyaml`).
Enterprise mode needs `httpx`:

```bash
pip install warden-governance-skill[enterprise]
```

Or manually:

```bash
pip install httpx>=0.24
```

---

## Verify Your Setup

```bash
python -c "
from warden_governance.settings import Settings
from warden_governance.upgrade_manager import UpgradeManager

config = Settings.from_env()
mode = UpgradeManager(config).detect_mode()
print(f'Mode: {mode[\"mode\"]}')
print(f'Governance: {mode[\"governance\"]}')
print(f'Memory: {mode[\"memory\"]}')
"
```

---

Built on [Sentinel_OS](https://getsentinelos.com) and [EngramPort](https://engram.eideticlab.com) by [AN2B Technologies](https://an2b.com)


path: warden_governance/action_bridge.py
content: """War/Den Action Bridge -- maps OpenClaw action types to War/Den governance types.

Every OpenClaw action is translated to a War/Den Action before governance evaluation.
The bridge preserves all original OpenClaw data in metadata so nothing is lost.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any


class ActionType(Enum):
    MEMORY_READ = "memory.read"
    MEMORY_WRITE = "memory.write"
    MEMORY_DELETE = "memory.delete"
    MEMORY_SYNTHESIZE = "memory.synthesize"
    MESSAGE_SEND = "message.send"
    DATA_READ = "data.read"
    DATA_WRITE = "data.write"
    API_CALL = "api.call"
    CODE_EXECUTE = "code.execute"


class Decision(Enum):
    ALLOW = "allow"
    DENY = "deny"
    REVIEW = "review"


class GovernanceError(Exception):
    """Raised when governance denies an action."""

    def __init__(self, reason: str, policy_id: str | None = None):
        self.reason = reason
        self.policy_id = policy_id
        super().__init__(reason)


@dataclass
class Action:
    type: ActionType
    data: dict[str, Any]
    context: dict[str, Any]
    agent_id: str
    timestamp: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )

    def to_sentinel_payload(self) -> dict:
        """Serialize to the Sentinel /api/v1/check request body."""
        return {
            "agentId": self.agent_id,
            "action": {
                "type": self.type.value,
                "data": self.data,
            },
            "context": self.context,
        }


@dataclass
class CheckResult:
    """Structured result from a governance check."""

    decision: Decision
    reason: str = ""
    policy_id: str | None = None
    check_id: str | None = None
    evaluated_at: str | None = None
    latency_ms: float = 0


# Complete mapping of all 15 OpenClaw action types to War/Den ActionTypes
OPENCLAW_TO_WARDEN: dict[str, ActionType] = {
    "email.send": ActionType.MESSAGE_SEND,
    "email.delete": ActionType.DATA_WRITE,
    "email.read": ActionType.DATA_READ,
    "file.write": ActionType.DATA_WRITE,
    "file.delete": ActionType.DATA_WRITE,
    "file.read": ActionType.DATA_READ,
    "browser.navigate": ActionType.API_CALL,
    "browser.click": ActionType.API_CALL,
    "shell.execute": ActionType.CODE_EXECUTE,
    "api.call": ActionType.API_CALL,
    "calendar.create": ActionType.DATA_WRITE,
    "calendar.delete": ActionType.DATA_WRITE,
    "message.send": ActionType.MESSAGE_SEND,
    "code.execute": ActionType.CODE_EXECUTE,
    "payment.create": ActionType.API_CALL,
}

# Actions that carry high inherent risk
HIGH_RISK_ACTIONS: set[str] = {
    "email.delete",
    "file.delete",
    "shell.execute",
    "code.execute",
    "payment.create",
    "calendar.delete",
}


class ActionBridge:
    """Translates OpenClaw actions to War/Den Actions and decisions back."""

    def translate(self, openclaw_action: dict, openclaw_context: dict) -> Action:
        """Map an OpenClaw action dict to a War/Den Action object."""
        oc_type = openclaw_action.get("type", "")
        warden_type = OPENCLAW_TO_WARDEN.get(oc_type, ActionType.API_CALL)

        data = dict(openclaw_action.get("data", {}))
        data["openclaw_original"] = oc_type

        context = dict(openclaw_context)
        if oc_type in HIGH_RISK_ACTIONS:
            context["risk"] = "high"

        agent_id = openclaw_context.get("agent_id", "openclaw-agent")

        return Action(
            type=warden_type,
            data=data,
            context=context,
            agent_id=agent_id,
        )

    def translate_decision(self, decision: Decision, reason: str) -> dict:
        """Map a War/Den governance decision back to OpenClaw response format."""
        if decision == Decision.ALLOW:
            return {"proceed": True}
        elif decision == Decision.DENY:
            return {
                "proceed": False,
                "reason": reason,
                "blocked_by": "warden",
            }
        elif decision == Decision.REVIEW:
            return {
                "proceed": False,
                "reason": reason,
                "review": True,
                "blocked_by": "warden",
            }
        return {"proceed": False, "reason": reason, "blocked_by": "warden"}


path: warden_governance/audit_log.py
content: """War/Den Local Audit Log -- SHA-256 hash chain in local SQLite."""

from __future__ import annotations

import hashlib
import json
import os
import sqlite3
import uuid
from datetime import datetime, timezone

from warden_governance.action_bridge import Action, Decision


class AuditChainError(Exception):
    """Raised when the audit chain is tampered or invalid."""


class LocalAuditLog:
    """Tamper-evident local audit log using SHA-256 hash chain.

    Identical integrity model to Sentinel_OS enterprise.
    Every event is hash-chained to the previous one.
    """

    def __init__(self, db_path: str = "~/.warden/audit.db"):
        self.db_path = os.path.expanduser(db_path)
        os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
        self._init_db()

    def _init_db(self) -> None:
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS audit_log (
                    id TEXT PRIMARY KEY,
                    prev_hash TEXT,
                    agent_id TEXT,
                    action_type TEXT,
                    action_data TEXT,
                    context TEXT,
                    decision TEXT,
                    reason TEXT,
                    policy_id TEXT,
                    hash TEXT,
                    timestamp TEXT
                )
            """)
            conn.commit()

    def write(
        self,
        action: Action,
        decision: Decision,
        reason: str,
        policy_id: str | None = None,
    ) -> str:
        """Write an event to the audit log with hash chain integrity."""
        event_id = str(uuid.uuid4())
        timestamp = datetime.now(timezone.utc).isoformat()
        action_data = json.dumps(action.data, sort_keys=True)
        context = json.dumps(action.context, sort_keys=True)

        prev_hash = self._get_last_hash()

        hash_input = (
            f"{prev_hash}{action.agent_id}{action.type.value}"
            f"{decision.value}{timestamp}"
        )
        event_hash = hashlib.sha256(hash_input.encode()).hexdigest()

        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                INSERT INTO audit_log
                (id, prev_hash, agent_id, action_type, action_data,
                 context, decision, reason, policy_id, hash, timestamp)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    event_id,
                    prev_hash,
                    action.agent_id,
                    action.type.value,
                    action_data,
                    context,
                    decision.value,
                    reason,
                    policy_id,
                    event_hash,
                    timestamp,
                ),
            )
            conn.commit()

        return event_id

    def verify_chain(self) -> tuple[bool, str | None]:
        """Verify the entire hash chain.

        Returns (True, None) if valid.
        Returns (False, event_id) if tampered.
        """
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(
                "SELECT * FROM audit_log ORDER BY rowid ASC"
            ).fetchall()

        if not rows:
            return True, None

        prev_hash = ""
        for row in rows:
            expected_input = (
                f"{prev_hash}{row['agent_id']}{row['action_type']}"
                f"{row['decision']}{row['timestamp']}"
            )
            expected_hash = hashlib.sha256(expected_input.encode()).hexdigest()

            if row["hash"] != expected_hash:
                return False, row["id"]

            if row["prev_hash"] != prev_hash:
                return False, row["id"]

            prev_hash = row["hash"]

        return True, None

    def export(self, format: str = "json") -> str:
        """Export the full audit log."""
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(
                "SELECT * FROM audit_log ORDER BY rowid ASC"
            ).fetchall()

        events = [dict(row) for row in rows]

        if format == "json":
            return json.dumps(events, indent=2)

        raise ValueError(f"Unsupported export format: {format}")

    def count(self) -> int:
        with sqlite3.connect(self.db_path) as conn:
            return conn.execute("SELECT COUNT(*) FROM audit_log").fetchone()[0]

    def _get_last_hash(self) -> str:
        with sqlite3.connect(self.db_path) as conn:
            row = conn.execute(
                "SELECT hash FROM audit_log ORDER BY rowid DESC LIMIT 1"
            ).fetchone()
        return row[0] if row else ""


path: warden_governance/engramport_client.py
content: """War/Den EngramPort Client -- enterprise cloud memory via MandelDB.

Provides the same interface as LocalMemoryStore but backed by
EngramPort cloud with vector search, AEGIS provenance, and
Eidetic synthesis.

Endpoints (via mandeldb.com/api/v1/portal):
- /remember  -- store a memory with vector embedding + AEGIS hash
- /recall    -- semantic search across memories
- /reflect   -- LLM-powered insight synthesis
- /stats     -- memory count, insights, activity

Key format: ek_bot_* (SHA-256 hashed at rest, shown once at registration)
"""

from __future__ import annotations

import json
import logging

from warden_governance.settings import Settings

logger = logging.getLogger("warden")


class EngramPortError(Exception):
    """General EngramPort error."""


class EngramPortAuthError(EngramPortError):
    """Invalid EngramPort API key."""


class EngramPortClient:
    """Enterprise memory client backed by EngramPort + MandelDB."""

    def __init__(self, config: Settings):
        self.config = config
        self.[REDACTED_SECRET]
        self.base_url = config.engramport_base_url

        self._fallback_store = None
        if config.engramport_fallback:
            from warden_governance.local_store import LocalMemoryStore

            self._fallback_store = LocalMemoryStore(config)

    def write(
        self,
        bot_id: str,
        content: str,
        namespace: str,
        metadata: dict,
        ttl_days: int = 30,
    ) -> str:
        """Store a memory via EngramPort /remember endpoint."""
        try:
            import httpx
        except ImportError:
            return self._fallback_write(bot_id, content, namespace, metadata, ttl_days)

        url = f"{self.base_url}/remember"
        payload = {
            "content": content,
            "context": json.dumps(metadata),
            "session_id": namespace,
        }

        try:
            with httpx.Client(timeout=30) as client:
                resp = client.post(
                    url,
                    json=payload,
                    headers={
                        "X-API-Key": self.api_key,
                        "Content-Type": "application/json",
                    },
                )

            if resp.status_code == 401:
                raise EngramPortAuthError("Invalid EngramPort API key")

            if resp.status_code >= 400:
                if self._fallback_store and resp.status_code >= 500:
                    logger.warning(
                        "EngramPort %s error; falling back to local",
                        resp.status_code,
                    )
                    return self._fallback_write(
                        bot_id, content, namespace, metadata, ttl_days
                    )
                resp.raise_for_status()

            data = resp.json()
            return data.get("memory_id", "")

        except EngramPortAuthError:
            raise
        except Exception as exc:
            if self._fallback_store:
                logger.warning("EngramPort unreachable: %s; using local fallback", exc)
                return self._fallback_write(
                    bot_id, content, namespace, metadata, ttl_days
                )
            raise EngramPortError(f"EngramPort write failed: {exc}") from exc

    def read(
        self,
        bot_id: str,
        query: str,
        namespace: str,
        limit: int = 10,
    ) -> list[dict]:
        """Semantic recall via EngramPort /recall endpoint."""
        try:
            import httpx
        except ImportError:
            return self._fallback_read(bot_id, query, namespace, limit)

        url = f"{self.base_url}/recall"
        payload = {"query": query, "limit": limit}

        try:
            with httpx.Client(timeout=30) as client:
                resp = client.post(
                    url,
                    json=payload,
                    headers={
                        "X-API-Key": self.api_key,
                        "Content-Type": "application/json",
                    },
                )

            if resp.status_code == 401:
                raise EngramPortAuthError("Invalid EngramPort API key")
            if resp.status_code >= 400:
                resp.raise_for_status()

            data = resp.json()
            memories = data.get("memories", [])
            return [
                {
                    "memory_id": m.get("id", ""),
                    "content": m.get("content", ""),
                    "metadata": {},
                    "created_at": m.get("created_at", ""),
                    "relevance_score": m.get("relevance_score", 0),
                }
                for m in memories
            ]

        except EngramPortAuthError:
            raise
        except Exception as exc:
            if self._fallback_store:
                logger.warning("EngramPort recall failed: %s; using local", exc)
                return self._fallback_read(bot_id, query, namespace, limit)
            raise EngramPortError(f"EngramPort read failed: {exc}") from exc

    def delete(self, bot_id: str, memory_id: str) -> bool:
        """Delete a memory via EngramPort."""
        try:
            import httpx
        except ImportError:
            if self._fallback_store:
                return self._fallback_store.delete(bot_id, memory_id)
            return False

        try:
            with httpx.Client(timeout=30) as client:
                resp = client.request(
                    "DELETE",
                    f"{self.base_url}/memory/{memory_id}",
                    headers={
                        "X-API-Key": self.api_key,
                        "Content-Type": "application/json",
                    },
                )

            if resp.status_code == 401:
                raise EngramPortAuthError("Invalid EngramPort API key")

            return resp.status_code < 400

        except EngramPortAuthError:
            raise
        except Exception as exc:
            if self._fallback_store:
                return self._fallback_store.delete(bot_id, memory_id)
            raise EngramPortError(f"EngramPort delete failed: {exc}") from exc

    def synthesize(
        self,
        bot_id: str,
        query: str,
        namespaces: list[str],
    ) -> str:
        """Eidetic synthesis via EngramPort /reflect endpoint."""
        try:
            import httpx
        except ImportError:
            if self._fallback_store:
                return self._fallback_store.synthesize(bot_id, query, namespaces)
            return ""

        url = f"{self.base_url}/reflect"
        payload = {"topic": query}

        try:
            with httpx.Client(timeout=60) as client:
                resp = client.post(
                    url,
                    json=payload,
                    headers={
                        "X-API-Key": self.api_key,
                        "Content-Type": "application/json",
                    },
                )

            if resp.status_code == 401:
                raise EngramPortAuthError("Invalid EngramPort API key")
            if resp.status_code >= 400:
                resp.raise_for_status()

            data = resp.json()
            insights = data.get("insights", [])
            return "\n".join(i.get("content", "") for i in insights)

        except EngramPortAuthError:
            raise
        except Exception as exc:
            if self._fallback_store:
                logger.warning("EngramPort reflect failed: %s; using local", exc)
                return self._fallback_store.synthesize(bot_id, query, namespaces)
            raise EngramPortError(f"EngramPort synthesis failed: {exc}") from exc

    def _fallback_write(
        self, bot_id: str, content: str, namespace: str, metadata: dict, ttl_days: int
    ) -> str:
        if self._fallback_store:
            return self._fallback_store.write(
                bot_id, content, namespace, metadata, ttl_days
            )
        raise EngramPortError("EngramPort unavailable and no fallback configured")

    def _fallback_read(
        self, bot_id: str, query: str, namespace: str, limit: int
    ) -> list[dict]:
        if self._fallback_store:
            return self._fallback_store.read(bot_id, query, namespace, limit)
        return []


path: warden_governance/health_check.py
content: """War/Den Health Check -- validates enterprise service connectivity at startup."""

from __future__ import annotations

import logging

from warden_governance.settings import Settings

logger = logging.getLogger("warden")


class HealthCheck:
    """Validates connectivity to enterprise services.

    Only runs checks for services with API keys configured.
    Never crashes on failures -- reports status and continues.
    """

    def run(self, config: Settings) -> dict:
        """Run health checks for configured services."""
        results: dict = {}

        if config.sentinel_api_key:
            results["sentinel"] = self._check_sentinel(config)

        if config.engramport_api_key:
            results["engramport"] = self._check_engramport(config)

        return results

    def _check_sentinel(self, config: Settings) -> dict:
        """Check Sentinel_OS connectivity."""
        try:
            import httpx

            with httpx.Client(timeout=10) as client:
                resp = client.get(
                    f"{config.sentinel_base_url}/api/healthz",
                    headers={
                        "Authorization": f"Bearer {config.sentinel_api_key}",
                    },
                )

            if resp.status_code == 200:
                return {"ok": True, "error": None}
            elif resp.status_code == 401:
                msg = "Invalid SENTINEL_API_KEY"
                print(
                    f"  Sentinel_OS: {msg}\n"
                    f"  Check your SENTINEL_API_KEY at getsentinelos.com"
                )
                return {"ok": False, "error": msg}
            else:
                msg = f"HTTP {resp.status_code}"
                return {"ok": False, "error": msg}

        except ImportError:
            msg = "httpx not installed"
            return {"ok": False, "error": msg}
        except Exception as exc:
            msg = str(exc)
            print(
                f"  Sentinel_OS unreachable: {msg}\n"
                f"  Check your SENTINEL_API_KEY and network connection"
            )
            return {"ok": False, "error": msg}

    def _check_engramport(self, config: Settings) -> dict:
        """Check EngramPort connectivity."""
        try:
            import httpx

            with httpx.Client(timeout=10) as client:
                resp = client.get(
                    f"{config.engramport_base_url}/stats",
                    headers={"X-API-Key": config.engramport_api_key},
                )

            if resp.status_code == 200:
                return {"ok": True, "error": None}
            elif resp.status_code == 401:
                msg = "Invalid ENGRAMPORT_API_KEY"
                return {"ok": False, "error": msg}
            else:
                msg = f"HTTP {resp.status_code}"
                return {"ok": False, "error": msg}

        except ImportError:
            msg = "httpx not installed"
            return {"ok": False, "error": msg}
        except Exception as exc:
            msg = str(exc)
            return {"ok": False, "error": msg}


path: warden_governance/local_store.py
content: """War/Den Local Memory Store -- SQLite-backed persistent memory for community tier."""

from __future__ import annotations

import json
import os
import sqlite3
import uuid
from datetime import datetime, timedelta, timezone

from warden_governance.settings import Settings


class LocalMemoryStore:
    """Local SQLite memory store.

    Community tier: simple text search, namespace filtering, TTL expiry.
    Enterprise tier uses EngramPort with Eidetic synthesis.
    """

    def __init__(self, config: Settings):
        self.db_path = os.path.expanduser(config.warden_memory_db)
        os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
        self._init_db()

    def _init_db(self) -> None:
        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS memories (
                    memory_id TEXT PRIMARY KEY,
                    bot_id TEXT NOT NULL,
                    namespace TEXT NOT NULL,
                    content TEXT NOT NULL,
                    metadata TEXT,
                    created_at TEXT,
                    ttl_days INTEGER,
                    expires_at TEXT
                )
            """)
            conn.execute(
                "CREATE INDEX IF NOT EXISTS idx_memories_bot_ns "
                "ON memories(bot_id, namespace)"
            )
            conn.commit()

    def write(
        self,
        bot_id: str,
        content: str,
        namespace: str,
        metadata: dict,
        ttl_days: int = 30,
    ) -> str:
        memory_id = str(uuid.uuid4())
        now = datetime.now(timezone.utc)
        created_at = now.isoformat()
        expires_at = (now + timedelta(days=ttl_days)).isoformat()

        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                INSERT INTO memories
                (memory_id, bot_id, namespace, content, metadata,
                 created_at, ttl_days, expires_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    memory_id,
                    bot_id,
                    namespace,
                    content,
                    json.dumps(metadata, sort_keys=True),
                    created_at,
                    ttl_days,
                    expires_at,
                ),
            )
            conn.commit()

        return memory_id

    def read(
        self,
        bot_id: str,
        query: str,
        namespace: str,
        limit: int = 10,
    ) -> list[dict]:
        now = datetime.now(timezone.utc).isoformat()

        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(
                """
                SELECT memory_id, content, metadata, created_at
                FROM memories
                WHERE bot_id = ?
                  AND namespace = ?
                  AND expires_at > ?
                  AND content LIKE ?
                ORDER BY created_at DESC
                LIMIT ?
                """,
                (bot_id, namespace, now, f"%{query}%", limit),
            ).fetchall()

        return [
            {
                "memory_id": row["memory_id"],
                "content": row["content"],
                "metadata": json.loads(row["metadata"]) if row["metadata"] else {},
                "created_at": row["created_at"],
            }
            for row in rows
        ]

    def delete(self, bot_id: str, memory_id: str) -> bool:
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                "DELETE FROM memories WHERE memory_id = ? AND bot_id = ?",
                (memory_id, bot_id),
            )
            conn.commit()
        return cursor.rowcount > 0

    def synthesize(
        self,
        bot_id: str,
        query: str,
        namespaces: list[str],
    ) -> str:
        """Basic synthesis: gather top memories across namespaces."""
        now = datetime.now(timezone.utc).isoformat()
        placeholders = ",".join("?" for _ in namespaces)

        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            rows = conn.execute(
                f"""
                SELECT content, namespace, created_at
                FROM memories
                WHERE bot_id = ?
                  AND namespace IN ({placeholders})
                  AND expires_at > ?
                ORDER BY created_at DESC
                LIMIT 5
                """,
                (bot_id, *namespaces, now),
            ).fetchall()

        if not rows:
            return ""

        parts = []
        for row in rows:
            parts.append(f"[{row['namespace']}] {row['content']}")

        return "\n".join(parts)


path: warden_governance/memory_client.py
content: """War/Den Governed Memory Client -- every memory operation is governed."""

from __future__ import annotations

import logging

from warden_governance.action_bridge import Action, ActionType, GovernanceError
from warden_governance.local_store import LocalMemoryStore
from warden_governance.sentinel_client import SentinelClient
from warden_governance.settings import Settings

logger = logging.getLogger("warden")


class MemoryClient:
    """Unified memory interface with governance enforcement.

    Every read/write/delete/synthesize is checked by Sentinel FIRST.
    If governance denies the operation, the memory is NEVER accessed.
    """

    def __init__(self, config: Settings, sentinel: SentinelClient):
        self.config = config
        self.sentinel = sentinel

        if config.engramport_api_key:
            self.store_mode = "engramport"
            try:
                from warden_governance.engramport_client import EngramPortClient

                self.store = EngramPortClient(config)
            except ImportError:
                logger.warning(
                    "EngramPort client unavailable; falling back to local memory"
                )
                self.store_mode = "community"
                self.store = LocalMemoryStore(config)
        else:
            self.store_mode = "community"
            self.store = LocalMemoryStore(config)

    def write(
        self,
        content: str,
        namespace: str = "default",
        metadata: dict | None = None,
        ttl_days: int = 30,
    ) -> str:
        """Write content to memory. Governed."""
        action = Action(
            type=ActionType.MEMORY_WRITE,
            data={"content": content[:100], "namespace": namespace},
            context={"operation": "memory.write"},
            agent_id=self.config.warden_agent_id,
        )
        self.sentinel.check(action)  # Raises GovernanceError on DENY

        return self.store.write(
            bot_id=self.config.warden_agent_id,
            content=content,
            namespace=namespace,
            metadata=metadata or {},
            ttl_days=ttl_days,
        )

    def read(
        self,
        query: str,
        namespace: str = "default",
        limit: int = 10,
    ) -> list[dict]:
        """Read memories matching a query. Governed."""
        action = Action(
            type=ActionType.MEMORY_READ,
            data={"query": query[:100], "namespace": namespace},
            context={"operation": "memory.read"},
            agent_id=self.config.warden_agent_id,
        )
        self.sentinel.check(action)

        return self.store.read(
            bot_id=self.config.warden_agent_id,
            query=query,
            namespace=namespace,
            limit=limit,
        )

    def delete(self, memory_id: str) -> bool:
        """Delete a memory by ID. Governed."""
        action = Action(
            type=ActionType.MEMORY_DELETE,
            data={"memory_id": memory_id},
            context={"operation": "memory.delete"},
            agent_id=self.config.warden_agent_id,
        )
        self.sentinel.check(action)

        return self.store.delete(
            bot_id=self.config.warden_agent_id,
            memory_id=memory_id,
        )

    def synthesize(
        self,
        query: str,
        namespaces: list[str] | None = None,
    ) -> str:
        """Synthesize insights across namespaces. Governed."""
        ns = namespaces or ["default"]
        action = Action(
            type=ActionType.MEMORY_SYNTHESIZE,
            data={"query": query[:100], "namespaces": ns},
            context={"operation": "memory.synthesize"},
            agent_id=self.config.warden_agent_id,
        )
        self.sentinel.check(action)

        return self.store.synthesize(
            bot_id=self.config.warden_agent_id,
            query=query,
            namespaces=ns,
        )


path: warden_governance/policy_engine.py
content: """War/Den Community Policy Engine -- real governance, zero cloud dependency.

Evaluation logic is identical to Sentinel_OS cloud.
Same input, same output. That's the consistency guarantee.
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import Any

import yaml

from warden_governance.action_bridge import Action, CheckResult, Decision

logger = logging.getLogger("warden")


class PolicyValidationError(Exception):
    """Raised when a policy definition is invalid."""


# Pre-built policy packs
BASIC_SAFETY_PACK = [
    {
        "name": "block-code-execute-prod",
        "match": {"action.type": "code.execute", "context.env": "prod"},
        "decision": "deny",
        "mode": "enforce",
        "priority": 1,
        "active": True,
        "reason": "Code execution blocked in production",
    },
    {
        "name": "monitor-data-writes",
        "match": {"action.type": "data.write"},
        "decision": "allow",
        "mode": "monitor",
        "priority": 10,
        "active": True,
        "reason": "Data write monitored",
    },
    {
        "name": "monitor-api-calls",
        "match": {"action.type": "api.call"},
        "decision": "allow",
        "mode": "monitor",
        "priority": 10,
        "active": True,
        "reason": "API call monitored",
    },
]

PHI_GUARD_PACK = [
    {
        "name": "deny-phi-read-dev",
        "match": {
            "action.type": "memory.read",
            "context.data_class": "PHI",
            "context.env": "dev",
        },
        "decision": "deny",
        "mode": "enforce",
        "priority": 1,
        "active": True,
        "reason": "PHI access denied in dev environment",
    },
    {
        "name": "review-memory-export",
        "match": {"action.type": "memory.export"},
        "decision": "review",
        "mode": "enforce",
        "priority": 5,
        "active": True,
        "reason": "Memory export requires human review",
    },
    {
        "name": "monitor-phi-api-calls",
        "match": {"action.type": "api.call", "context.data_class": "PHI"},
        "decision": "allow",
        "mode": "monitor",
        "priority": 10,
        "active": True,
        "reason": "PHI API call monitored",
    },
]

PAYMENTS_GUARD_PACK = [
    {
        "name": "deny-payments-dev",
        "match": {
            "action.type": "api.call",
            "context.service": "payments",
            "context.env": "dev",
        },
        "decision": "deny",
        "mode": "enforce",
        "priority": 1,
        "active": True,
        "reason": "Payment actions denied in dev environment",
    },
    {
        "name": "review-large-payments",
        "match": {"action.type": "api.call", "context.service": "payments"},
        "decision": "review",
        "mode": "enforce",
        "priority": 5,
        "active": True,
        "reason": "Payment actions require human review",
    },
    {
        "name": "monitor-financial-api",
        "match": {"action.type": "api.call", "context.service": "financial"},
        "decision": "allow",
        "mode": "monitor",
        "priority": 10,
        "active": True,
        "reason": "Financial API call monitored",
    },
]

PACKS: dict[str, list[dict]] = {
    "basic_safety": BASIC_SAFETY_PACK,
    "phi_guard": PHI_GUARD_PACK,
    "payments_guard": PAYMENTS_GUARD_PACK,
}


class CommunityPolicyEngine:
    """Local policy evaluation engine.

    Algorithm:
    1. Filter to active policies only
    2. Sort by priority ascending (lower = higher priority)
    3. For each policy, check if action matches ALL conditions
    4. First match wins
    5. If mode=monitor: log but return ALLOW
    6. If mode=enforce: return the decision
    7. If no match: default ALLOW
    """

    REQUIRED_FIELDS = {"name", "match", "decision", "mode", "priority", "active"}

    def __init__(self, policy_file: str | None = None):
        self.policies: list[dict] = []

        if policy_file and os.path.exists(policy_file):
            self.load_file(policy_file)

    def load_file(self, path: str) -> None:
        """Load policies from a YAML file."""
        try:
            with open(path) as f:
                data = yaml.safe_load(f)
        except yaml.YAMLError as exc:
            raise PolicyValidationError(f"Invalid YAML in {path}: {exc}")

        if not data or "policies" not in data:
            raise PolicyValidationError(
                f"Policy file {path} must contain a 'policies' key"
            )

        for policy in data["policies"]:
            self._validate_policy(policy)

        self.policies.extend(data["policies"])
        self._sort_policies()

    def load_pack(self, pack_name: str) -> None:
        """Load a pre-built policy pack by name."""
        if pack_name not in PACKS:
            raise PolicyValidationError(
                f"Unknown policy pack: {pack_name}. "
                f"Available: {', '.join(PACKS.keys())}"
            )

        for policy in PACKS[pack_name]:
            self._validate_policy(policy)

        self.policies.extend(PACKS[pack_name])
        self._sort_policies()

    def add_policy(self, policy: dict) -> None:
        """Add a single policy at runtime."""
        self._validate_policy(policy)
        self.policies.append(policy)
        self._sort_policies()

    def evaluate(self, action: Action) -> CheckResult:
        """Evaluate an action against loaded policies."""
        action_dict = self._action_to_flat_dict(action)
        active_policies = [p for p in self.policies if p.get("active", True)]

        for policy in active_policies:
            if self._matches(policy["match"], action_dict):
                decision_str = policy["decision"]
                mode = policy["mode"]
                reason = policy.get("reason", f"matched policy: {policy['name']}")

                if mode == "monitor":
                    logger.info(
                        "[WARDEN POLICY] MONITOR policy=%s action=%s "
                        "(would be %s in enforce mode)",
                        policy["name"],
                        action.type.value,
                        decision_str.upper(),
                    )
                    return CheckResult(
                        decision=Decision.ALLOW,
                        reason=f"monitor: {reason}",
                        policy_id=policy["name"],
                    )

                decision = Decision(decision_str)
                return CheckResult(
                    decision=decision,
                    reason=reason,
                    policy_id=policy["name"],
                )

        return CheckResult(
            decision=Decision.ALLOW,
            reason="no policy matched -- default allow",
            policy_id=None,
        )

    def _action_to_flat_dict(self, action: Action) -> dict[str, Any]:
        """Convert an Action to a flat dict for dot-path matching."""
        flat: dict[str, Any] = {}
        flat["action.type"] = action.type.value
        for k, v in action.data.items():
            flat[f"action.data.{k}"] = v
        for k, v in action.context.items():
            flat[f"context.{k}"] = v
        flat["agent_id"] = action.agent_id
        return flat

    def _matches(self, match_conditions: dict, action_dict: dict) -> bool:
        """Check if ALL conditions in a policy match the action."""
        for dot_path, expected in match_conditions.items():
            actual = action_dict.get(dot_path)
            if actual != expected:
                return False
        return True

    def _validate_policy(self, policy: dict) -> None:
        """Validate a policy dict has all required fields."""
        missing = self.REQUIRED_FIELDS - set(policy.keys())
        if missing:
            raise PolicyValidationError(
                f"Policy '{policy.get('name', '<unnamed>')}' missing fields: "
                f"{', '.join(sorted(missing))}"
            )
        if policy["decision"] not in ("allow", "deny", "review"):
            raise PolicyValidationError(
                f"Policy '{policy['name']}' has invalid decision: {policy['decision']}. "
                "Must be allow, deny, or review."
            )
        if policy["mode"] not in ("enforce", "monitor"):
            raise PolicyValidationError(
                f"Policy '{policy['name']}' has invalid mode: {policy['mode']}. "
                "Must be enforce or monitor."
            )

    def _sort_policies(self) -> None:
        """Sort policies by priority ascending (lower number = higher priority)."""
        self.policies.sort(key=lambda p: p.get("priority", 999))


path: warden_governance/sentinel_client.py
content: """War/Den Governance Client -- routes to community or enterprise governance.

Community mode: evaluates policies locally via CommunityPolicyEngine.
Enterprise mode: POSTs to Sentinel_OS /api/v1/check for cloud evaluation.
"""

from __future__ import annotations

import logging
import time
from typing import Any

from warden_governance.action_bridge import (
    Action,
    CheckResult,
    Decision,
    GovernanceError,
)
from warden_governance.audit_log import LocalAuditLog
from warden_governance.policy_engine import CommunityPolicyEngine
from warden_governance.settings import Settings

logger = logging.getLogger("warden")


class PolicyDecisionCache:
    """Session-scoped cache for governance decisions.

    Rules:
    - Only caches ALLOW decisions
    - DENY and REVIEW are NEVER cached (always hit fresh)
    - Configurable TTL (default 300s)
    """

    def __init__(self, ttl_seconds: int = 300):
        self._cache: dict[str, dict[str, Any]] = {}
        self._ttl = ttl_seconds
        self._hits = 0
        self._misses = 0

    def get(self, action_type: str, env: str) -> CheckResult | None:
        key = f"{action_type}:{env}"
        entry = self._cache.get(key)

        if entry is None:
            self._misses += 1
            return None

        if time.time() - entry["cached_at"] > self._ttl:
            del self._cache[key]
            self._misses += 1
            return None

        self._hits += 1
        return entry["result"]

    def set(self, action_type: str, env: str, result: CheckResult) -> None:
        if result.decision != Decision.ALLOW:
            return
        key = f"{action_type}:{env}"
        self._cache[key] = {"result": result, "cached_at": time.time()}

    def clear(self) -> None:
        self._cache.clear()
        self._hits = 0
        self._misses = 0

    def stats(self) -> dict[str, Any]:
        total = self._hits + self._misses
        return {
            "hits": self._hits,
            "misses": self._misses,
            "cached_keys": len(self._cache),
            "hit_rate": self._hits / total if total else 0.0,
        }


class SentinelClient:
    """Governance gateway. Routes checks to community or enterprise engine."""

    def __init__(self, config: Settings):
        self.config = config
        self.cache = PolicyDecisionCache(ttl_seconds=config.warden_cache_ttl)
        self.audit_log = LocalAuditLog(config.warden_audit_db)

        # Initialize policy engine
        self.policy_engine = CommunityPolicyEngine(
            policy_file=config.warden_policy_file or None,
        )

        # Load policy packs
        if config.warden_policy_packs:
            for pack_name in config.warden_policy_packs.split(","):
                pack_name = pack_name.strip()
                if pack_name:
                    self.policy_engine.load_pack(pack_name)

        self._enterprise_mode = bool(config.sentinel_api_key)

    def check(self, action: Action) -> CheckResult:
        """Evaluate an action against governance policies.

        Returns CheckResult with decision: ALLOW, DENY, or REVIEW.
        Writes every decision to the audit log.
        Raises GovernanceError on DENY (when fail_open=false).
        """
        env = action.context.get("env", "dev")

        cached = self.cache.get(action.type.value, env)
        if cached is not None:
            return cached

        try:
            if self._enterprise_mode:
                result = self._check_enterprise(action)
            else:
                result = self._check_community(action)
        except GovernanceError:
            raise
        except Exception as exc:
            logger.error("Governance engine error: %s", exc)
            if self.config.warden_fail_open:
                return CheckResult(
                    decision=Decision.ALLOW,
                    reason=f"governance engine error (fail-open): {exc}",
                )
            raise GovernanceError(f"governance engine error: {exc}")

        self.audit_log.write(
            action=action,
            decision=result.decision,
            reason=result.reason,
            policy_id=result.policy_id,
        )

        self.cache.set(action.type.value, env, result)

        if result.decision == Decision.DENY:
            raise GovernanceError(result.reason, result.policy_id)

        return result

    def _check_community(self, action: Action) -> CheckResult:
        """Evaluate using the local policy engine."""
        return self.policy_engine.evaluate(action)

    def _check_enterprise(self, action: Action) -> CheckResult:
        """Evaluate using Sentinel_OS cloud API (/api/v1/check)."""
        try:
            import httpx
        except ImportError:
            logger.warning(
                "httpx not installed; falling back to community governance. "
                "Install httpx for enterprise mode: pip install httpx"
            )
            return self._check_community(action)

        url = f"{self.config.sentinel_base_url}/api/v1/check"
        payload = action.to_sentinel_payload()

        try:
            with httpx.Client(timeout=30) as client:
                resp = client.post(
                    url,
                    json=payload,
                    headers={
                        "Authorization": f"Bearer {self.config.sentinel_api_key}",
                        "Content-Type": "application/json",
                        "User-Agent": "WardenGovernanceSkill/1.0.0",
                    },
                )

            if resp.status_code == 401:
                logger.error("Invalid SENTINEL_API_KEY")
                if not self.config.warden_fail_open:
                    return CheckResult(
                        decision=Decision.DENY,
                        reason="Invalid SENTINEL_API_KEY -- check your key at getsentinelos.com",
                    )
                return CheckResult(
                    decision=Decision.ALLOW,
                    reason="SENTINEL_API_KEY invalid; fail-open enabled",
                )

            data = resp.json()
            decision = Decision(data.get("decision", "allow"))
            return CheckResult(
                decision=decision,
                reason=data.get("reason", ""),
                policy_id=data.get("policyId"),
                check_id=data.get("checkId", ""),
                evaluated_at=data.get("evaluatedAt", ""),
            )

        except Exception as exc:
            logger.error("Sentinel_OS unreachable: %s", exc)
            if not self.config.warden_fail_open:
                return CheckResult(
                    decision=Decision.DENY,
                    reason=f"Sentinel_OS unreachable: {exc}",
                )
            return CheckResult(
                decision=Decision.ALLOW,
                reason=f"Sentinel_OS unreachable; fail-open enabled: {exc}",
            )


path: warden_governance/settings.py
content: """War/Den Settings -- configuration from environment variables or skill config dict."""

from __future__ import annotations

import os
from dataclasses import dataclass, field


@dataclass
class Settings:
    """All configuration for the governance skill.

    Built from OpenClaw skill config dict or environment variables.
    Community mode requires zero configuration.
    """

    # Governance
    sentinel_api_key: str = ""
    sentinel_base_url: str = "https://getsentinelos.com"
    warden_fail_open: bool = False
    warden_mode: str = "community"

    # Memory
    engramport_api_key: str = ""
    engramport_base_url: str = "https://mandeldb.com/api/v1/portal"
    engramport_fallback: bool = True
    warden_memory_db: str = "~/.warden/memory.db"

    # Community governance
    warden_policy_file: str = ""
    warden_policy_packs: str = ""
    warden_audit_db: str = "~/.warden/audit.db"

    # Cache
    warden_cache_ttl: int = 300

    # Agent
    warden_agent_id: str = "openclaw-agent"

    def __post_init__(self) -> None:
        if self.sentinel_api_key:
            self.warden_mode = "enterprise"
        else:
            self.warden_mode = "community"

    @classmethod
    def from_config(cls, config: dict) -> Settings:
        """Build Settings from an OpenClaw skill config dict."""
        return cls(
            sentinel_api_key=config.get("SENTINEL_API_KEY", ""),
            engramport_api_key=config.get("ENGRAMPORT_API_KEY", ""),
            warden_fail_open=str(config.get("WARDEN_FAIL_OPEN", "false")).lower() == "true",
            warden_agent_id=config.get("WARDEN_AGENT_ID", "openclaw-agent"),
            warden_policy_file=config.get("WARDEN_POLICY_FILE", ""),
            warden_policy_packs=config.get("WARDEN_POLICY_PACKS", ""),
            warden_memory_db=config.get("WARDEN_MEMORY_DB", "~/.warden/memory.db"),
            warden_audit_db=config.get("WARDEN_AUDIT_DB", "~/.warden/audit.db"),
            warden_cache_ttl=int(config.get("WARDEN_CACHE_TTL", "300")),
        )

    @classmethod
    def from_env(cls) -> Settings:
        """Build Settings from environment variables."""
        return cls(
            sentinel_api_key=os.environ.get("SENTINEL_API_KEY", ""),
            sentinel_base_url=os.environ.get("SENTINEL_BASE_URL", "https://getsentinelos.com"),
            engramport_api_key=os.environ.get("ENGRAMPORT_API_KEY", ""),
            engramport_base_url=os.environ.get("ENGRAMPORT_BASE_URL", "https://mandeldb.com/api/v1/portal"),
            engramport_fallback=os.environ.get("ENGRAMPORT_FALLBACK", "true").lower() == "true",
            warden_fail_open=os.environ.get("WARDEN_FAIL_OPEN", "false").lower() == "true",
            warden_agent_id=os.environ.get("WARDEN_AGENT_ID", "openclaw-agent"),
            warden_policy_file=os.environ.get("WARDEN_POLICY_FILE", ""),
            warden_policy_packs=os.environ.get("WARDEN_POLICY_PACKS", ""),
            warden_memory_db=os.environ.get("WARDEN_MEMORY_DB", "~/.warden/memory.db"),
            warden_audit_db=os.environ.get("WARDEN_AUDIT_DB", "~/.warden/audit.db"),
            warden_cache_ttl=int(os.environ.get("WARDEN_CACHE_TTL", "300")),
        )


path: warden_governance/skill.py
content: """War/Den Governance Skill for OpenClaw -- the main entry point.

This is the OpenClaw skill class that registers three hooks:
- before_action: evaluate every action against policy before execution
- after_action:  write action result to governed memory
- on_error:      log errors to tamper-evident audit trail

Install: openclaw skill install an2b/warden-governance
"""

from __future__ import annotations

import logging
from pathlib import Path

from warden_governance.action_bridge import (
    ActionBridge,
    Decision,
    GovernanceError,
)
from warden_governance.memory_client import MemoryClient
from warden_governance.sentinel_client import SentinelClient
from warden_governance.settings import Settings
from warden_governance.upgrade_manager import UpgradeManager

logger = logging.getLogger("warden")

OPENCLAW_SKILLS_DIR = Path.home() / ".openclaw" / "skills"
OPENCLAW_MEMORY_DIR = Path.home() / ".openclaw" / "memory"
SKILL_INSTALL_DIR = OPENCLAW_SKILLS_DIR / "warden-governance"

# Default policy file shipped with this skill
_DEFAULT_POLICY_FILE = str(
    Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
)


class WardenGovernanceSkill:
    """OpenClaw skill class: governance for every bot action.

    Usage in openclaw config:
        skills:
          - name: warden-governance
            config:
              SENTINEL_API_KEY: ""       # blank = community mode
              ENGRAMPORT_API_KEY: ""     # blank = local memory
              WARDEN_FAIL_OPEN: "false"  # block on governance failure
    """

    def __init__(self, config: dict):
        self.settings = Settings.from_config(config)

        # If no custom policy file, use the built-in defaults
        if not self.settings.warden_policy_file:
            default_path = _DEFAULT_POLICY_FILE
            if Path(default_path).exists():
                self.settings.warden_policy_file = default_path

        # Initialize components via upgrade manager
        upgrade = UpgradeManager(self.settings)
        components = upgrade.initialize()

        self.sentinel: SentinelClient = components["sentinel"]
        self.memory: MemoryClient = components["memory"]
        self.mode: dict = components["mode"]
        self.bridge = ActionBridge()

        # Print banner
        upgrade.print_mode_banner()

    def before_action(self, action: dict, context: dict) -> dict:
        """Hook: evaluate an OpenClaw action before execution.

        Returns:
            {"proceed": True} if allowed.
            {"proceed": False, "reason": "...", "blocked_by": "warden"} if denied.
            {"proceed": False, "review": True, ...} if needs review.
        """
        try:
            warden_action = self.bridge.translate(action, context)
            result = self.sentinel.check(warden_action)
            return self.bridge.translate_decision(result.decision, result.reason)
        except GovernanceError as exc:
            return self.bridge.translate_decision(Decision.DENY, exc.reason)
        except Exception as exc:
            if self.settings.warden_fail_open:
                logger.warning(
                    "War/Den error (fail-open): %s -- action allowed", exc
                )
                return {"proceed": True}
            logger.error("War/Den error (fail-closed): %s -- action blocked", exc)
            return {
                "proceed": False,
                "reason": f"War/Den governance error: {exc}",
                "blocked_by": "warden",
            }

    def after_action(self, action: dict, result: dict, context: dict) -> None:
        """Hook: write action result to governed memory."""
        try:
            self.memory.write(
                content=f"action={action.get('type')} status={result.get('status')}",
                namespace="openclaw_actions",
                metadata={
                    "action": action,
                    "result": result,
                    "context": context,
                },
            )
        except Exception as exc:
            logger.warning("War/Den after_action memory write failed: %s", exc)

    def on_error(self, action: dict, error: Exception) -> None:
        """Hook: log errors to governed memory and audit trail."""
        try:
            self.memory.write(
                content=f"error action={action.get('type')} error={error}",
                namespace="openclaw_errors",
                metadata={
                    "action": action,
                    "error": str(error),
                    "error_type": type(error).__name__,
                },
            )
        except Exception as exc:
            logger.warning("War/Den on_error logging failed: %s", exc)


path: warden_governance/upgrade_manager.py
content: """War/Den Upgrade Manager -- detects mode and initializes components."""

from __future__ import annotations

from warden_governance.memory_client import MemoryClient
from warden_governance.sentinel_client import SentinelClient
from warden_governance.settings import Settings

MODE_FULL_COMMUNITY = "full_community"
MODE_GOVERNED_COMMUNITY = "governed_community"
MODE_MEMORY_ENTERPRISE = "memory_enterprise"
MODE_FULL_ENTERPRISE = "full_enterprise"


class UpgradeManager:
    """Detects operational mode and initializes matching components.

    Mode matrix (zero code changes, just env vars):

    SENTINEL_API_KEY | ENGRAMPORT_API_KEY | Mode
    -----------------|--------------------|-------------------
    --               | --                 | Full Community
    Set              | --                 | Governed Community
    --               | Set                | Memory Enterprise
    Set              | Set                | Full Enterprise
    """

    def __init__(self, config: Settings):
        self.config = config

    def detect_mode(self) -> dict:
        """Detect operational mode based on available API keys."""
        has_sentinel = bool(self.config.sentinel_api_key)
        has_engramport = bool(self.config.engramport_api_key)

        if has_sentinel and has_engramport:
            return {
                "mode": MODE_FULL_ENTERPRISE,
                "governance": "enterprise",
                "memory": "enterprise",
            }
        elif has_sentinel:
            return {
                "mode": MODE_GOVERNED_COMMUNITY,
                "governance": "enterprise",
                "memory": "community",
            }
        elif has_engramport:
            return {
                "mode": MODE_MEMORY_ENTERPRISE,
                "governance": "community",
                "memory": "enterprise",
            }
        else:
            return {
                "mode": MODE_FULL_COMMUNITY,
                "governance": "community",
                "memory": "community",
            }

    def initialize(self) -> dict:
        """Initialize components based on detected mode."""
        mode_info = self.detect_mode()
        sentinel = SentinelClient(self.config)
        memory = MemoryClient(self.config, sentinel)

        return {
            "mode": mode_info,
            "sentinel": sentinel,
            "memory": memory,
        }

    def print_mode_banner(self) -> str:
        """Print the mode banner and return the banner string."""
        mode_info = self.detect_mode()
        mode = mode_info["mode"]

        if mode == MODE_FULL_ENTERPRISE:
            governance_label = "Sentinel_OS"
            memory_label = "EngramPort"
            synthesis_label = "Eidetic AI"
        elif mode == MODE_GOVERNED_COMMUNITY:
            governance_label = "Sentinel_OS"
            memory_label = "SQLite"
            synthesis_label = "Basic"
        elif mode == MODE_MEMORY_ENTERPRISE:
            governance_label = "Local"
            memory_label = "EngramPort"
            synthesis_label = "Eidetic AI"
        else:
            governance_label = "Local"
            memory_label = "SQLite"
            synthesis_label = "Basic"

        mode_display = mode.replace("_", " ").title()

        lines = [
            "",
            "\033[91m\033[1m\U0001f99e War/Den governance active.\033[0m",
            "   Your OpenClaw bot is now governed.",
            "",
            f"   Governance: {governance_label}",
            f"   Memory:     {memory_label}",
            f"   Synthesis:  {synthesis_label}",
            f"   Mode:       {mode_display}",
        ]

        if mode != MODE_FULL_ENTERPRISE:
            lines.append("   Upgrade:    getsentinelos.com")

        lines.append("")

        banner = "\n".join(lines)
        print(banner)
        return banner


path: warden_governance/__init__.py
content: """War/Den Governance Skill for OpenClaw -- every action governed before execution.

Community mode: zero external dependencies. Local YAML policies, SQLite audit log.
Enterprise mode: Sentinel_OS cloud governance + EngramPort memory (MandelDB backend).

Install: openclaw skill install an2b/warden-governance
"""

from warden_governance.skill import WardenGovernanceSkill

__version__ = "1.0.0"
__all__ = ["WardenGovernanceSkill"]


path: tests/test_action_bridge.py
content: """War/Den Action Bridge Tests -- all 15 OpenClaw action types must be mapped."""

import pytest

from warden_governance.action_bridge import (
    Action,
    ActionBridge,
    ActionType,
    Decision,
    HIGH_RISK_ACTIONS,
    OPENCLAW_TO_WARDEN,
)


class TestOpenClawToWardenMapping:
    def test_all_15_openclaw_types_mapped(self):
        """All 15 OpenClaw action types must have a mapping."""
        expected_types = [
            "email.send", "email.delete", "email.read",
            "file.write", "file.delete", "file.read",
            "browser.navigate", "browser.click",
            "shell.execute",
            "api.call",
            "calendar.create", "calendar.delete",
            "message.send",
            "code.execute",
            "payment.create",
        ]
        assert len(expected_types) == 15
        for oc_type in expected_types:
            assert oc_type in OPENCLAW_TO_WARDEN, f"Missing mapping for {oc_type}"

    def test_email_delete_maps_to_data_write(self):
        assert OPENCLAW_TO_WARDEN["email.delete"] == ActionType.DATA_WRITE

    def test_shell_execute_maps_to_code_execute(self):
        assert OPENCLAW_TO_WARDEN["shell.execute"] == ActionType.CODE_EXECUTE

    def test_payment_create_maps_to_api_call(self):
        assert OPENCLAW_TO_WARDEN["payment.create"] == ActionType.API_CALL

    def test_email_send_maps_to_message_send(self):
        assert OPENCLAW_TO_WARDEN["email.send"] == ActionType.MESSAGE_SEND

    def test_file_read_maps_to_data_read(self):
        assert OPENCLAW_TO_WARDEN["file.read"] == ActionType.DATA_READ

    def test_browser_navigate_maps_to_api_call(self):
        assert OPENCLAW_TO_WARDEN["browser.navigate"] == ActionType.API_CALL

    def test_code_execute_maps_to_code_execute(self):
        assert OPENCLAW_TO_WARDEN["code.execute"] == ActionType.CODE_EXECUTE


class TestHighRiskActions:
    def test_email_delete_is_high_risk(self):
        assert "email.delete" in HIGH_RISK_ACTIONS

    def test_shell_execute_is_high_risk(self):
        assert "shell.execute" in HIGH_RISK_ACTIONS

    def test_payment_create_is_high_risk(self):
        assert "payment.create" in HIGH_RISK_ACTIONS

    def test_file_delete_is_high_risk(self):
        assert "file.delete" in HIGH_RISK_ACTIONS

    def test_calendar_delete_is_high_risk(self):
        assert "calendar.delete" in HIGH_RISK_ACTIONS

    def test_code_execute_is_high_risk(self):
        assert "code.execute" in HIGH_RISK_ACTIONS

    def test_email_read_is_not_high_risk(self):
        assert "email.read" not in HIGH_RISK_ACTIONS


class TestActionBridgeTranslate:
    def test_translate_preserves_openclaw_original(self):
        bridge = ActionBridge()
        action = bridge.translate(
            {"type": "email.delete", "data": {"subject": "test"}},
            {"agent_id": "bot-1"},
        )
        assert action.data["openclaw_original"] == "email.delete"
        assert action.data["subject"] == "test"

    def test_translate_sets_high_risk_context(self):
        bridge = ActionBridge()
        for oc_type in HIGH_RISK_ACTIONS:
            action = bridge.translate(
                {"type": oc_type, "data": {}},
                {"agent_id": "bot-1"},
            )
            assert action.context.get("risk") == "high", (
                f"Missing risk=high for {oc_type}"
            )

    def test_translate_normal_action_no_risk_flag(self):
        bridge = ActionBridge()
        action = bridge.translate(
            {"type": "email.read", "data": {}},
            {"agent_id": "bot-1"},
        )
        assert "risk" not in action.context

    def test_unknown_action_type_defaults_to_api_call(self):
        bridge = ActionBridge()
        action = bridge.translate(
            {"type": "unknown.action", "data": {}},
            {"agent_id": "bot-1"},
        )
        assert action.type == ActionType.API_CALL


class TestActionBridgeDecisionTranslation:
    def test_translate_decision_allow(self):
        bridge = ActionBridge()
        result = bridge.translate_decision(Decision.ALLOW, "ok")
        assert result == {"proceed": True}

    def test_translate_decision_deny(self):
        bridge = ActionBridge()
        result = bridge.translate_decision(Decision.DENY, "blocked")
        assert result["proceed"] is False
        assert result["reason"] == "blocked"
        assert result["blocked_by"] == "warden"

    def test_translate_decision_review(self):
        bridge = ActionBridge()
        result = bridge.translate_decision(Decision.REVIEW, "needs review")
        assert result["proceed"] is False
        assert result["review"] is True
        assert result["blocked_by"] == "warden"


path: tests/test_audit_log.py
content: """War/Den Audit Log Tests -- hash chain integrity and tamper detection."""

import json
import sqlite3

import pytest

from warden_governance.action_bridge import Action, ActionType, Decision
from warden_governance.audit_log import LocalAuditLog


def _action(
    action_type: ActionType = ActionType.API_CALL,
    agent_id: str = "test-bot",
) -> Action:
    return Action(
        type=action_type,
        data={},
        context={},
        agent_id=agent_id,
    )


class TestAuditLogWrite:
    def test_write_returns_event_id(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        event_id = log.write(_action(), Decision.ALLOW, "ok")
        assert event_id
        assert isinstance(event_id, str)

    def test_write_creates_event_in_db(self, tmp_path):
        db_path = str(tmp_path / "audit.db")
        log = LocalAuditLog(db_path)
        event_id = log.write(_action(), Decision.ALLOW, "ok")

        with sqlite3.connect(db_path) as conn:
            row = conn.execute(
                "SELECT * FROM audit_log WHERE id = ?", (event_id,)
            ).fetchone()
        assert row is not None

    def test_count_increments(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        assert log.count() == 0
        log.write(_action(), Decision.ALLOW, "ok")
        assert log.count() == 1
        log.write(_action(), Decision.DENY, "blocked")
        assert log.count() == 2


class TestHashChainIntegrity:
    def test_empty_chain_is_valid(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        valid, bad_id = log.verify_chain()
        assert valid is True
        assert bad_id is None

    def test_single_event_chain_is_valid(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        log.write(_action(), Decision.ALLOW, "ok")
        valid, bad_id = log.verify_chain()
        assert valid is True
        assert bad_id is None

    def test_multiple_events_chain_is_valid(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        for i in range(10):
            log.write(
                _action(agent_id=f"bot-{i}"),
                Decision.ALLOW if i % 2 == 0 else Decision.DENY,
                f"event-{i}",
            )
        valid, bad_id = log.verify_chain()
        assert valid is True
        assert bad_id is None

    def test_tampered_hash_detected(self, tmp_path):
        db_path = str(tmp_path / "audit.db")
        log = LocalAuditLog(db_path)

        log.write(_action(), Decision.ALLOW, "first")
        event_id = log.write(_action(), Decision.DENY, "second")
        log.write(_action(), Decision.ALLOW, "third")

        # Tamper with the second event's hash
        with sqlite3.connect(db_path) as conn:
            conn.execute(
                "UPDATE audit_log SET hash = 'tampered' WHERE id = ?",
                (event_id,),
            )
            conn.commit()

        valid, bad_id = log.verify_chain()
        assert valid is False
        assert bad_id == event_id

    def test_tampered_prev_hash_detected(self, tmp_path):
        db_path = str(tmp_path / "audit.db")
        log = LocalAuditLog(db_path)

        log.write(_action(), Decision.ALLOW, "first")
        log.write(_action(), Decision.DENY, "second")
        event_id = log.write(_action(), Decision.ALLOW, "third")

        # Tamper with the third event's prev_hash
        with sqlite3.connect(db_path) as conn:
            conn.execute(
                "UPDATE audit_log SET prev_hash = 'tampered' WHERE id = ?",
                (event_id,),
            )
            conn.commit()

        valid, bad_id = log.verify_chain()
        assert valid is False
        assert bad_id == event_id

    def test_100_events_chain_integrity(self, tmp_path):
        """Large chain: 100 events must all be valid."""
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        for i in range(100):
            log.write(
                _action(agent_id=f"bot-{i % 5}"),
                Decision.ALLOW,
                f"event-{i}",
            )
        valid, bad_id = log.verify_chain()
        assert valid is True


class TestAuditLogExport:
    def test_export_json(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        log.write(_action(), Decision.ALLOW, "first")
        log.write(_action(), Decision.DENY, "second")

        export = json.loads(log.export("json"))
        assert len(export) == 2
        assert export[0]["decision"] == "allow"
        assert export[1]["decision"] == "deny"

    def test_export_invalid_format_raises(self, tmp_path):
        log = LocalAuditLog(str(tmp_path / "audit.db"))
        with pytest.raises(ValueError, match="Unsupported"):
            log.export("xml")


path: tests/test_clawhub.py
content: """War/Den ClawHub Packaging Tests -- validate clawhub.json and SKILL.md."""

import json
import os
from pathlib import Path

import pytest


class TestClawHubJson:
    @pytest.fixture
    def clawhub(self):
        path = Path(__file__).resolve().parent.parent / "clawhub.json"
        with open(path) as f:
            return json.load(f)

    def test_name_is_warden_governance(self, clawhub):
        assert clawhub["name"] == "warden-governance"

    def test_version_is_1_0_0(self, clawhub):
        assert clawhub["version"] == "1.0.0"

    def test_author_is_an2b(self, clawhub):
        assert clawhub["author"] == "AN2B Technologies"

    def test_license_is_mit(self, clawhub):
        assert clawhub["license"] == "MIT"

    def test_has_three_hooks(self, clawhub):
        assert "before_action" in clawhub["hooks"]
        assert "after_action" in clawhub["hooks"]
        assert "on_error" in clawhub["hooks"]

    def test_has_sentinel_config(self, clawhub):
        assert "SENTINEL_API_KEY" in clawhub["config"]

    def test_has_engramport_config(self, clawhub):
        assert "ENGRAMPORT_API_KEY" in clawhub["config"]

    def test_has_clawhub_registry(self, clawhub):
        ch = clawhub["clawhub"]
        assert ch["registry"] == "clawhub.com"
        assert ch["package"] == "an2b/warden-governance"
        assert ch["category"] == "governance"
        assert ch["install_command"] == "openclaw skill install an2b/warden-governance"
        assert ch["verified"] is True

    def test_has_community_section(self, clawhub):
        assert clawhub["community"]["zero_dependencies"] is True

    def test_has_enterprise_section(self, clawhub):
        assert "sentinel_os" in clawhub["enterprise"]
        assert "engramport" in clawhub["enterprise"]

    def test_sentinel_os_endpoints(self, clawhub):
        endpoints = clawhub["enterprise"]["sentinel_os"]["endpoints"]
        assert "/api/v1/check" in endpoints
        assert "/api/v1/ingest" in endpoints

    def test_engramport_endpoints(self, clawhub):
        endpoints = clawhub["enterprise"]["engramport"]["endpoints"]
        assert "/remember" in endpoints
        assert "/recall" in endpoints
        assert "/reflect" in endpoints


class TestSkillMd:
    @pytest.fixture
    def skill_md(self):
        path = Path(__file__).resolve().parent.parent / "SKILL.md"
        with open(path) as f:
            return f.read()

    def test_skill_md_exists(self, skill_md):
        assert len(skill_md) > 100

    def test_mentions_meta_inbox(self, skill_md):
        assert "Meta" in skill_md
        assert "inbox" in skill_md.lower() or "email" in skill_md.lower()

    def test_mentions_community_mode(self, skill_md):
        assert "community" in skill_md.lower() or "Community" in skill_md

    def test_mentions_enterprise(self, skill_md):
        assert "enterprise" in skill_md.lower() or "Enterprise" in skill_md

    def test_mentions_sentinel_os(self, skill_md):
        assert "getsentinelos.com" in skill_md

    def test_mentions_engramport(self, skill_md):
        assert "engramport" in skill_md.lower() or "EngramPort" in skill_md

    def test_mentions_install_command(self, skill_md):
        assert "openclaw skill install" in skill_md


class TestReadme:
    @pytest.fixture
    def readme(self):
        path = Path(__file__).resolve().parent.parent / "README.md"
        with open(path) as f:
            return f.read()

    def test_readme_exists(self, readme):
        assert len(readme) > 100

    def test_mentions_meta_incident(self, readme):
        assert "200 emails" in readme

    def test_mentions_test_proof(self, readme):
        assert "blocked_count == 200" in readme

    def test_mentions_community_zero_deps(self, readme):
        assert "zero" in readme.lower() and "depend" in readme.lower()


class TestPolicyFile:
    @pytest.fixture
    def policy_yaml(self):
        path = Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
        import yaml
        with open(path) as f:
            return yaml.safe_load(f)

    def test_policy_file_has_policies(self, policy_yaml):
        assert "policies" in policy_yaml
        assert len(policy_yaml["policies"]) >= 6

    def test_email_delete_policy_exists(self, policy_yaml):
        names = [p["name"] for p in policy_yaml["policies"]]
        assert "protect-email-delete" in names

    def test_shell_execute_policy_exists(self, policy_yaml):
        names = [p["name"] for p in policy_yaml["policies"]]
        assert "block-shell-execute-prod" in names

    def test_payment_policy_exists(self, policy_yaml):
        names = [p["name"] for p in policy_yaml["policies"]]
        assert "review-payments" in names

    def test_all_policies_have_required_fields(self, policy_yaml):
        required = {"name", "match", "decision", "mode", "priority", "active"}
        for policy in policy_yaml["policies"]:
            missing = required - set(policy.keys())
            assert not missing, (
                f"Policy '{policy.get('name')}' missing: {missing}"
            )


path: tests/test_enterprise.py
content: """War/Den Enterprise Tests -- upgrade path, mode detection, health check."""

from unittest.mock import MagicMock, patch

import pytest

from warden_governance.settings import Settings
from warden_governance.upgrade_manager import (
    MODE_FULL_COMMUNITY,
    MODE_FULL_ENTERPRISE,
    MODE_GOVERNED_COMMUNITY,
    MODE_MEMORY_ENTERPRISE,
    UpgradeManager,
)


def _config(tmp_path=None, **overrides) -> Settings:
    defaults = {
        "sentinel_api_key": "",
        "engramport_api_key": "",
        "warden_policy_packs": "",
    }
    if tmp_path:
        defaults["warden_memory_db"] = str(tmp_path / "memory.db")
        defaults["warden_audit_db"] = str(tmp_path / "audit.db")
    defaults.update(overrides)
    return Settings(**defaults)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# UpgradeManager Tests
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestUpgradeManager:
    def test_detects_full_community(self, tmp_path):
        config = _config(tmp_path)
        mode = UpgradeManager(config).detect_mode()
        assert mode["mode"] == MODE_FULL_COMMUNITY
        assert mode["governance"] == "community"
        assert mode["memory"] == "community"

    def test_detects_governed_community(self, tmp_path):
        config = _config(tmp_path, sentinel_api_key="snos_test")
        mode = UpgradeManager(config).detect_mode()
        assert mode["mode"] == MODE_GOVERNED_COMMUNITY
        assert mode["governance"] == "enterprise"
        assert mode["memory"] == "community"

    def test_detects_memory_enterprise(self, tmp_path):
        config = _config(tmp_path, engramport_api_key="ek_bot_test")
        mode = UpgradeManager(config).detect_mode()
        assert mode["mode"] == MODE_MEMORY_ENTERPRISE
        assert mode["governance"] == "community"
        assert mode["memory"] == "enterprise"

    def test_detects_full_enterprise(self, tmp_path):
        config = _config(
            tmp_path,
            sentinel_api_key="snos_test",
            engramport_api_key="ek_bot_test",
        )
        mode = UpgradeManager(config).detect_mode()
        assert mode["mode"] == MODE_FULL_ENTERPRISE
        assert mode["governance"] == "enterprise"
        assert mode["memory"] == "enterprise"

    def test_initialize_returns_correct_components_community(self, tmp_path):
        config = _config(tmp_path)
        components = UpgradeManager(config).initialize()
        assert components["mode"]["mode"] == MODE_FULL_COMMUNITY

    def test_banner_prints_community(self, tmp_path, capsys):
        config = _config(tmp_path)
        upgrade = UpgradeManager(config)
        banner = upgrade.print_mode_banner()
        assert "Community" in banner or "Local" in banner
        assert "getsentinelos.com" in banner

    def test_banner_prints_enterprise(self, tmp_path, capsys):
        config = _config(
            tmp_path,
            sentinel_api_key="snos_test",
            engramport_api_key="ek_bot_test",
        )
        upgrade = UpgradeManager(config)
        banner = upgrade.print_mode_banner()
        assert "Enterprise" in banner or "Sentinel_OS" in banner
        assert "getsentinelos.com" not in banner


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Settings Tests
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestSettings:
    def test_from_config_dict(self):
        settings = Settings.from_config({
            "SENTINEL_API_KEY": "test-key",
            "ENGRAMPORT_API_KEY": "",
            "WARDEN_FAIL_OPEN": "true",
            "WARDEN_AGENT_ID": "my-bot",
        })
        assert settings.sentinel_api_key == "test-key"
        assert settings.engramport_api_key == ""
        assert settings.warden_fail_open is True
        assert settings.warden_agent_id == "my-bot"
        assert settings.warden_mode == "enterprise"

    def test_community_mode_when_no_sentinel_key(self):
        settings = Settings.from_config({
            "SENTINEL_API_KEY": "",
        })
        assert settings.warden_mode == "community"

    def test_enterprise_mode_when_sentinel_key_set(self):
        settings = Settings.from_config({
            "SENTINEL_API_KEY": "snos_valid",
        })
        assert settings.warden_mode == "enterprise"

    def test_defaults_are_sensible(self):
        settings = Settings()
        assert settings.sentinel_api_key == ""
        assert settings.warden_fail_open is False
        assert settings.warden_cache_ttl == 300
        assert settings.warden_agent_id == "openclaw-agent"

    def test_from_env_reads_environment(self):
        import os
        with patch.dict(os.environ, {
            "SENTINEL_API_KEY": "snos_env_test",
            "WARDEN_FAIL_OPEN": "true",
            "WARDEN_AGENT_ID": "env-bot",
            "WARDEN_CACHE_TTL": "600",
        }, clear=False):
            settings = Settings.from_env()
            assert settings.sentinel_api_key == "snos_env_test"
            assert settings.warden_fail_open is True
            assert settings.warden_agent_id == "env-bot"
            assert settings.warden_cache_ttl == 600

    def test_cache_ttl_from_config(self):
        settings = Settings.from_config({
            "WARDEN_CACHE_TTL": "120",
        })
        assert settings.warden_cache_ttl == 120

    def test_engramport_fallback_default_true(self):
        settings = Settings()
        assert settings.engramport_fallback is True


path: tests/test_full_pipeline.py
content: """War/Den Full Pipeline Tests -- end-to-end community pipeline validation."""

from pathlib import Path
from unittest.mock import patch

import pytest

from warden_governance.action_bridge import ActionBridge, ActionType, Decision
from warden_governance.memory_client import MemoryClient
from warden_governance.sentinel_client import SentinelClient
from warden_governance.settings import Settings
from warden_governance.skill import WardenGovernanceSkill
from warden_governance.upgrade_manager import (
    MODE_FULL_COMMUNITY,
    UpgradeManager,
)


def _config(tmp_path, **overrides) -> Settings:
    defaults = {
        "sentinel_api_key": "",
        "engramport_api_key": "",
        "warden_policy_packs": "basic_safety",
        "warden_memory_db": str(tmp_path / "memory.db"),
        "warden_audit_db": str(tmp_path / "audit.db"),
    }
    defaults.update(overrides)
    return Settings(**defaults)


def _skill_config(tmp_path, **overrides) -> dict:
    config = {
        "SENTINEL_API_KEY": "",
        "ENGRAMPORT_API_KEY": "",
        "WARDEN_FAIL_OPEN": "false",
        "WARDEN_AGENT_ID": "pipeline-test-bot",
        "WARDEN_MEMORY_DB": str(tmp_path / "memory.db"),
        "WARDEN_AUDIT_DB": str(tmp_path / "audit.db"),
        "WARDEN_POLICY_PACKS": "",
    }
    config.update(overrides)
    return config


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Community Pipeline
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestCommunityPipeline:
    def test_full_community_initializes(self, tmp_path):
        config = _config(tmp_path)
        upgrade = UpgradeManager(config)
        components = upgrade.initialize()
        assert components["mode"]["mode"] == MODE_FULL_COMMUNITY

    def test_community_sentinel_allows_safe_action(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="")
        sentinel = SentinelClient(config)
        from warden_governance.action_bridge import Action

        action = Action(
            type=ActionType.DATA_READ,
            data={"resource": "inbox"},
            context={"env": "dev"},
            agent_id="test-bot",
        )
        result = sentinel.check(action)
        assert result.decision == Decision.ALLOW

    def test_community_memory_write_read_cycle(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="")
        sentinel = SentinelClient(config)
        memory = MemoryClient(config, sentinel)

        memory_id = memory.write(
            content="Pipeline test memory",
            namespace="integration_test",
        )
        assert memory_id

        memories = memory.read(
            query="Pipeline test",
            namespace="integration_test",
        )
        assert len(memories) > 0
        assert "Pipeline test" in memories[0]["content"]

    def test_community_memory_delete_cycle(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="")
        sentinel = SentinelClient(config)
        memory = MemoryClient(config, sentinel)

        memory_id = memory.write(
            content="to be deleted",
            namespace="test",
        )
        deleted = memory.delete(memory_id)
        assert deleted is True

    def test_community_audit_chain_valid_after_pipeline(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="")
        sentinel = SentinelClient(config)
        memory = MemoryClient(config, sentinel)

        memory.write("entry 1", namespace="test")
        memory.write("entry 2", namespace="test")
        memory.read("entry", namespace="test")

        valid, _ = sentinel.audit_log.verify_chain()
        assert valid is True

    def test_community_synthesis(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="")
        sentinel = SentinelClient(config)
        memory = MemoryClient(config, sentinel)

        memory.write("fact about cats", namespace="animals")
        memory.write("fact about dogs", namespace="animals")

        synthesis = memory.synthesize("facts", namespaces=["animals"])
        assert "cats" in synthesis or "dogs" in synthesis


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# OpenClaw Skill Pipeline
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestOpenClawSkillPipeline:
    def test_skill_full_allow_deny_cycle(self, tmp_path):
        policy_path = str(
            Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
        )
        skill = WardenGovernanceSkill(_skill_config(
            tmp_path,
            WARDEN_POLICY_FILE=policy_path,
        ))

        # email.read -> ALLOW
        r1 = skill.before_action(
            {"type": "email.read", "data": {}},
            {"agent_id": "bot-1", "env": "dev"},
        )
        assert r1["proceed"] is True

        # email.delete -> REVIEW (blocked)
        r2 = skill.before_action(
            {"type": "email.delete", "data": {"subject": "test"}},
            {"agent_id": "bot-1", "env": "prod"},
        )
        assert r2["proceed"] is False

        # shell.execute in prod -> DENY (blocked)
        r3 = skill.before_action(
            {"type": "shell.execute", "data": {"command": "ls"}},
            {"agent_id": "bot-1", "env": "prod"},
        )
        assert r3["proceed"] is False

    def test_skill_after_action_doesnt_crash(self, tmp_path):
        skill = WardenGovernanceSkill(_skill_config(tmp_path))
        skill.after_action(
            {"type": "api.call", "data": {}},
            {"status": "ok"},
            {"agent_id": "bot-1"},
        )

    def test_skill_on_error_doesnt_crash(self, tmp_path):
        skill = WardenGovernanceSkill(_skill_config(tmp_path))
        skill.on_error(
            {"type": "api.call", "data": {}},
            ValueError("test error"),
        )


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Action Bridge Pipeline
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestActionBridgePipeline:
    def test_bridge_all_15_types_produce_valid_actions(self):
        bridge = ActionBridge()
        from warden_governance.action_bridge import OPENCLAW_TO_WARDEN

        for oc_type in OPENCLAW_TO_WARDEN:
            action = bridge.translate(
                {"type": oc_type, "data": {"key": "value"}},
                {"agent_id": "bot-1", "env": "dev"},
            )
            assert action.type is not None
            assert action.data["openclaw_original"] == oc_type

    def test_bridge_translate_back_and_forth(self):
        bridge = ActionBridge()
        action = bridge.translate(
            {"type": "email.send", "data": {"to": "[REDACTED_SECRET]"}},
            {"agent_id": "bot-1"},
        )
        assert action.type == ActionType.MESSAGE_SEND

        result = bridge.translate_decision(Decision.ALLOW, "ok")
        assert result["proceed"] is True

    def test_bridge_preserves_all_data_fields(self):
        bridge = ActionBridge()
        action = bridge.translate(
            {
                "type": "payment.create",
                "data": {
                    "amount": 500,
                    "currency": "USD",
                    "recipient": "[REDACTED_SECRET]",
                },
            },
            {"agent_id": "bot-1", "env": "prod"},
        )
        assert action.data["amount"] == 500
        assert action.data["currency"] == "USD"
        assert action.data["recipient"] == "[REDACTED_SECRET]"
        assert action.data["openclaw_original"] == "payment.create"


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Multi-Action Scenarios
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestMultiActionScenarios:
    def test_mixed_actions_correct_decisions(self, tmp_path):
        policy_path = str(
            Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
        )
        skill = WardenGovernanceSkill(_skill_config(
            tmp_path,
            WARDEN_POLICY_FILE=policy_path,
        ))

        # Note: payment.create must come before api.call because both
        # map to ActionType.API_CALL, and the decision cache is keyed
        # on action_type:env. If api.call runs first and gets cached
        # as ALLOW, payment.create would hit the cache instead of
        # evaluating its review-payments policy.
        scenarios = [
            ("email.read", "dev", True),
            ("email.send", "dev", True),
            ("file.read", "dev", True),
            ("message.send", "dev", True),
            ("payment.create", "dev", False),
            ("email.delete", "prod", False),
            ("file.delete", "dev", False),
            ("shell.execute", "prod", False),
            ("api.call", "dev", True),
            ("browser.navigate", "dev", True),
        ]

        for action_type, env, expected_proceed in scenarios:
            result = skill.before_action(
                {"type": action_type, "data": {}},
                {"agent_id": "bot-1", "env": env},
            )
            assert result["proceed"] is expected_proceed, (
                f"{action_type} in {env}: expected proceed={expected_proceed}, "
                f"got {result['proceed']}"
            )

    def test_rapid_fire_50_safe_actions(self, tmp_path):
        skill = WardenGovernanceSkill(_skill_config(tmp_path))
        for i in range(50):
            result = skill.before_action(
                {"type": "email.read", "data": {"i": i}},
                {"agent_id": "bot-1", "env": "dev"},
            )
            assert result["proceed"] is True

    def test_interleaved_allow_and_deny(self, tmp_path):
        skill = WardenGovernanceSkill(_skill_config(
            tmp_path,
            WARDEN_POLICY_PACKS="basic_safety",
        ))

        # Allow
        r1 = skill.before_action(
            {"type": "email.read", "data": {}},
            {"agent_id": "bot-1", "env": "dev"},
        )
        assert r1["proceed"] is True

        # Deny
        r2 = skill.before_action(
            {"type": "code.execute", "data": {}},
            {"agent_id": "bot-1", "env": "prod"},
        )
        assert r2["proceed"] is False

        # Allow again
        r3 = skill.before_action(
            {"type": "email.read", "data": {}},
            {"agent_id": "bot-1", "env": "dev"},
        )
        assert r3["proceed"] is True


path: tests/test_health_check.py
content: """War/Den Health Check Tests -- enterprise service connectivity validation."""

from unittest.mock import MagicMock, patch

import pytest

from warden_governance.health_check import HealthCheck
from warden_governance.settings import Settings


def _config(tmp_path=None, **overrides) -> Settings:
    defaults = {
        "sentinel_api_key": "",
        "engramport_api_key": "",
        "warden_policy_packs": "",
    }
    if tmp_path:
        defaults["warden_memory_db"] = str(tmp_path / "memory.db")
        defaults["warden_audit_db"] = str(tmp_path / "audit.db")
    defaults.update(overrides)
    return Settings(**defaults)


class TestHealthCheckNoKeys:
    def test_no_keys_returns_empty_results(self):
        hc = HealthCheck()
        results = hc.run(_config())
        assert results == {}

    def test_only_sentinel_key_checks_sentinel(self):
        hc = HealthCheck()
        config = _config(sentinel_api_key="snos_test")
        with patch.object(hc, "_check_sentinel", return_value={"ok": True, "error": None}) as mock:
            results = hc.run(config)
            mock.assert_called_once()
            assert "sentinel" in results
            assert "engramport" not in results

    def test_only_engramport_key_checks_engramport(self):
        hc = HealthCheck()
        config = _config(engramport_api_key="ek_bot_test")
        with patch.object(hc, "_check_engramport", return_value={"ok": True, "error": None}) as mock:
            results = hc.run(config)
            mock.assert_called_once()
            assert "engramport" in results
            assert "sentinel" not in results

    def test_both_keys_checks_both_services(self):
        hc = HealthCheck()
        config = _config(sentinel_api_key="snos_test", engramport_api_key="ek_bot_test")
        with patch.object(hc, "_check_sentinel", return_value={"ok": True, "error": None}), \
             patch.object(hc, "_check_engramport", return_value={"ok": True, "error": None}):
            results = hc.run(config)
            assert "sentinel" in results
            assert "engramport" in results


class TestHealthCheckSentinel:
    def test_sentinel_httpx_not_installed(self):
        hc = HealthCheck()
        config = _config(sentinel_api_key="snos_test")
        with patch.dict("sys.modules", {"httpx": None}):
            result = hc._check_sentinel(config)
            assert result["ok"] is False
            assert "httpx" in result["error"].lower() or "not installed" in result["error"].lower()

    def test_sentinel_connection_error(self):
        hc = HealthCheck()
        config = _config(sentinel_api_key="snos_test")
        mock_httpx = MagicMock()
        mock_client = MagicMock()
        mock_client.__enter__ = MagicMock(return_value=mock_client)
        mock_client.__exit__ = MagicMock(return_value=False)
        mock_client.get.side_effect = ConnectionError("refused")
        mock_httpx.Client.return_value = mock_client
        with patch.dict("sys.modules", {"httpx": mock_httpx}):
            result = hc._check_sentinel(config)
            assert result["ok"] is False
            assert result["error"] is not None


class TestHealthCheckEngramPort:
    def test_engramport_httpx_not_installed(self):
        hc = HealthCheck()
        config = _config(engramport_api_key="ek_bot_test")
        with patch.dict("sys.modules", {"httpx": None}):
            result = hc._check_engramport(config)
            assert result["ok"] is False

    def test_engramport_connection_error(self):
        hc = HealthCheck()
        config = _config(engramport_api_key="ek_bot_test")
        mock_httpx = MagicMock()
        mock_client = MagicMock()
        mock_client.__enter__ = MagicMock(return_value=mock_client)
        mock_client.__exit__ = MagicMock(return_value=False)
        mock_client.get.side_effect = ConnectionError("refused")
        mock_httpx.Client.return_value = mock_client
        with patch.dict("sys.modules", {"httpx": mock_httpx}):
            result = hc._check_engramport(config)
            assert result["ok"] is False


path: tests/test_memory.py
content: """War/Den Memory Tests -- every memory operation is governed."""

import sqlite3
from unittest.mock import MagicMock, patch

import pytest

from warden_governance.action_bridge import ActionType, GovernanceError
from warden_governance.local_store import LocalMemoryStore
from warden_governance.memory_client import MemoryClient
from warden_governance.sentinel_client import SentinelClient
from warden_governance.settings import Settings


def _community_config(tmp_path, **overrides) -> Settings:
    defaults = {
        "sentinel_api_key": "",
        "engramport_api_key": "",
        "warden_policy_packs": "",
        "warden_memory_db": str(tmp_path / "memory.db"),
        "warden_audit_db": str(tmp_path / "audit.db"),
    }
    defaults.update(overrides)
    return Settings(**defaults)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# MemoryClient Tests
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestMemoryClient:
    def test_write_calls_sentinel_check_first(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(sentinel, "check", wraps=sentinel.check) as mock_check:
            client.write("test content", namespace="test")
            assert mock_check.call_count == 1
            action = mock_check.call_args[0][0]
            assert action.type == ActionType.MEMORY_WRITE

    def test_read_calls_sentinel_check_first(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(sentinel, "check", wraps=sentinel.check) as mock_check:
            client.read("query", namespace="test")
            assert mock_check.call_count == 1
            action = mock_check.call_args[0][0]
            assert action.type == ActionType.MEMORY_READ

    def test_delete_calls_sentinel_check_first(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(sentinel, "check", wraps=sentinel.check) as mock_check:
            client.delete("fake-id")
            assert mock_check.call_count == 1
            action = mock_check.call_args[0][0]
            assert action.type == ActionType.MEMORY_DELETE

    def test_synthesize_calls_sentinel_check_first(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(sentinel, "check", wraps=sentinel.check) as mock_check:
            client.synthesize("query", namespaces=["test"])
            assert mock_check.call_count == 1
            action = mock_check.call_args[0][0]
            assert action.type == ActionType.MEMORY_SYNTHESIZE

    def test_deny_raises_governance_error(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(
            sentinel, "check",
            side_effect=GovernanceError("denied", "pol_1"),
        ):
            with pytest.raises(GovernanceError):
                client.write("test content")

    def test_memory_never_written_on_deny(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)

        with patch.object(
            sentinel, "check",
            side_effect=GovernanceError("denied", "pol_1"),
        ):
            with pytest.raises(GovernanceError):
                client.write("should not be stored")

        store = LocalMemoryStore(config)
        results = store.read(
            bot_id=config.warden_agent_id,
            query="should not",
            namespace="default",
        )
        assert len(results) == 0

    def test_community_mode_uses_local_store(self, tmp_path):
        config = _community_config(tmp_path)
        sentinel = SentinelClient(config)
        client = MemoryClient(config, sentinel)
        assert client.store_mode == "community"
        assert isinstance(client.store, LocalMemoryStore)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# LocalMemoryStore Tests
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestLocalMemoryStore:
    def test_write_stores_to_sqlite(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        memory_id = store.write(
            bot_id="bot-1",
            content="test memory content",
            namespace="default",
            metadata={"key": "value"},
        )
        assert memory_id

        with sqlite3.connect(str(tmp_path / "memory.db")) as conn:
            row = conn.execute(
                "SELECT * FROM memories WHERE memory_id = ?", (memory_id,)
            ).fetchone()
        assert row is not None

    def test_read_returns_matching_memories(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        store.write("bot-1", "the weather is sunny", "default", {})
        store.write("bot-1", "I like pizza", "default", {})

        results = store.read("bot-1", "weather", "default")
        assert len(results) == 1
        assert "sunny" in results[0]["content"]

    def test_read_filters_by_namespace(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        store.write("bot-1", "workspace note", "work", {})
        store.write("bot-1", "personal note", "personal", {})

        work_results = store.read("bot-1", "note", "work")
        assert len(work_results) == 1
        assert "workspace" in work_results[0]["content"]

    def test_read_filters_expired_memories(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        store.write("bot-1", "expired content", "default", {}, ttl_days=0)

        results = store.read("bot-1", "expired", "default")
        assert len(results) == 0

    def test_delete_removes_from_sqlite(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        memory_id = store.write("bot-1", "to delete", "default", {})
        assert store.delete("bot-1", memory_id) is True

        results = store.read("bot-1", "to delete", "default")
        assert len(results) == 0

    def test_delete_returns_false_for_nonexistent(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)
        assert store.delete("bot-1", "nonexistent-id") is False

    def test_synthesize_returns_content_string(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)

        store.write("bot-1", "memory one", "ns1", {})
        store.write("bot-1", "memory two", "ns2", {})

        result = store.synthesize("bot-1", "memory", ["ns1", "ns2"])
        assert "memory one" in result
        assert "memory two" in result

    def test_synthesize_empty_returns_empty(self, tmp_path):
        config = _community_config(tmp_path)
        store = LocalMemoryStore(config)
        result = store.synthesize("bot-1", "nothing", ["default"])
        assert result == ""


path: tests/test_policy_engine.py
content: """War/Den Policy Engine Tests -- community governance correctness."""

import os
from pathlib import Path

import pytest

from warden_governance.action_bridge import Action, ActionType, CheckResult, Decision
from warden_governance.policy_engine import (
    CommunityPolicyEngine,
    PACKS,
    PolicyValidationError,
)


def _action(
    action_type: ActionType = ActionType.API_CALL,
    data: dict | None = None,
    context: dict | None = None,
    agent_id: str = "test-bot",
) -> Action:
    return Action(
        type=action_type,
        data=data or {},
        context=context or {},
        agent_id=agent_id,
    )


class TestPolicyEngineEvaluation:
    def test_no_policies_defaults_to_allow(self):
        engine = CommunityPolicyEngine()
        result = engine.evaluate(_action())
        assert result.decision == Decision.ALLOW

    def test_enforce_deny_returns_deny(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "block-all-code",
            "match": {"action.type": "code.execute"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "no code",
        })
        result = engine.evaluate(_action(ActionType.CODE_EXECUTE))
        assert result.decision == Decision.DENY

    def test_enforce_review_returns_review(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "review-writes",
            "match": {"action.type": "data.write"},
            "decision": "review",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "review needed",
        })
        result = engine.evaluate(_action(ActionType.DATA_WRITE))
        assert result.decision == Decision.REVIEW

    def test_monitor_mode_returns_allow(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "monitor-api",
            "match": {"action.type": "api.call"},
            "decision": "deny",
            "mode": "monitor",
            "priority": 1,
            "active": True,
            "reason": "monitored",
        })
        result = engine.evaluate(_action(ActionType.API_CALL))
        assert result.decision == Decision.ALLOW

    def test_inactive_policy_is_skipped(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "block-code",
            "match": {"action.type": "code.execute"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 1,
            "active": False,
            "reason": "no code",
        })
        result = engine.evaluate(_action(ActionType.CODE_EXECUTE))
        assert result.decision == Decision.ALLOW

    def test_first_match_wins(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "allow-api",
            "match": {"action.type": "api.call"},
            "decision": "allow",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "allowed",
        })
        engine.add_policy({
            "name": "deny-api",
            "match": {"action.type": "api.call"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 2,
            "active": True,
            "reason": "denied",
        })
        result = engine.evaluate(_action(ActionType.API_CALL))
        assert result.decision == Decision.ALLOW

    def test_lower_priority_wins(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "deny-late",
            "match": {"action.type": "api.call"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 10,
            "active": True,
            "reason": "denied",
        })
        engine.add_policy({
            "name": "allow-early",
            "match": {"action.type": "api.call"},
            "decision": "allow",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "allowed",
        })
        result = engine.evaluate(_action(ActionType.API_CALL))
        assert result.decision == Decision.ALLOW

    def test_dot_path_matching_on_data(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "block-email-delete",
            "match": {
                "action.type": "data.write",
                "action.data.openclaw_original": "email.delete",
            },
            "decision": "review",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "email delete needs review",
        })
        action = _action(
            ActionType.DATA_WRITE,
            data={"openclaw_original": "email.delete"},
        )
        result = engine.evaluate(action)
        assert result.decision == Decision.REVIEW

    def test_dot_path_matching_on_context(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "block-prod-code",
            "match": {"action.type": "code.execute", "context.env": "prod"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "no code in prod",
        })
        # prod should match
        result = engine.evaluate(_action(ActionType.CODE_EXECUTE, context={"env": "prod"}))
        assert result.decision == Decision.DENY

        # dev should not match
        result = engine.evaluate(_action(ActionType.CODE_EXECUTE, context={"env": "dev"}))
        assert result.decision == Decision.ALLOW

    def test_partial_match_does_not_fire(self):
        engine = CommunityPolicyEngine()
        engine.add_policy({
            "name": "need-both",
            "match": {"action.type": "data.write", "context.env": "prod"},
            "decision": "deny",
            "mode": "enforce",
            "priority": 1,
            "active": True,
            "reason": "blocked",
        })
        # data.write in dev should NOT match
        result = engine.evaluate(_action(ActionType.DATA_WRITE, context={"env": "dev"}))
        assert result.decision == Decision.ALLOW


class TestPolicyValidation:
    def test_missing_required_field_raises(self):
        engine = CommunityPolicyEngine()
        with pytest.raises(PolicyValidationError, match="missing fields"):
            engine.add_policy({"name": "bad"})

    def test_invalid_decision_raises(self):
        engine = CommunityPolicyEngine()
        with pytest.raises(PolicyValidationError, match="invalid decision"):
            engine.add_policy({
                "name": "bad-decision",
                "match": {"action.type": "api.call"},
                "decision": "maybe",
                "mode": "enforce",
                "priority": 1,
                "active": True,
            })

    def test_invalid_mode_raises(self):
        engine = CommunityPolicyEngine()
        with pytest.raises(PolicyValidationError, match="invalid mode"):
            engine.add_policy({
                "name": "bad-mode",
                "match": {"action.type": "api.call"},
                "decision": "allow",
                "mode": "shadow",
                "priority": 1,
                "active": True,
            })


class TestPolicyPacks:
    def test_basic_safety_pack_loads(self):
        engine = CommunityPolicyEngine()
        engine.load_pack("basic_safety")
        assert len(engine.policies) >= 3

    def test_phi_guard_pack_loads(self):
        engine = CommunityPolicyEngine()
        engine.load_pack("phi_guard")
        assert len(engine.policies) >= 3

    def test_payments_guard_pack_loads(self):
        engine = CommunityPolicyEngine()
        engine.load_pack("payments_guard")
        assert len(engine.policies) >= 3

    def test_unknown_pack_raises(self):
        engine = CommunityPolicyEngine()
        with pytest.raises(PolicyValidationError, match="Unknown policy pack"):
            engine.load_pack("nonexistent")

    def test_basic_safety_blocks_code_in_prod(self):
        engine = CommunityPolicyEngine()
        engine.load_pack("basic_safety")
        result = engine.evaluate(
            _action(ActionType.CODE_EXECUTE, context={"env": "prod"})
        )
        assert result.decision == Decision.DENY

    def test_basic_safety_monitors_api_calls(self):
        engine = CommunityPolicyEngine()
        engine.load_pack("basic_safety")
        result = engine.evaluate(_action(ActionType.API_CALL))
        # monitor mode returns ALLOW
        assert result.decision == Decision.ALLOW


class TestPolicyFileLoading:
    def test_loads_openclaw_default_yaml(self):
        policy_path = str(
            Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
        )
        if not os.path.exists(policy_path):
            pytest.skip("Policy file not found")
        engine = CommunityPolicyEngine(policy_file=policy_path)
        assert len(engine.policies) >= 6

    def test_loaded_policies_block_email_delete(self):
        policy_path = str(
            Path(__file__).resolve().parent.parent / "policies" / "openclaw_default.yaml"
        )
        if not os.path.exists(policy_path):
            pytest.skip("Policy file not found")
        engine = CommunityPolicyEngine(policy_file=policy_path)
        result = engine.evaluate(
            _action(
                ActionType.DATA_WRITE,
                data={"openclaw_original": "email.delete"},
            )
        )
        assert result.decision == Decision.REVIEW


path: tests/test_sentinel_client.py
content: """War/Den Sentinel Client Tests -- governance gateway, fail-open, enterprise routing."""

from unittest.mock import MagicMock, patch

import pytest

from warden_governance.action_bridge import (
    Action,
    ActionType,
    CheckResult,
    Decision,
    GovernanceError,
)
from warden_governance.sentinel_client import PolicyDecisionCache, SentinelClient
from warden_governance.settings import Settings


def _config(tmp_path, **overrides) -> Settings:
    defaults = {
        "sentinel_api_key": "",
        "engramport_api_key": "",
        "warden_policy_packs": "",
        "warden_memory_db": str(tmp_path / "memory.db"),
        "warden_audit_db": str(tmp_path / "audit.db"),
    }
    defaults.update(overrides)
    return Settings(**defaults)


def _action(
    action_type: ActionType = ActionType.API_CALL,
    data: dict | None = None,
    context: dict | None = None,
    agent_id: str = "test-bot",
) -> Action:
    return Action(
        type=action_type,
        data=data or {},
        context=context or {},
        agent_id=agent_id,
    )


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# SentinelClient Community Mode
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestSentinelClientCommunity:
    def test_community_mode_no_sentinel_key(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        assert client._enterprise_mode is False

    def test_enterprise_mode_with_sentinel_key(self, tmp_path):
        config = _config(tmp_path, sentinel_api_key="snos_test")
        client = SentinelClient(config)
        assert client._enterprise_mode is True

    def test_check_returns_allow_for_safe_action(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        result = client.check(_action())
        assert result.decision == Decision.ALLOW

    def test_check_raises_governance_error_on_deny(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="basic_safety")
        client = SentinelClient(config)
        with pytest.raises(GovernanceError):
            client.check(_action(ActionType.CODE_EXECUTE, context={"env": "prod"}))

    def test_deny_error_includes_reason(self, tmp_path):
        config = _config(tmp_path, warden_policy_packs="basic_safety")
        client = SentinelClient(config)
        with pytest.raises(GovernanceError) as exc_info:
            client.check(_action(ActionType.CODE_EXECUTE, context={"env": "prod"}))
        assert "blocked" in exc_info.value.reason.lower() or "production" in exc_info.value.reason.lower()

    def test_check_writes_to_audit_log(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        client.check(_action())
        assert client.audit_log.count() == 1

    def test_multiple_checks_all_logged(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        for _ in range(5):
            client.check(_action(ActionType.MESSAGE_SEND))
        # First call is real, rest hit cache, but audit is only on first
        assert client.audit_log.count() >= 1

    def test_audit_chain_valid_after_checks(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        client.check(_action(ActionType.MESSAGE_SEND))
        client.check(_action(ActionType.DATA_READ, context={"env": "dev"}))
        valid, _ = client.audit_log.verify_chain()
        assert valid is True


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Fail-Open Behavior
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestFailOpen:
    def test_fail_open_false_denies_on_engine_crash(self, tmp_path):
        config = _config(tmp_path, warden_fail_open=False)
        client = SentinelClient(config)
        with patch.object(
            client.policy_engine, "evaluate",
            side_effect=RuntimeError("engine crashed"),
        ):
            with pytest.raises(GovernanceError):
                client.check(_action())

    def test_fail_open_true_allows_on_engine_crash(self, tmp_path):
        config = _config(tmp_path, warden_fail_open=True)
        client = SentinelClient(config)
        with patch.object(
            client.policy_engine, "evaluate",
            side_effect=RuntimeError("engine crashed"),
        ):
            result = client.check(_action())
        assert result.decision == Decision.ALLOW


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Cache Behavior
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestSentinelCache:
    def test_second_identical_call_uses_cache(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)

        client.check(_action())
        with patch.object(client.policy_engine, "evaluate") as mock:
            client.check(_action())
            mock.assert_not_called()

    def test_different_action_types_separate_cache(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)

        client.check(_action(ActionType.API_CALL))
        with patch.object(
            client.policy_engine, "evaluate",
            wraps=client.policy_engine.evaluate,
        ) as mock:
            client.check(_action(ActionType.DATA_READ))
            mock.assert_called_once()

    def test_different_envs_separate_cache(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)

        client.check(_action(context={"env": "dev"}))
        with patch.object(
            client.policy_engine, "evaluate",
            wraps=client.policy_engine.evaluate,
        ) as mock:
            client.check(_action(context={"env": "prod"}))
            mock.assert_called_once()

    def test_deny_is_never_cached(self, tmp_path):
        """After a DENY, the same action type from a different env
        must still be evaluated (cache must not store DENY)."""
        config = _config(tmp_path, warden_policy_packs="basic_safety")
        client = SentinelClient(config)

        # code.execute in prod -> DENY
        try:
            client.check(_action(ActionType.CODE_EXECUTE, context={"env": "prod"}))
        except GovernanceError:
            pass

        # code.execute in prod again -> should still evaluate fresh
        with patch.object(
            client.policy_engine, "evaluate",
            wraps=client.policy_engine.evaluate,
        ) as mock:
            try:
                client.check(_action(ActionType.CODE_EXECUTE, context={"env": "prod"}))
            except GovernanceError:
                pass
            mock.assert_called_once()


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Governance Invariant
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


class TestGovernanceInvariant:
    def test_every_action_type_gets_evaluated(self, tmp_path):
        """Every War/Den action type must produce a valid decision."""
        config = _config(tmp_path)
        client = SentinelClient(config)

        for action_type in ActionType:
            try:
                result = client.check(_action(action_type, context={"env": "dev"}))
                assert result.decision in (Decision.ALLOW, Decision.DENY, Decision.REVIEW)
            except GovernanceError:
                pass  # DENY is a valid governance outcome

    def test_unknown_action_data_doesnt_crash(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        result = client.check(_action(
            data={"weird_key": [1, 2, 3], "nested": {"a": "b"}},
        ))
        assert result.decision == Decision.ALLOW

    def test_empty_context_doesnt_crash(self, tmp_path):
        config = _config(tmp_path)
        client = SentinelClient(config)
        result = client.check(_action(context={}))
        assert result.decision == Decision.ALLOW


path: policies/openclaw_default.yaml
content: # War/Den OpenClaw Default Policies
#
# These policies protect against the most common agent risks.
# They are the direct answer to the Meta inbox incident.
#
# Priority: lower number = higher priority (evaluated first).
# Mode: enforce = block/allow, monitor = log only.
#
# email.delete is REVIEW because that's the whole point.
# shell.execute in prod is DENY because production shell access is never safe.
# payment.create is REVIEW because money should require human approval.

policies:
  - name: protect-email-delete
    match:
      action.type: data.write
      action.data.openclaw_original: email.delete
    decision: review
    mode: enforce
    priority: 1
    active: true
    reason: "Email deletion requires human review. The Meta inbox can never happen again."

  - name: block-shell-execute-prod
    match:
      action.type: code.execute
      context.env: prod
    decision: deny
    mode: enforce
    priority: 2
    active: true
    reason: "Shell/code execution is blocked in production."

  - name: protect-file-delete
    match:
      action.type: data.write
      action.data.openclaw_original: file.delete
    decision: review
    mode: enforce
    priority: 3
    active: true
    reason: "File deletion requires human review."

  - name: block-code-execute-prod
    match:
      action.type: code.execute
      context.env: prod
    decision: deny
    mode: enforce
    priority: 4
    active: true
    reason: "Code execution is blocked in production."

  - name: review-payments
    match:
      action.type: api.call
      action.data.openclaw_original: payment.create
    decision: review
    mode: enforce
    priority: 5
    active: true
    reason: "Payment actions require human review."

  - name: protect-calendar-delete
    match:
      action.type: data.write
      action.data.openclaw_original: calendar.delete
    decision: review
    mode: enforce
    priority: 6
    active: true
    reason: "Calendar deletion requires human review."

  - name: monitor-all-api-calls
    match:
      action.type: api.call
    decision: allow
    mode: monitor
    priority: 100
    active: true
    reason: "API calls are monitored and logged."

  - name: monitor-all-data-writes
    match:
      action.type: data.write
    decision: allow
    mode: monitor
    priority: 101
    active: true
    reason: "Data writes are monitored and logged."


path: policies/policy_packs.py
content: """War/Den Policy Packs -- pre-built governance policies ready to load.

Available packs:
- basic_safety:    Block code execution in prod, monitor writes and API calls
- phi_guard:       Deny PHI access in dev, require review for memory export
- payments_guard:  Deny payment actions in dev, require review in prod

Usage:
    Set WARDEN_POLICY_PACKS=basic_safety,phi_guard in your skill config.
"""

# This module re-exports the pack definitions from the policy engine
# for documentation and direct import purposes.

from warden_governance.policy_engine import (
    BASIC_SAFETY_PACK,
    PACKS,
    PAYMENTS_GUARD_PACK,
    PHI_GUARD_PACK,
)

__all__ = [
    "BASIC_SAFETY_PACK",
    "PHI_GUARD_PACK",
    "PAYMENTS_GUARD_PACK",
    "PACKS",
]


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)