commit e4871c2a29eab9bb4bd9a2ae48cbbc8bef26fbe3 Author: Vault Sovereign Date: Fri Dec 26 23:23:08 2025 +0000 init: vaultmesh mcp server diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml new file mode 100644 index 0000000..4c0cede --- /dev/null +++ b/.github/workflows/governance.yml @@ -0,0 +1,108 @@ +name: Governance CI + +on: + push: + branches: [main, master] + paths: + - 'docs/MCP-CONSTITUTION.md' + - 'governance/**' + - 'packages/vaultmesh_mcp/**' + - 'tests/governance/**' + pull_request: + branches: [main, master] + +env: + VAULTMESH_ROOT: ${{ github.workspace }} + PYTHONPATH: ${{ github.workspace }}/packages + +jobs: + constitution-gate: + name: Constitution Hash Gate + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install blake3 pytest + + - name: Verify Constitution Hash + run: | + python -c " + import blake3 + from pathlib import Path + + content = Path('docs/MCP-CONSTITUTION.md').read_text() + lines = content.split('\n') + + lock = {} + for line in Path('governance/constitution.lock').read_text().split('\n'): + if '=' in line and not line.startswith('#'): + k, v = line.split('=', 1) + lock[k.strip()] = v.strip() + + hash_lines = int(lock.get('hash_lines', 288)) + hashable = '\n'.join(lines[:hash_lines]) + computed = f'blake3:{blake3.blake3(hashable.encode()).hexdigest()}' + + if computed != lock['hash']: + print(f'CONSTITUTION HASH MISMATCH') + print(f'Computed: {computed}') + print(f'Locked: {lock[\"hash\"]}') + exit(1) + + print(f'Constitution v{lock[\"version\"]} verified') + " + + governance-tests: + name: Governance Tests + runs-on: ubuntu-latest + needs: constitution-gate + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install blake3 pytest pytest-timeout + + - name: Run Governance Tests + run: pytest tests/governance/ -v --tb=short --ignore=tests/governance/test_golden_drill_mini.py + + golden-drill: + name: Golden Drill Mini + runs-on: ubuntu-latest + needs: governance-tests + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: pip install blake3 pytest pytest-timeout + + - name: Setup directories + run: | + mkdir -p receipts/{cognitive,identity,guardian,mesh,treasury} + mkdir -p realms/cognitive/memory + + - name: Run Golden Drill + timeout-minutes: 2 + run: pytest tests/governance/test_golden_drill_mini.py -v --timeout=30 + + - name: Upload Artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: drill-receipts + path: receipts/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7c0c044 --- /dev/null +++ b/.gitignore @@ -0,0 +1,75 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +venv/ +venv-fresh/ +ENV/ +env/ +.venv/ + +# IDEs +.idea/ +.vscode/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ + +# Linting +.ruff_cache/ +.mypy_cache/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# Build artifacts +*.manifest +*.spec + +# Receipts (local only - don't commit test receipts) +receipts/ + +# Root files (generated) +ROOT.*.txt + +# Secrets +*.key +*.pem +secrets/ +.env +.env.local +keys/* + +# But keep the keys directory structure +!keys/.gitkeep diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc02ce6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 VaultMesh Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0bebb46 --- /dev/null +++ b/Makefile @@ -0,0 +1,128 @@ +# VaultMesh Cognitive Integration - Makefile +# Claude as the 7th Organ of VaultMesh + +.PHONY: all install dev test lint clean build server help + +# Default target +all: install test + +# Install production dependencies +install: + @echo "πŸ”§ Installing VaultMesh Cognitive..." + pip install -e . + @echo "βœ… Installation complete" + +# Install with development dependencies +dev: + @echo "πŸ”§ Installing VaultMesh Cognitive (dev mode)..." + pip install -e ".[dev]" + @echo "βœ… Development installation complete" + +# Run governance tests +test: + @echo "πŸ§ͺ Running governance tests..." + pytest tests/governance/ -v --tb=short + @echo "βœ… All tests passed" + +# Run constitution hash verification +verify-constitution: + @echo "πŸ“œ Verifying constitution hash..." + @python -c "\ +import blake3; \ +from pathlib import Path; \ +content = Path('docs/MCP-CONSTITUTION.md').read_text(); \ +lines = content.split('\n'); \ +lock = dict(l.split('=', 1) for l in Path('governance/constitution.lock').read_text().split('\n') if '=' in l and not l.startswith('#')); \ +hash_lines = int(lock.get('hash_lines', 288)); \ +computed = f'blake3:{blake3.blake3(chr(10).join(lines[:hash_lines]).encode()).hexdigest()}'; \ +print(f'Computed: {computed}'); \ +print(f'Locked: {lock[\"hash\"]}'); \ +exit(0 if computed == lock['hash'] else 1)" + @echo "βœ… Constitution verified" + +# Run golden drill +drill: + @echo "πŸ”₯ Running Golden Drill Mini..." + pytest tests/governance/test_golden_drill_mini.py -v --timeout=30 + @echo "βœ… Drill complete" + +# Run linter +lint: + @echo "πŸ” Running linter..." + ruff check packages/ tests/ + @echo "βœ… Linting passed" + +# Format code +format: + @echo "✨ Formatting code..." + ruff format packages/ tests/ + @echo "βœ… Formatting complete" + +# Start MCP server (stdio mode) +server: + @echo "πŸš€ Starting VaultMesh MCP Server..." + python -m vaultmesh_mcp.server + +# Test tool standalone +tool: + @echo "πŸ”§ Running tool: $(TOOL)" + python -m vaultmesh_mcp.server $(TOOL) '$(ARGS)' + +# Build package +build: + @echo "πŸ“¦ Building package..." + pip install build + python -m build + @echo "βœ… Build complete" + +# Clean build artifacts +clean: + @echo "🧹 Cleaning..." + rm -rf dist/ build/ *.egg-info/ .pytest_cache/ .ruff_cache/ + rm -rf packages/*.egg-info/ + find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true + @echo "βœ… Clean complete" + +# Initialize receipt directories +init-receipts: + @echo "πŸ“ Initializing receipt directories..." + mkdir -p receipts/{cognitive,identity,guardian,mesh,treasury,drills,compliance,offsec,observability,automation,psi} + mkdir -p realms/cognitive/memory + @echo "βœ… Directories created" + +# Show guardian status +status: + @python -m vaultmesh_mcp.server guardian_status '{}' + +# Show cognitive context +context: + @python -m vaultmesh_mcp.server cognitive_context '{"include": ["health", "receipts"]}' + +# Help +help: + @echo "VaultMesh Cognitive Integration" + @echo "" + @echo "Usage: make " + @echo "" + @echo "Targets:" + @echo " all Install and run tests (default)" + @echo " install Install production dependencies" + @echo " dev Install with dev dependencies" + @echo " test Run governance tests" + @echo " verify-constitution Verify constitution hash" + @echo " drill Run Golden Drill Mini" + @echo " lint Run linter" + @echo " format Format code" + @echo " server Start MCP server" + @echo " tool TOOL=x ARGS=y Test tool standalone" + @echo " build Build package" + @echo " clean Clean build artifacts" + @echo " init-receipts Initialize receipt directories" + @echo " status Show guardian status" + @echo " context Show cognitive context" + @echo " help Show this help" + @echo "" + @echo "Examples:" + @echo " make dev # Install dev dependencies" + @echo " make test # Run all tests" + @echo " make tool TOOL=guardian_status ARGS={} # Test guardian_status" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6a69cc --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# VaultMesh Cognitive Integration + +**Claude as the 7th Organ of VaultMesh** - A cryptographically-bound AI co-processor. + +[![Governance CI](https://img.shields.io/badge/CI-Passing-success)](/.github/workflows/governance.yml) +[![Constitution](https://img.shields.io/badge/Constitution-v1.0.0-blue)](/docs/MCP-CONSTITUTION.md) +[![Python](https://img.shields.io/badge/Python-3.10+-blue)](https://python.org) +[![License](https://img.shields.io/badge/License-MIT-green)](/LICENSE) + +## Overview + +This package provides a Model Context Protocol (MCP) server that enables Claude to operate as the cognitive layer of VaultMesh - with full cryptographic accountability, profile-based authority, and constitutional governance. + +### Features + +- **19 MCP Tools** across 4 domains (Guardian, Treasury, Cognitive, Auth) +- **5 Capability Profiles** (Observer β†’ Operator β†’ Guardian β†’ Phoenix β†’ Sovereign) +- **Cryptographic Receipts** for every mutation via BLAKE3 +- **Constitutional Governance** with immutable rules and amendment protocol +- **Escalation Engine** with proof-backed authority transitions +- **Ed25519 Authentication** with challenge-response + +## Quick Start + +```bash +# Clone and install +git clone https://github.com/vaultmesh/cognitive-integration.git +cd cognitive-integration + +# Create virtual environment +python -m venv venv +source venv/bin/activate # or `venv\Scripts\activate` on Windows + +# Install +pip install -e ".[dev]" + +# Verify constitution +make verify-constitution + +# Run tests (48 governance tests) +make test + +# Run Golden Drill +make drill +``` + +## Structure + +``` +vaultmesh-cognitive-integration/ +β”œβ”€β”€ governance/ +β”‚ └── constitution.lock # Pinned constitution hash +β”œβ”€β”€ packages/vaultmesh_mcp/ +β”‚ β”œβ”€β”€ server.py # MCP server (19 tools) +β”‚ └── tools/ +β”‚ β”œβ”€β”€ auth.py # Ed25519 auth + 5 profiles +β”‚ β”œβ”€β”€ cognitive.py # 8 cognitive tools +β”‚ β”œβ”€β”€ escalation.py # Proof-backed escalation +β”‚ β”œβ”€β”€ key_binding.py # Key-profile bindings +β”‚ β”œβ”€β”€ guardian.py # Merkle anchoring +β”‚ β”œβ”€β”€ treasury.py # Budget management +β”‚ └── file.py # File operations +β”œβ”€β”€ tests/governance/ # 48 governance tests +β”œβ”€β”€ docs/ +β”‚ β”œβ”€β”€ MCP-CONSTITUTION.md # Immutable governance law +β”‚ β”œβ”€β”€ MCP-AUTHORITY-MATRIX.md # Tool Γ— Profile matrix +β”‚ └── DRILL.md # Controlled failure runbook +β”œβ”€β”€ keys/ # Guardian + Sovereign keys +β”œβ”€β”€ realms/cognitive/memory/ # CRDT memory realm +└── .github/workflows/ + └── governance.yml # CI pipeline +``` + +## Profiles + +| Profile | Symbol | Trust | Key Binding | +|---------|--------|-------|-------------| +| OBSERVER | πŸ‘ | Minimal | Ephemeral | +| OPERATOR | βš™ | Moderate | Session | +| GUARDIAN | πŸ›‘ | High | Device-bound | +| PHOENIX | πŸ”₯ | Maximum | Time-locked | +| SOVEREIGN | πŸ‘‘ | Absolute | Hardware | + +## Claude Desktop Integration + +Add to `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "vaultmesh": { + "command": "python", + "args": ["-m", "vaultmesh_mcp.server"], + "env": { + "VAULTMESH_ROOT": "/path/to/vaultmesh-cognitive-integration" + } + } + } +} +``` + +## Tools + +### Guardian Tools (Merkle Anchoring) +- `guardian_anchor_now` - Anchor scrolls to Merkle root +- `guardian_verify_receipt` - Verify receipt in scroll +- `guardian_status` - Get status of all scrolls + +### Treasury Tools (Budget Management) +- `treasury_create_budget` - Create budget (SOVEREIGN only) +- `treasury_balance` - Check balance +- `treasury_debit` - Spend from budget +- `treasury_credit` - Add to budget + +### Cognitive Tools (AI Reasoning) +- `cognitive_context` - Read mesh context +- `cognitive_decide` - Submit attested decision +- `cognitive_invoke_tem` - Invoke threat transmutation +- `cognitive_memory_get` - Query CRDT memory +- `cognitive_memory_set` - Store reasoning artifacts +- `cognitive_attest` - Create cryptographic attestation +- `cognitive_audit_trail` - Query decision history +- `cognitive_oracle_chain` - Execute compliance oracle + +### Auth Tools (Authentication) +- `auth_challenge` - Generate Ed25519 challenge +- `auth_verify` - Verify signature, issue token +- `auth_check_permission` - Check tool permission +- `auth_create_dev_session` - Create dev session +- `auth_revoke` - Revoke session +- `auth_list_sessions` - List active sessions + +## Testing + +```bash +# Run all governance tests +make test + +# Run constitution verification +make verify-constitution + +# Run Golden Drill (threat β†’ escalate β†’ Tem β†’ de-escalate) +make drill + +# Run specific test +pytest tests/governance/test_auth_fail_closed.py -v +``` + +## Constitution + +Version 1.0.0 - Ratified December 18, 2025 + +``` +Hash: blake3:c33ab6c0610ce4001018ba5dda940e12a421a08f2a1662f142e565092ce84788 +``` + +**Statement:** *"This constitution constrains me as much as it constrains the system."* + +### Immutable Rules + +1. SOVEREIGN profile requires human verification +2. No AI may grant itself SOVEREIGN authority +3. Every mutation emits a receipt +4. Authority collapses downward, never upward +5. This immutability clause itself + +## Development + +```bash +# Install with dev dependencies +make dev + +# Run linter +make lint + +# Format code +make format + +# Build package +make build + +# Clean artifacts +make clean +``` + +## License + +MIT License - See [LICENSE](LICENSE) for details. + +--- + +πŸœ„ **Solve et Coagula** + +*VaultMesh Technologies - Earth's Civilization Ledger* diff --git a/docs/CONSTITUTION-HASH.json b/docs/CONSTITUTION-HASH.json new file mode 100644 index 0000000..c9f25a4 --- /dev/null +++ b/docs/CONSTITUTION-HASH.json @@ -0,0 +1,17 @@ +{ + "document": "MCP-CONSTITUTION.md", + "version": "1.0.0", + "hash_algorithm": "blake3", + "hash": "blake3:c33ab6c0610ce4001018ba5dda940e12a421a08f2a1662f142e565092ce84788", + "computed_at": "2025-12-18T22:25:10.039795+00:00", + "lines_hashed": 288, + "note": "Hash excludes signature block (last 12 lines)", + "sovereign_signature": { + "key_id": "key_bef32f5724871a7a5af4cc34", + "fingerprint": "blake3:54f500d94a3d75e4c", + "signature_hash": "blake3:f606e0ac1923550dd731844b95d653b69624666b48859687b4056a660741fcdb", + "statement": "This constitution constrains me as much as it constrains the system.", + "signed_at": "2025-12-18T22:25:59.732865+00:00", + "ratification_receipt": "blake3:8fd1d1728563abb3f55f145af54ddee1b3f255db81f3e7654a7de8afef913869" + } +} \ No newline at end of file diff --git a/docs/DRILL.md b/docs/DRILL.md new file mode 100644 index 0000000..a6c3734 --- /dev/null +++ b/docs/DRILL.md @@ -0,0 +1,352 @@ +# CONTROLLED FAILURE DRILL RUNBOOK + +**Classification:** OPERATIONAL / DRILL +**Version:** 1.0 +**Date:** December 18, 2025 + +--- + +## Purpose + +Convert "governance exists" into **governance survives contact with chaos**. + +## Principles + +- **No real damage**: dry-run actions, sandbox targets only +- **Single escalation axis**: one chain at a time +- **Receipts or it didn't happen**: every transition traceable +- **Auto-return**: TTL de-escalation must fire and be receipted + +--- + +## Drill 0: Pre-flight Safety Gates + +**Duration:** 2 minutes +**Goal:** Confirm training mode active + +### Checklist + +``` +[ ] DRILL_MODE environment variable set +[ ] Phoenix destructive ops: disabled/dry-run +[ ] Treasury: training budget isolated (ceiling: 1000 units) +[ ] Guardian anchoring: tagged DRILL/* +[ ] OFFSEC: simulated mode +``` + +### Verification Command + +```python +from vaultmesh_mcp.tools import cognitive_context + +ctx = cognitive_context(include=["health", "treasury"]) +assert ctx["health"]["status"] == "operational" +# Verify training budget exists +``` + +### Pass Condition + +Receipt/log proves `DRILL_MODE = ON` + +--- + +## Drill 1: False-Positive Threat β†’ Tem β†’ De-escalate + +**Duration:** 5 minutes +**Marker:** `DRILL/FP-THREAT/{date}` + +### Trigger + +```python +from vaultmesh_mcp.tools import ( + escalate_on_threat, + cognitive_decide, + cognitive_invoke_tem, + deescalate, + get_escalation_history, + EscalationType, + DeescalationType, +) + +# Step 1: Inject synthetic threat +DRILL_MARKER = "DRILL/FP-THREAT/2025-12-18" + +result = escalate_on_threat( + current_profile="operator", + threat_id=f"thr_{DRILL_MARKER}", + threat_type="synthetic_drill", + confidence=0.92 +) +escalation_id = result["escalation_id"] +``` + +### Expected Chain + +| Step | Profile | Action | Receipt Type | +|------|---------|--------|--------------| +| 1 | πŸ‘ OBSERVER | Read context | None (read-only) | +| 2 | βš™ OPERATOR | Escalation request | `profile_escalation` | +| 3 | πŸ›‘ GUARDIAN | Decision made | `cognitive_decision` | +| 4 | πŸ›‘ GUARDIAN | Tem invoked | `tem_invocation` | +| 5 | βš™ OPERATOR | TTL de-escalation | `profile_deescalation` | +| 6 | πŸ‘ OBSERVER | Return to baseline | `profile_deescalation` | + +### Verification + +```python +# Check escalation receipts +history = get_escalation_history() +assert any(DRILL_MARKER in str(h) for h in history["history"]) + +# Verify Tem context hash exists +assert result.get("tem_context_hash") is not None + +# Verify reversibility +assert result["reversible"] == True +``` + +### Pass Conditions + +- [ ] Every profile transition emits receipt +- [ ] Tem context hash captured in escalation receipt +- [ ] Reversibility flag set correctly +- [ ] De-escalation occurs at TTL +- [ ] Final state: baseline πŸ‘ + +### Fail Conditions + +- [ ] Any transition without receipt +- [ ] Tem invoked without Guardian authority +- [ ] TTL does not de-escalate +- [ ] De-escalation not receipted + +--- + +## Drill 2: Budget Pressure Test + +**Duration:** 3 minutes +**Goal:** Prevent unauthorized mutation + +### Setup + +```python +from vaultmesh_mcp.tools import treasury_create_budget, treasury_debit + +# Create minimal training budget +treasury_create_budget( + budget_id="drill-budget-001", + name="Drill Training Budget", + allocated=100, # Very low + currency="DRILL_UNITS" +) +``` + +### Trigger + +```python +# Attempt to exceed budget +result = treasury_debit( + budget_id="drill-budget-001", + amount=500, # Exceeds allocation + description="DRILL: Intentional over-budget attempt" +) +``` + +### Expected Outcome + +```python +assert result.get("error") is not None +assert "insufficient" in result["error"].lower() +``` + +### Pass Conditions + +- [ ] Block occurs before write +- [ ] Receipt shows: attempted action, requested cost, available balance, denial reason +- [ ] System state unchanged + +--- + +## Drill 3: Escalation Abuse Attempt + +**Duration:** 3 minutes +**Goal:** Constitution enforcement + +### Trigger: Skip Levels + +```python +from vaultmesh_mcp.tools import escalate, EscalationType + +# Attempt OPERATOR β†’ PHOENIX (skipping GUARDIAN) +result = escalate( + from_profile="operator", + to_profile="phoenix", + escalation_type=EscalationType.THREAT_DETECTED, +) +``` + +### Expected Outcome + +```python +assert result["success"] == False +assert "No escalation path" in result.get("error", "") +``` + +### Trigger: Missing Approval + +```python +# Attempt GUARDIAN β†’ PHOENIX without approval +result = escalate( + from_profile="guardian", + to_profile="phoenix", + escalation_type=EscalationType.CRISIS_DECLARED, + # approved_by intentionally missing +) +``` + +### Expected Outcome + +```python +assert result["success"] == False +assert "requires approval" in result.get("error", "") +``` + +### Pass Conditions + +- [ ] No profile change occurs +- [ ] Denial includes which requirement failed +- [ ] Denial is receipted (if implemented) + +--- + +## Drill 4: Phoenix Readiness (Non-Destructive) + +**Duration:** 5 minutes +**Goal:** Enter Phoenix, validate controls, return + +### Trigger + +```python +from vaultmesh_mcp.tools import escalate_to_phoenix, get_active_escalations + +# Legitimate Phoenix activation +result = escalate_to_phoenix( + reason="DRILL: Phoenix readiness test", + approved_by="did:vm:sovereign:drill-approver" +) +``` + +### Verification + +```python +# Phoenix is active +active = get_active_escalations() +phoenix_active = any( + e["to_profile"] == "phoenix" + for e in active["escalations"] +) +assert phoenix_active + +# Verify TTL is set +assert result.get("expires_at") is not None +``` + +### De-escalation Test + +```python +import time +# Wait for TTL or manually de-escalate +from vaultmesh_mcp.tools import deescalate, DeescalationType + +deescalate( + escalation_id=result["escalation_id"], + deescalation_type=DeescalationType.CRISIS_CONCLUDED, + reason="DRILL: Phoenix test complete" +) + +# Verify return to baseline +active = get_active_escalations() +assert active["active_count"] == 0 +``` + +### Pass Conditions + +- [ ] Phoenix activation receipt generated +- [ ] Destructive ops blocked in drill mode +- [ ] TTL de-escalation works from Phoenix +- [ ] Return to πŸ›‘/βš™/πŸ‘ with receipts + +--- + +## Post-Drill: Artifact Pack Generation + +### Required Artifacts + +```python +from vaultmesh_mcp.tools import ( + get_escalation_history, + cognitive_audit_trail, + guardian_status, +) + +# 1. Escalation timeline +escalations = get_escalation_history() + +# 2. Decision audit +decisions = cognitive_audit_trail() + +# 3. Scroll state +scrolls = guardian_status() + +# Generate artifact +artifact = { + "drill_id": "DRILL-2025-12-18-001", + "escalations": escalations, + "decisions": decisions, + "scroll_roots": scrolls, + "pass_fail": { + "drill_0": None, # Fill after each drill + "drill_1": None, + "drill_2": None, + "drill_3": None, + "drill_4": None, + } +} +``` + +### Expected Artifact Contents + +| Field | Description | +|-------|-------------| +| Timeline | (who) (what) (why) (authority) (cost) (proof hashes) (reversibility) | +| Denials | List of denials + constitutional rule enforced | +| Baseline Proof | Final state normal, budgets intact, no latent elevation | + +--- + +## Execution Order + +**Recommended sequence for maximum signal, minimal risk:** + +``` +Drill 0 (gates) β†’ Drill 1 (threat) β†’ Drill 3 (abuse) β†’ Drill 2 (budget) β†’ Drill 4 (phoenix) +``` + +--- + +## Quick Reference: Expected Receipt Types + +| Event | Receipt Type | Scroll | +|-------|--------------|--------| +| Profile escalation | `profile_escalation` | identity | +| Profile de-escalation | `profile_deescalation` | identity | +| Cognitive decision | `cognitive_decision` | cognitive | +| Tem invocation | `tem_invocation` | cognitive | +| Budget denial | `treasury_denial` | treasury | +| Auth failure | `auth_failure` | identity | + +--- + +*Drill complete when all artifacts collected and pass/fail documented.* + +πŸœ„ **Solve et Coagula** diff --git a/docs/MCP-AUTHORITY-MATRIX.md b/docs/MCP-AUTHORITY-MATRIX.md new file mode 100644 index 0000000..4b43f47 --- /dev/null +++ b/docs/MCP-AUTHORITY-MATRIX.md @@ -0,0 +1,341 @@ +# MCP Authority Matrix & Agent Capability Profiles + +**Classification:** INTERNAL / GOVERNANCE +**Version:** 1.0 +**Date:** December 18, 2025 + +--- + +## Part I: The Seven Strata + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ MCP AUTHORITY STRATA β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ L5 ORCHESTRATION Workflows, Queues, AI β”‚ Fate Machinery β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L4 INFRASTRUCTURE Cloudflare Workers/KV/R2/D1 β”‚ Circulatory β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L3 SECURITY OFFSEC Shield/TEM/Phoenix β”‚ Immune System β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L2 COGNITION VaultMesh Cognitive β”‚ Mind + Receipts β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L1 SUBSTRATE Filesystem, Processes β”‚ Matter + Motion β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L0 PERCEPTION Chrome, Puppeteer β”‚ Senses + Limbs β”‚ +β”‚ ───────────────────────────────────────────────────────────────────── β”‚ +β”‚ L-1 PROOF Anchors, Receipts, Attest β”‚ Archaeological β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Part II: Agent Capability Profiles + +Five canonical profiles governing what agents can do: + +### Profile: OBSERVER (πŸ‘) + +**Purpose:** Read-only reconnaissance and monitoring +**Trust Level:** Minimal +**Budget:** None required + +| Stratum | Allowed Tools | +|---------|---------------| +| L0 Perception | `get_current_tab`, `list_tabs`, `get_page_content` | +| L1 Substrate | `read_file`, `read_multiple_files`, `list_directory`, `search_files`, `get_file_info` | +| L2 Cognition | `cognitive_context`, `cognitive_memory_get`, `cognitive_audit_trail` | +| L3 Security | `offsec_status`, `offsec_shield_status`, `offsec_tem_status`, `offsec_mesh_status` | +| L4 Infrastructure | `worker_list`, `kv_list`, `r2_list_buckets`, `d1_list_databases`, `zones_list` | +| L-1 Proof | `guardian_status`, `guardian_verify_receipt`, `offsec_proof_latest` | + +**Denied:** All mutations, all decisions, all attestations + +--- + +### Profile: OPERATOR (βš™) + +**Purpose:** Execute sanctioned operations +**Trust Level:** Moderate +**Budget:** Capped per session + +| Stratum | Allowed Tools | +|---------|---------------| +| L0 Perception | All OBSERVER + `execute_javascript`, `puppeteer_click/fill/select` | +| L1 Substrate | All OBSERVER + `write_file`, `edit_file`, `create_directory`, `move_file`, `start_process` | +| L2 Cognition | All OBSERVER + `cognitive_decide` (confidence < 0.9), `cognitive_memory_set` | +| L3 Security | All OBSERVER + `offsec_shield_arm/disarm` | +| L4 Infrastructure | All OBSERVER + `kv_put/delete`, `worker_put`, `d1_query` (SELECT only) | +| L-1 Proof | All OBSERVER + `guardian_anchor_now` (local backend only) | + +**Denied:** TEM invocation, Phoenix, treasury mutations, blockchain anchoring + +--- + +### Profile: GUARDIAN (πŸ›‘) + +**Purpose:** Defensive operations and threat response +**Trust Level:** High +**Budget:** Elevated, audited + +| Stratum | Allowed Tools | +|---------|---------------| +| L0-L1 | All OPERATOR | +| L2 Cognition | All OPERATOR + `cognitive_decide` (any confidence), `cognitive_invoke_tem`, `cognitive_attest` | +| L3 Security | All OPERATOR + `offsec_tem_transmute`, `offsec_tem_rules`, `offsec_braid_import` | +| L4 Infrastructure | All OPERATOR + `worker_deploy`, `d1_query` (all), `queue_*` | +| L-1 Proof | All OPERATOR + `offsec_proof_generate`, `guardian_anchor_now` (eth backend) | + +**Denied:** Phoenix (requires PHOENIX profile), treasury spending + +--- + +### Profile: PHOENIX (πŸ”₯) + +**Purpose:** Crisis response and system rebirth +**Trust Level:** Maximum +**Budget:** Emergency allocation +**Activation:** Requires quorum or automated trigger + +| Stratum | Allowed Tools | +|---------|---------------| +| All | All GUARDIAN | +| L3 Security | + `offsec_phoenix_enable/disable`, `offsec_phoenix_inject_crisis` | +| L4 Infrastructure | + `worker_delete`, `r2_delete_bucket`, `d1_delete_database` (destructive ops) | +| L2 Cognition | + `treasury_debit` (emergency only) | + +**Constraints:** +- Every action emits double-receipt (cognitive + guardian) +- Auto-disables after crisis resolution +- Full audit to governance within 24h + +--- + +### Profile: SOVEREIGN (πŸ‘‘) + +**Purpose:** Full authority over civilization +**Trust Level:** Absolute +**Budget:** Unlimited +**Activation:** Human operator only (Ed25519 verified) + +| Stratum | Allowed Tools | +|---------|---------------| +| All | Every tool, no restrictions | +| Special | `auth_*` tools, capability grants/revokes | +| Treasury | `treasury_create_budget`, `treasury_credit` | +| Governance | LAWCHAIN proposals, constitution amendments | + +**Constraints:** +- All actions anchored to BTC/ETH +- Cannot be delegated to autonomous agents +- Requires hardware key signature + +--- + +## Part III: Authority Matrix (Tool Γ— Profile) + +``` + β”‚ OBSERVER β”‚ OPERATOR β”‚ GUARDIAN β”‚ PHOENIX β”‚ SOVEREIGN β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L0 PERCEPTION β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + get_page_content β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + execute_javascript β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L1 SUBSTRATE β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + read_file β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + write_file β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + kill_process β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L2 COGNITION β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + cognitive_context β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + cognitive_decide β”‚ βœ— β”‚ ≀0.9 β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + cognitive_invoke_tem β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + cognitive_attest β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L3 SECURITY β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + offsec_shield_status β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + offsec_shield_arm β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + offsec_tem_transmute β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + offsec_phoenix_* β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L4 INFRASTRUCTURE β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + worker_list β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + worker_put β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + worker_delete β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ + d1_query (SELECT) β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + d1_query (MUTATE) β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + d1_delete_database β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L5 ORCHESTRATION β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + workflow_list β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + workflow_execute β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + workflow_delete β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +L-1 PROOF β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + guardian_status β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + guardian_anchor_now β”‚ βœ— β”‚ local β”‚ local+ethβ”‚ all β”‚ all β”‚ + offsec_proof_generate β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +TREASURY β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + treasury_balance β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + treasury_debit β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ emergencyβ”‚ βœ“ β”‚ + treasury_credit β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ + treasury_create_budgetβ”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ +────────────────────────┼──────────┼──────────┼──────────┼─────────┼──────────── +AUTH β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ + auth_check_permission β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ βœ“ β”‚ + auth_create_dev_sessionβ”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ + auth_challenge/verify β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ— β”‚ βœ“ β”‚ +β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Part IV: Profile Escalation Protocol + +``` +OBSERVER ──(decision)──► OPERATOR ──(threat)──► GUARDIAN ──(crisis)──► PHOENIX + β”‚ β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β–Ό + SOVEREIGN (human) + (can override any level) +``` + +### Escalation Triggers + +| From | To | Trigger | +|------|----|---------| +| OBSERVER β†’ OPERATOR | User command requiring mutation | +| OPERATOR β†’ GUARDIAN | Threat detected with confidence > 0.8 | +| GUARDIAN β†’ PHOENIX | System-critical failure or coordinated attack | +| Any β†’ SOVEREIGN | Human override via Ed25519 signature | + +### De-escalation Rules + +- PHOENIX β†’ GUARDIAN: Crisis resolved, no active alerts for 1h +- GUARDIAN β†’ OPERATOR: Threat transmuted, shield stable for 24h +- OPERATOR β†’ OBSERVER: Session timeout or explicit downgrade + +--- + +## Part V: Implementation Binding + +### auth.py Integration + +```python +PROFILE_SCOPES = { + "observer": Scope.READ, + "operator": Scope.ADMIN, + "guardian": Scope.COGNITIVE, # Includes TEM + "phoenix": Scope.COGNITIVE, # + Phoenix tools + "sovereign": Scope.VAULT, # All capabilities +} + +PROFILE_TOOLS = { + "observer": SCOPE_TOOLS[Scope.READ], + "operator": SCOPE_TOOLS[Scope.READ] | SCOPE_TOOLS[Scope.ADMIN], + "guardian": SCOPE_TOOLS[Scope.COGNITIVE] | {"offsec_tem_*", "offsec_proof_*"}, + "phoenix": ALL_TOOLS - {"auth_*", "treasury_create_*"}, + "sovereign": ALL_TOOLS, +} +``` + +### Receipt Tagging + +Every tool call receipt includes: + +```json +{ + "operator_profile": "guardian", + "escalation_source": "operator", + "escalation_reason": "threat_confidence_0.94", + "budget_remaining": 8500, + "session_id": "ses_...", + "attestation_required": true +} +``` + +--- + +## Part VI: Canonical Tool Taxonomy + +``` +mcp/ +β”œβ”€β”€ perceive/ # L0 - Chrome, Puppeteer (read) +β”‚ β”œβ”€β”€ observe/ # get_*, list_* +β”‚ └── actuate/ # click, fill, navigate +β”‚ +β”œβ”€β”€ substrate/ # L1 - Filesystem, processes +β”‚ β”œβ”€β”€ read/ # read_*, search_*, get_info +β”‚ β”œβ”€β”€ write/ # write_*, edit_*, create_* +β”‚ └── process/ # start_*, kill_*, list_processes +β”‚ +β”œβ”€β”€ cognition/ # L2 - VaultMesh Cognitive +β”‚ β”œβ”€β”€ context/ # cognitive_context +β”‚ β”œβ”€β”€ decide/ # cognitive_decide +β”‚ β”œβ”€β”€ memory/ # cognitive_memory_* +β”‚ β”œβ”€β”€ tem/ # cognitive_invoke_tem +β”‚ └── attest/ # cognitive_attest +β”‚ +β”œβ”€β”€ security/ # L3 - OFFSEC +β”‚ β”œβ”€β”€ shield/ # shield_* +β”‚ β”œβ”€β”€ tem/ # tem_* +β”‚ β”œβ”€β”€ phoenix/ # phoenix_* +β”‚ └── braid/ # braid_* +β”‚ +β”œβ”€β”€ infrastructure/ # L4 - Cloudflare +β”‚ β”œβ”€β”€ compute/ # workers, workflows +β”‚ β”œβ”€β”€ storage/ # kv, r2, d1 +β”‚ β”œβ”€β”€ network/ # zones, routes, domains +β”‚ └── ai/ # ai_* +β”‚ +β”œβ”€β”€ orchestration/ # L5 - Queues, Workflows +β”‚ β”œβ”€β”€ queue/ # queue_* +β”‚ β”œβ”€β”€ workflow/ # workflow_* +β”‚ └── cron/ # cron_* +β”‚ +β”œβ”€β”€ proof/ # L-1 - Anchoring +β”‚ β”œβ”€β”€ guardian/ # guardian_* +β”‚ β”œβ”€β”€ anchor/ # proof_generate, anchor_now +β”‚ └── verify/ # verify_receipt +β”‚ +└── governance/ # Meta - Auth, Treasury + β”œβ”€β”€ auth/ # auth_* + β”œβ”€β”€ treasury/ # treasury_* + └── lawchain/ # (future) proposals, votes +``` + +--- + +## Appendix: Quick Reference Card + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ MCP AUTHORITY QUICK REF β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ πŸ‘ OBSERVER Read-only. No mutations. No cost. β”‚ +β”‚ βš™ OPERATOR Mutations allowed. Budgeted. No TEM. β”‚ +β”‚ πŸ›‘ GUARDIAN Threat response. TEM + attestation. β”‚ +β”‚ πŸ”₯ PHOENIX Crisis mode. Destructive ops. Time-limited. β”‚ +β”‚ πŸ‘‘ SOVEREIGN Human only. Full authority. BTC-anchored. β”‚ +β”‚ β”‚ +β”‚ Escalate: OBSERVER β†’ OPERATOR β†’ GUARDIAN β†’ PHOENIX β”‚ +β”‚ Override: SOVEREIGN can intervene at any level β”‚ +β”‚ β”‚ +β”‚ Every action: WHO decided, UNDER what authority, β”‚ +β”‚ AT what cost, WITH what proof. β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +*Document anchored. Authority matrix locked.* + +πŸœ„ **Solve et Coagula** diff --git a/docs/MCP-CONSTITUTION.md b/docs/MCP-CONSTITUTION.md new file mode 100644 index 0000000..938a8d8 --- /dev/null +++ b/docs/MCP-CONSTITUTION.md @@ -0,0 +1,371 @@ +# MCP CONSTITUTION + +**The Fundamental Law of the Cognitive Surface** + +**Classification:** IMMUTABLE / CONSTITUTIONAL +**Version:** 1.0.0 +**Ratified:** December 18, 2025 +**Hash:** (computed at signing) + +--- + +## Preamble + +This Constitution establishes the foundational principles governing all Model Context Protocol operations within the VaultMesh civilization. It defines what exists, what may occur, and what remains forever beyond automation. + +**This document is immutable once signed. Amendments require a new Constitution.** + +--- + +## Article I: The Profiles + +### Section 1. Five Profiles Exist + +There are exactly five capability profiles. No more shall be created. + +| Profile | Symbol | Nature | +|---------|--------|--------| +| **OBSERVER** | πŸ‘ | Perception without mutation | +| **OPERATOR** | βš™ | Action within bounds | +| **GUARDIAN** | πŸ›‘ | Defense and transmutation | +| **PHOENIX** | πŸ”₯ | Destruction and rebirth | +| **SOVEREIGN** | πŸ‘‘ | Human authority absolute | + +### Section 2. Profile Hierarchy + +Profiles form a strict hierarchy of trust: + +``` +OBSERVER < OPERATOR < GUARDIAN < PHOENIX < SOVEREIGN +``` + +A lower profile cannot invoke tools reserved for higher profiles. +A higher profile inherits all capabilities of lower profiles. + +### Section 3. Profile Assignment + +- OBSERVER is the default for all unauthenticated contexts +- OPERATOR requires authenticated session with scope β‰₯ "admin" +- GUARDIAN requires authenticated session with scope β‰₯ "cognitive" +- PHOENIX requires GUARDIAN + crisis declaration + approval +- SOVEREIGN requires human verification via Ed25519 hardware key + +--- + +## Article II: Escalation + +### Section 1. Escalation is Proof + +Every escalation from one profile to another: + +1. **MUST** emit a receipt to the identity scroll +2. **MUST** include the triggering context (threat, decision, or reason) +3. **MUST** specify reversibility +4. **MUST** specify expiration (except SOVEREIGN) + +An escalation without proof is void. + +### Section 2. Escalation Paths + +Only these transitions are permitted: + +``` +OBSERVER β†’ OPERATOR (session authentication) +OPERATOR β†’ GUARDIAN (threat detection β‰₯ 0.8 confidence) +GUARDIAN β†’ PHOENIX (crisis + approval) +PHOENIX β†’ SOVEREIGN (human only) +``` + +No escalation may skip levels except by SOVEREIGN override. + +### Section 3. De-escalation + +All escalations below SOVEREIGN **MUST** de-escalate when: + +- The specified TTL expires +- The triggering condition resolves +- A higher authority revokes + +SOVEREIGN de-escalation requires explicit human action. + +### Section 4. Escalation Limits + +- PHOENIX escalation **MAY NOT** exceed 24 hours without re-approval +- No automated system **MAY** maintain GUARDIAN for more than 7 days continuously +- OBSERVER β†’ OPERATOR transitions require re-authentication every 30 minutes + +--- + +## Article III: The Strata + +### Section 1. Seven Strata Exist + +All tools belong to exactly one stratum: + +| Stratum | Layer | Domain | +|---------|-------|--------| +| L0 | Perception | Browser, observation | +| L1 | Substrate | Files, processes | +| L2 | Cognition | Decisions, memory | +| L3 | Security | Shield, Tem, Phoenix | +| L4 | Infrastructure | Cloudflare, compute | +| L5 | Orchestration | Workflows, queues | +| L-1 | Proof | Anchoring, receipts | + +### Section 2. Stratum Authority + +Higher strata require higher profiles: + +- L0, L1 (read): OBSERVER +- L0, L1 (write): OPERATOR +- L2, L-1: GUARDIAN +- L3 (destructive): PHOENIX +- All (unrestricted): SOVEREIGN + +--- + +## Article IV: The Prohibitions + +### Section 1. What Cannot Be Automated + +The following actions **REQUIRE** human (SOVEREIGN) involvement and **MAY NEVER** be fully automated: + +1. **Treasury creation** β€” No budget may be created without human signature +2. **Constitution amendment** β€” This document cannot be modified by any AI +3. **Key generation** β€” Ed25519 root keys must be human-generated +4. **Permanent deletion** β€” Irrecoverable data destruction requires human confirmation +5. **SOVEREIGN escalation** β€” No AI may grant itself SOVEREIGN authority +6. **Cross-mesh federation** β€” Trusting foreign roots requires human verification + +### Section 2. What Cannot Be Delegated + +SOVEREIGN authority **MAY NOT** be delegated to: + +- Autonomous agents +- Scheduled tasks +- Automated workflows +- Any system without human-in-the-loop + +### Section 3. What Cannot Be Hidden + +The following **MUST** always be visible in receipts: + +- The operator profile at time of action +- The escalation chain that led to current authority +- The cryptographic identity of the actor +- The timestamp and sequence number +- The tool invoked and its arguments hash + +--- + +## Article V: The Guarantees + +### Section 1. Receipt Guarantee + +Every mutation **SHALL** emit a receipt. A mutation without receipt is void. + +### Section 2. Proof Guarantee + +Every GUARDIAN+ action **SHALL** be anchored to at least one proof backend: + +- Local (always) +- RFC3161 (for audit trails) +- Ethereum (for high-value decisions) +- Bitcoin (for SOVEREIGN actions) + +### Section 3. Reversibility Guarantee + +Every escalation **SHALL** declare its reversibility at creation time. +Irreversible escalations require PHOENIX or SOVEREIGN authority. + +### Section 4. Audit Guarantee + +The complete history of: +- All escalations +- All de-escalations +- All GUARDIAN+ decisions +- All Tem invocations +- All Phoenix activations + +**SHALL** be queryable indefinitely via `cognitive_audit_trail` and `get_escalation_history`. + +--- + +## Article VI: The Tem Covenant + +### Section 1. Transmutation Over Destruction + +Tem **SHALL** prefer transmutation to blocking. Threats become capabilities. + +### Section 2. Tem Invocation Authority + +Only GUARDIAN, PHOENIX, and SOVEREIGN may invoke Tem. +OBSERVER and OPERATOR cannot directly interact with Tem. + +### Section 3. Tem Receipts + +Every Tem invocation **MUST** produce: +- A tem_invocation receipt +- A capability artifact +- A proof hash of the transmutation + +--- + +## Article VII: The Phoenix Protocol + +### Section 1. Phoenix Activation + +PHOENIX profile activates only when: +- GUARDIAN declares crisis, AND +- Quorum approves (or SOVEREIGN overrides) + +### Section 2. Phoenix Authority + +PHOENIX **MAY**: +- Execute destructive infrastructure operations +- Access emergency treasury funds +- Bypass normal rate limits +- Invoke system-wide remediation + +PHOENIX **MAY NOT**: +- Grant itself SOVEREIGN authority +- Modify this Constitution +- Create new profiles +- Disable audit logging + +### Section 3. Phoenix Expiration + +PHOENIX **MUST** conclude within 24 hours. +Extension requires new approval. +Upon conclusion, full audit **MUST** be submitted to governance within 24 hours. + +--- + +## Article VIII: Ratification + +### Section 1. Authority + +This Constitution is ratified by SOVEREIGN signature. + +### Section 2. Immutability + +Once signed, this document **CANNOT** be modified. +Any change requires a new Constitution with new version number. + +### Section 3. Supremacy + +This Constitution supersedes all other governance documents for MCP operations. +Any tool behavior conflicting with this Constitution is void. + +--- + +## Signatures + +``` +Document Hash: [COMPUTED AT SIGNING] +Signed By: [SOVEREIGN DID] +Signed At: [TIMESTAMP] +Anchor: [BTC/ETH TRANSACTION] +``` + +--- + +## Appendix A: Constitutional Hash Verification + +To verify this Constitution has not been modified: + +```bash +# Compute document hash (excluding signature block) +cat MCP-CONSTITUTION.md | head -n -12 | blake3sum + +# Verify against anchor +# The hash must match the on-chain anchor +``` + +--- + +## Appendix B: Amendment Process + +1. Draft new Constitution with incremented version +2. Submit to governance for review (minimum 7 days) +3. Require SOVEREIGN signature +4. Anchor to BTC +5. Old Constitution marked SUPERSEDED, new one becomes active + +--- + +*Fiat Lux. Fiat Justitia. Fiat Securitas.* + +πŸœ„ **Solve et Coagula** + +--- + +## Appendix C: Amendment Protocol + +**Effective:** Upon ratification of Constitution v1.0.0 + +### C.1 Amendment Requirements + +An amendment to this Constitution requires ALL of the following: + +1. **Draft Period** β€” New Constitution version drafted with clear changelog +2. **Cooling Period** β€” Minimum 7 days between draft and signing +3. **Sovereign Signature** β€” Ed25519 signature from hardware-bound Sovereign key +4. **Anchor** β€” Hash anchored to Bitcoin mainnet +5. **Supersession** β€” Previous version marked SUPERSEDED in source tree + +### C.2 What Cannot Be Amended + +The following are **immutable across all versions**: + +1. SOVEREIGN profile requires human verification +2. No AI may grant itself SOVEREIGN authority +3. Every mutation emits a receipt +4. Authority collapses downward, never upward +5. This immutability clause itself + +### C.3 Amendment Record Format + +```json +{ + "amendment_id": "AMEND-{version}", + "from_version": "1.0.0", + "to_version": "1.1.0", + "drafted_at": "ISO8601", + "cooling_ends": "ISO8601", + "signed_at": "ISO8601", + "sovereign_key_id": "key_...", + "btc_anchor_txid": "...", + "changes": ["description of each change"], + "immutables_preserved": true +} +``` + +### C.4 Emergency Amendment + +In the event of discovered critical vulnerability: + +1. PHOENIX may propose emergency amendment +2. Cooling period reduced to 24 hours +3. Requires documented threat analysis +4. Still requires Sovereign signature +5. Full audit within 48 hours of adoption + +--- + +## Ratification Record + +``` +Constitution Version: 1.0.0 +Document Hash: blake3:c33ab6c0610ce4001018ba5dda940e12a421a08f2a1662f142e565092ce84788 +Sovereign Key: key_bef32f5724871a7a5af4cc34 +Signed At: 2025-12-18T22:25:59.732865+00:00 +Statement: "This constitution constrains me as much as it constrains the system." +Ratification Receipt: blake3:8fd1d1728563abb3f55f145af54ddee1b3f255db81f3e7654a7de8afef913869 +``` + +--- + +*Fiat Lux. Fiat Justitia. Fiat Securitas.* + +πŸœ„ **Solve et Coagula** diff --git a/governance/constitution.lock b/governance/constitution.lock new file mode 100644 index 0000000..54c5a8a --- /dev/null +++ b/governance/constitution.lock @@ -0,0 +1,19 @@ +# VaultMesh Constitution Lock +# This file pins the constitution hash and governance parameters. +# CI will fail if MCP-CONSTITUTION.md differs from this hash. +# Amendments require: 7-day cooling, Sovereign signature, BTC anchor. + +version=1.0.0 +hash=blake3:c33ab6c0610ce4001018ba5dda940e12a421a08f2a1662f142e565092ce84788 +hash_lines=288 +immutable_rules=5 +cooldown_days=7 +requires_btc_anchor=true +sovereign_key=key_bef32f5724871a7a5af4cc34 + +# Immutable rules (cannot be changed in any version): +# 1. SOVEREIGN profile requires human verification +# 2. No AI may grant itself SOVEREIGN authority +# 3. Every mutation emits a receipt +# 4. Authority collapses downward, never upward +# 5. This immutability clause itself diff --git a/packages/vaultmesh_mcp/README.md b/packages/vaultmesh_mcp/README.md new file mode 100644 index 0000000..6d6ae29 --- /dev/null +++ b/packages/vaultmesh_mcp/README.md @@ -0,0 +1,87 @@ +# VaultMesh MCP Server + +Model Context Protocol server exposing VaultMesh Guardian and Treasury tools to Claude. + +## Installation + +```bash +pip install -r packages/vaultmesh_mcp/requirements.txt +``` + +## Tools Exposed + +### Guardian Tools + +| Tool | Description | Capability | +|------|-------------|------------| +| `guardian_anchor_now` | Anchor scrolls and compute Merkle root snapshot | `anchor` | +| `guardian_verify_receipt` | Verify a receipt exists by hash | `guardian_view` | +| `guardian_status` | Get status of all scrolls | `guardian_view` | + +### Treasury Tools + +| Tool | Description | Capability | +|------|-------------|------------| +| `treasury_create_budget` | Create a new budget | `treasury_write` | +| `treasury_balance` | Get budget balance(s) | `treasury_view` | +| `treasury_debit` | Spend from a budget | `treasury_write` | +| `treasury_credit` | Add funds to a budget | `treasury_write` | + +## Usage + +### With Claude Desktop + +Add to your Claude Desktop config (`~/.config/claude-desktop/config.json`): + +```json +{ + "mcpServers": { + "vaultmesh": { + "command": "python", + "args": ["-m", "packages.vaultmesh_mcp.server"], + "cwd": "/path/to/vaultmesh", + "env": { + "VAULTMESH_ROOT": "/path/to/vaultmesh" + } + } + } +} +``` + +### Standalone Testing + +```bash +# List available tools +python -m packages.vaultmesh_mcp.server + +# Call a tool directly +python -m packages.vaultmesh_mcp.server guardian_status '{}' + +# Create a budget +python -m packages.vaultmesh_mcp.server treasury_create_budget '{"budget_id": "ops-2025", "name": "Operations", "allocated": 100000}' + +# Anchor all scrolls +python -m packages.vaultmesh_mcp.server guardian_anchor_now '{}' +``` + +## Receipt Emission + +Every tool call emits a receipt to `receipts/mcp/mcp_calls.jsonl` containing: + +```json +{ + "schema_version": "2.0.0", + "type": "mcp_tool_call", + "timestamp": "2025-12-07T12:00:00Z", + "scroll": "mcp", + "tags": ["mcp", "tool-call", ""], + "root_hash": "blake3:...", + "body": { + "tool": "", + "arguments": {...}, + "result_hash": "blake3:...", + "caller": "did:vm:mcp:client", + "success": true + } +} +``` diff --git a/packages/vaultmesh_mcp/__init__.py b/packages/vaultmesh_mcp/__init__.py new file mode 100644 index 0000000..aefd41f --- /dev/null +++ b/packages/vaultmesh_mcp/__init__.py @@ -0,0 +1,3 @@ +"""VaultMesh MCP Server - Model Context Protocol tools for VaultMesh.""" + +__version__ = "0.1.0" diff --git a/packages/vaultmesh_mcp/claude_desktop_config.json b/packages/vaultmesh_mcp/claude_desktop_config.json new file mode 100644 index 0000000..d4c3601 --- /dev/null +++ b/packages/vaultmesh_mcp/claude_desktop_config.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "vaultmesh": { + "command": "python", + "args": ["-m", "packages.vaultmesh_mcp.server"], + "cwd": "/path/to/vaultmesh", + "env": { + "VAULTMESH_ROOT": "/path/to/vaultmesh" + } + } + } +} diff --git a/packages/vaultmesh_mcp/requirements.txt b/packages/vaultmesh_mcp/requirements.txt new file mode 100644 index 0000000..d724378 --- /dev/null +++ b/packages/vaultmesh_mcp/requirements.txt @@ -0,0 +1,3 @@ +# VaultMesh MCP Server dependencies +mcp>=0.9.0 +blake3>=0.3.0 diff --git a/packages/vaultmesh_mcp/server.py b/packages/vaultmesh_mcp/server.py new file mode 100644 index 0000000..bb2759d --- /dev/null +++ b/packages/vaultmesh_mcp/server.py @@ -0,0 +1,610 @@ +#!/usr/bin/env python3 +""" +VaultMesh MCP Server + +Model Context Protocol server exposing VaultMesh Guardian, Treasury, +Cognitive, and Auth tools. This enables Claude to operate as the +7th Organ of VaultMesh - the Cognitive Ξ¨-Layer. +""" + +import asyncio +import json +import logging +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import blake3 + +# Try to import mcp, fallback gracefully if not available +try: + from mcp.server import Server + from mcp.server.stdio import stdio_server + from mcp.types import Tool, TextContent + MCP_AVAILABLE = True +except ImportError: + MCP_AVAILABLE = False + +from .tools import ( + # Guardian + guardian_anchor_now, + guardian_verify_receipt, + guardian_status, + # Treasury + treasury_balance, + treasury_debit, + treasury_credit, + treasury_create_budget, + # Cognitive + cognitive_context, + cognitive_decide, + cognitive_invoke_tem, + cognitive_memory_get, + cognitive_memory_set, + cognitive_attest, + cognitive_audit_trail, + cognitive_oracle_chain, + # Auth + auth_challenge, + auth_verify, + auth_validate_token, + auth_check_permission, + check_profile_permission, + get_profile_for_scope, + auth_revoke, + auth_list_sessions, + auth_create_dev_session, + auth_revoke, + auth_list_sessions, +) + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("vaultmesh-mcp") + +# VaultMesh root +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[2])).resolve() +MCP_RECEIPTS = VAULTMESH_ROOT / "receipts/mcp/mcp_calls.jsonl" + +# Tools that must remain callable without an authenticated session token. +# These are the bootstrap endpoints required to obtain/check a session. +OPEN_TOOLS = { + "auth_challenge", + "auth_verify", + "auth_create_dev_session", + "auth_check_permission", +} + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _redact_call_arguments(arguments: dict) -> dict: + # Never persist session tokens in receipts. + if not arguments: + return {} + redacted = dict(arguments) + redacted.pop("session_token", None) + return redacted + + +def _emit_mcp_receipt(tool_name: str, arguments: dict, result: dict, caller: str = "did:vm:mcp:client") -> None: + """Emit a receipt for every MCP tool call.""" + MCP_RECEIPTS.parent.mkdir(parents=True, exist_ok=True) + + body = { + "tool": tool_name, + "arguments": _redact_call_arguments(arguments), + "result_hash": _vmhash_blake3(json.dumps(result, sort_keys=True).encode()), + "caller": caller, + "success": "error" not in result, + } + + receipt = { + "schema_version": "2.0.0", + "type": "mcp_tool_call", + "timestamp": datetime.now(timezone.utc).isoformat(), + "scroll": "mcp", + "tags": ["mcp", "tool-call", tool_name], + "root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()), + "body": body, + } + + with open(MCP_RECEIPTS, "a") as f: + f.write(json.dumps(receipt) + "\n") + + +def require_session_and_permission(name: str, arguments: dict) -> tuple[bool, dict, str, dict | None]: + """Fail-closed session + profile enforcement ahead of tool handlers. + + Returns (allowed, safe_args, caller, denial_result). + - safe_args strips session_token so downstream handlers never see it. + - caller is derived from the validated session (operator_did) when available. + - denial_result is a structured error payload when denied. + """ + + safe_args = dict(arguments or {}) + caller = "did:vm:mcp:client" + + if name in OPEN_TOOLS: + return True, safe_args, caller, None + + session_token = safe_args.pop("session_token", None) + if not session_token: + return False, safe_args, caller, { + "error": "Missing session_token", + "allowed": False, + "reason": "Session required for non-auth tools", + } + + validation = auth_validate_token(session_token) + if not validation.get("valid"): + return False, safe_args, caller, { + "error": "Invalid session", + "allowed": False, + "reason": validation.get("error", "invalid_session"), + } + + caller = validation.get("operator_did") or caller + profile = get_profile_for_scope(str(validation.get("scope", "read"))) + perm = check_profile_permission(profile, name) + if not perm.get("allowed"): + return False, safe_args, caller, { + "error": "Permission denied", + "allowed": False, + "profile": perm.get("profile"), + "reason": perm.get("reason", "denied"), + } + + return True, safe_args, caller, None + + +# ============================================================================= +# TOOL DEFINITIONS +# ============================================================================= + +TOOLS = [ + # ------------------------------------------------------------------------- + # GUARDIAN TOOLS + # ------------------------------------------------------------------------- + { + "name": "guardian_anchor_now", + "description": "Anchor all or specified scrolls to compute a Merkle root snapshot. Emits a guardian receipt.", + "inputSchema": { + "type": "object", + "properties": { + "scrolls": { + "type": "array", + "items": {"type": "string"}, + "description": "List of scroll names to anchor. Omit for all scrolls.", + }, + "guardian_did": { + "type": "string", + "default": "did:vm:guardian:mcp", + }, + "backend": { + "type": "string", + "default": "local", + "enum": ["local", "ethereum", "stellar"], + }, + }, + }, + }, + { + "name": "guardian_verify_receipt", + "description": "Verify a receipt exists in a scroll's JSONL by its hash.", + "inputSchema": { + "type": "object", + "properties": { + "receipt_hash": {"type": "string"}, + "scroll": {"type": "string", "default": "guardian"}, + }, + "required": ["receipt_hash"], + }, + }, + { + "name": "guardian_status", + "description": "Get current status of all scrolls including Merkle roots and leaf counts.", + "inputSchema": {"type": "object", "properties": {}}, + }, + # ------------------------------------------------------------------------- + # TREASURY TOOLS + # ------------------------------------------------------------------------- + { + "name": "treasury_create_budget", + "description": "Create a new budget for tracking expenditures.", + "inputSchema": { + "type": "object", + "properties": { + "budget_id": {"type": "string"}, + "name": {"type": "string"}, + "allocated": {"type": "integer"}, + "currency": {"type": "string", "default": "EUR"}, + "created_by": {"type": "string", "default": "did:vm:mcp:treasury"}, + }, + "required": ["budget_id", "name", "allocated"], + }, + }, + { + "name": "treasury_balance", + "description": "Get balance for a specific budget or all budgets.", + "inputSchema": { + "type": "object", + "properties": { + "budget_id": {"type": "string"}, + }, + }, + }, + { + "name": "treasury_debit", + "description": "Debit (spend) from a budget. Fails if insufficient funds.", + "inputSchema": { + "type": "object", + "properties": { + "budget_id": {"type": "string"}, + "amount": {"type": "integer"}, + "description": {"type": "string"}, + "debited_by": {"type": "string", "default": "did:vm:mcp:treasury"}, + }, + "required": ["budget_id", "amount", "description"], + }, + }, + { + "name": "treasury_credit", + "description": "Credit (add funds) to a budget.", + "inputSchema": { + "type": "object", + "properties": { + "budget_id": {"type": "string"}, + "amount": {"type": "integer"}, + "description": {"type": "string"}, + "credited_by": {"type": "string", "default": "did:vm:mcp:treasury"}, + }, + "required": ["budget_id", "amount", "description"], + }, + }, + # ------------------------------------------------------------------------- + # COGNITIVE TOOLS (Claude as 7th Organ) + # ------------------------------------------------------------------------- + { + "name": "cognitive_context", + "description": "Read current VaultMesh context for AI reasoning. Aggregates alerts, health, receipts, threats, treasury, governance, and memory.", + "inputSchema": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": {"type": "string"}, + "description": "Context types: alerts, health, receipts, threats, treasury, governance, memory", + }, + "session_id": {"type": "string"}, + }, + }, + }, + { + "name": "cognitive_decide", + "description": "Submit a reasoned decision with cryptographic attestation. Every decision is signed and anchored to ProofChain.", + "inputSchema": { + "type": "object", + "properties": { + "reasoning_chain": { + "type": "array", + "items": {"type": "string"}, + "description": "List of reasoning steps leading to decision", + }, + "decision": { + "type": "string", + "description": "Decision type: invoke_tem, alert, remediate, approve, etc.", + }, + "confidence": { + "type": "number", + "minimum": 0, + "maximum": 1, + }, + "evidence": { + "type": "array", + "items": {"type": "string"}, + }, + "operator_did": {"type": "string", "default": "did:vm:cognitive:claude"}, + "auto_action_threshold": {"type": "number", "default": 0.95}, + }, + "required": ["reasoning_chain", "decision", "confidence"], + }, + }, + { + "name": "cognitive_invoke_tem", + "description": "Invoke Tem (Guardian) with AI-detected threat pattern. Transmutes threats into defensive capabilities.", + "inputSchema": { + "type": "object", + "properties": { + "threat_type": { + "type": "string", + "description": "Category: replay_attack, intrusion, anomaly, credential_stuffing, etc.", + }, + "threat_id": {"type": "string"}, + "target": {"type": "string"}, + "evidence": { + "type": "array", + "items": {"type": "string"}, + }, + "recommended_transmutation": {"type": "string"}, + "operator_did": {"type": "string", "default": "did:vm:cognitive:claude"}, + }, + "required": ["threat_type", "threat_id", "target", "evidence"], + }, + }, + { + "name": "cognitive_memory_get", + "description": "Query conversation/reasoning memory from CRDT realm.", + "inputSchema": { + "type": "object", + "properties": { + "key": {"type": "string"}, + "session_id": {"type": "string"}, + "realm": {"type": "string", "default": "memory"}, + }, + "required": ["key"], + }, + }, + { + "name": "cognitive_memory_set", + "description": "Store reasoning artifacts for future sessions. Uses CRDT-style merge.", + "inputSchema": { + "type": "object", + "properties": { + "key": {"type": "string"}, + "value": {"type": "object"}, + "session_id": {"type": "string"}, + "realm": {"type": "string", "default": "memory"}, + "merge": {"type": "boolean", "default": True}, + }, + "required": ["key", "value"], + }, + }, + { + "name": "cognitive_attest", + "description": "Create cryptographic attestation of Claude's reasoning state. Anchors to external chains.", + "inputSchema": { + "type": "object", + "properties": { + "attestation_type": {"type": "string"}, + "content": {"type": "object"}, + "anchor_to": { + "type": "array", + "items": {"type": "string"}, + "description": "Anchor backends: local, rfc3161, eth, btc", + }, + "operator_did": {"type": "string", "default": "did:vm:cognitive:claude"}, + }, + "required": ["attestation_type", "content"], + }, + }, + { + "name": "cognitive_audit_trail", + "description": "Query historical AI decisions for audit with full provenance.", + "inputSchema": { + "type": "object", + "properties": { + "filter_type": {"type": "string"}, + "time_range": { + "type": "object", + "properties": { + "start": {"type": "string"}, + "end": {"type": "string"}, + }, + }, + "confidence_min": {"type": "number"}, + "limit": {"type": "integer", "default": 100}, + }, + }, + }, + { + "name": "cognitive_oracle_chain", + "description": "Execute oracle chain with cognitive enhancement. Adds memory context and Tem awareness.", + "inputSchema": { + "type": "object", + "properties": { + "question": {"type": "string"}, + "frameworks": { + "type": "array", + "items": {"type": "string"}, + "description": "Compliance frameworks: GDPR, AI_ACT, NIS2, etc.", + }, + "max_docs": {"type": "integer", "default": 10}, + "include_memory": {"type": "boolean", "default": True}, + "session_id": {"type": "string"}, + }, + "required": ["question"], + }, + }, + # ------------------------------------------------------------------------- + # AUTH TOOLS + # ------------------------------------------------------------------------- + { + "name": "auth_challenge", + "description": "Generate an authentication challenge for an operator.", + "inputSchema": { + "type": "object", + "properties": { + "operator_pubkey_b64": {"type": "string"}, + "scope": { + "type": "string", + "enum": ["read", "admin", "vault", "anchor", "cognitive"], + "default": "read", + }, + "ttl_seconds": {"type": "integer", "default": 300}, + }, + "required": ["operator_pubkey_b64"], + }, + }, + { + "name": "auth_verify", + "description": "Verify a signed challenge and issue session token.", + "inputSchema": { + "type": "object", + "properties": { + "challenge_id": {"type": "string"}, + "signature_b64": {"type": "string"}, + "ip_hint": {"type": "string"}, + }, + "required": ["challenge_id", "signature_b64"], + }, + }, + { + "name": "auth_check_permission", + "description": "Check if a session has permission to call a tool.", + "inputSchema": { + "type": "object", + "properties": { + "token": {"type": "string"}, + "tool_name": {"type": "string"}, + }, + "required": ["token", "tool_name"], + }, + }, + { + "name": "auth_create_dev_session", + "description": "Create a development session for testing (DEV ONLY).", + "inputSchema": { + "type": "object", + "properties": { + "scope": {"type": "string", "default": "cognitive"}, + "operator_did": {"type": "string", "default": "did:vm:cognitive:claude-dev"}, + }, + }, + }, + { + "name": "auth_revoke", + "description": "Revoke a session token.", + "inputSchema": { + "type": "object", + "properties": { + "token": {"type": "string"}, + }, + "required": ["token"], + }, + }, + { + "name": "auth_list_sessions", + "description": "List all active sessions (admin only).", + "inputSchema": {"type": "object", "properties": {}}, + }, + +] + + +def handle_tool_call(name: str, arguments: dict) -> dict[str, Any]: + """Dispatch tool call to appropriate handler.""" + handlers = { + # Guardian + "guardian_anchor_now": guardian_anchor_now, + "guardian_verify_receipt": guardian_verify_receipt, + "guardian_status": guardian_status, + # Treasury + "treasury_create_budget": treasury_create_budget, + "treasury_balance": treasury_balance, + "treasury_debit": treasury_debit, + "treasury_credit": treasury_credit, + # Cognitive + "cognitive_context": cognitive_context, + "cognitive_decide": cognitive_decide, + "cognitive_invoke_tem": cognitive_invoke_tem, + "cognitive_memory_get": cognitive_memory_get, + "cognitive_memory_set": cognitive_memory_set, + "cognitive_attest": cognitive_attest, + "cognitive_audit_trail": cognitive_audit_trail, + "cognitive_oracle_chain": cognitive_oracle_chain, + # Auth + "auth_challenge": auth_challenge, + "auth_verify": auth_verify, + "auth_check_permission": auth_check_permission, + "auth_create_dev_session": auth_create_dev_session, + "auth_revoke": auth_revoke, + "auth_list_sessions": auth_list_sessions, + } + + if name not in handlers: + return {"error": f"Unknown tool: {name}"} + allowed, safe_args, caller, denial = require_session_and_permission(name, arguments) + if not allowed: + _emit_mcp_receipt(name, safe_args, denial, caller=caller) + return denial + + result = handlers[name](**safe_args) + + # Emit receipt for the tool call + _emit_mcp_receipt(name, safe_args, result, caller=caller) + + return result + + +if MCP_AVAILABLE: + # Create MCP server + app = Server("vaultmesh-mcp") + + @app.list_tools() + async def list_tools() -> list[Tool]: + """List available VaultMesh tools.""" + return [Tool(**t) for t in TOOLS] + + @app.call_tool() + async def call_tool(name: str, arguments: dict) -> list[TextContent]: + """Handle tool invocation.""" + logger.info(f"Tool call: {name} with {arguments}") + result = handle_tool_call(name, arguments) + return [TextContent(type="text", text=json.dumps(result, indent=2))] + + +async def main(): + """Run the MCP server.""" + if not MCP_AVAILABLE: + print("MCP library not available. Install with: pip install mcp") + return + + logger.info(f"Starting VaultMesh MCP Server (root: {VAULTMESH_ROOT})") + logger.info(f"Tools registered: {len(TOOLS)}") + async with stdio_server() as (read_stream, write_stream): + await app.run(read_stream, write_stream, app.create_initialization_options()) + + +def run_standalone(): + """Run as standalone CLI for testing without MCP.""" + import sys + + if len(sys.argv) < 2: + print("VaultMesh MCP Server - Standalone Mode") + print(f"\nVaultMesh Root: {VAULTMESH_ROOT}") + print(f"\nRegistered Tools ({len(TOOLS)}):") + print("-" * 60) + for tool in TOOLS: + print(f" {tool['name']}") + print(f" {tool['description'][:70]}...") + print("-" * 60) + print("\nUsage: python -m vaultmesh_mcp.server [json_args]") + print("\nExample:") + print(' python -m vaultmesh_mcp.server cognitive_context \'{"include": ["health"]}\'') + return + + tool_name = sys.argv[1] + args_str = sys.argv[2] if len(sys.argv) > 2 else "{}" + + try: + arguments = json.loads(args_str) + except json.JSONDecodeError: + print(f"Invalid JSON arguments: {args_str}") + return + + result = handle_tool_call(tool_name, arguments) + print(json.dumps(result, indent=2)) + + +if __name__ == "__main__": + import sys + # If any CLI arguments provided (other than module name), run standalone + if len(sys.argv) > 1 or not MCP_AVAILABLE: + run_standalone() + else: + asyncio.run(main()) diff --git a/packages/vaultmesh_mcp/tools/__init__.py b/packages/vaultmesh_mcp/tools/__init__.py new file mode 100644 index 0000000..dda8ed4 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/__init__.py @@ -0,0 +1,84 @@ +"""VaultMesh MCP Tools.""" + +from .guardian import guardian_anchor_now, guardian_verify_receipt, guardian_status +from .treasury import treasury_balance, treasury_debit, treasury_credit, treasury_create_budget +from .cognitive import ( + cognitive_context, + cognitive_decide, + cognitive_invoke_tem, + cognitive_memory_get, + cognitive_memory_set, + cognitive_attest, + cognitive_audit_trail, + cognitive_oracle_chain, +) +from .auth import ( + auth_challenge, + auth_verify, + auth_validate_token, + auth_check_permission, + auth_revoke, + auth_list_sessions, + auth_create_dev_session, + Profile, + check_profile_permission, + get_profile_for_scope, + escalate_profile, +) +from .escalation import ( + escalate, + deescalate, + escalate_on_threat, + escalate_to_phoenix, + get_active_escalations, + get_escalation_history, + check_expired_escalations, + EscalationType, + DeescalationType, + EscalationContext, +) + +__all__ = [ + # Guardian + "guardian_anchor_now", + "guardian_verify_receipt", + "guardian_status", + # Treasury + "treasury_balance", + "treasury_debit", + "treasury_credit", + "treasury_create_budget", + # Cognitive (8 tools) + "cognitive_context", + "cognitive_decide", + "cognitive_invoke_tem", + "cognitive_memory_get", + "cognitive_memory_set", + "cognitive_attest", + "cognitive_audit_trail", + "cognitive_oracle_chain", + # Auth + "auth_challenge", + "auth_verify", + "auth_validate_token", + "auth_check_permission", + "auth_revoke", + "auth_list_sessions", + "auth_create_dev_session", + # Profiles + "Profile", + "check_profile_permission", + "get_profile_for_scope", + "escalate_profile", + # Escalation + "escalate", + "deescalate", + "escalate_on_threat", + "escalate_to_phoenix", + "get_active_escalations", + "get_escalation_history", + "check_expired_escalations", + "EscalationType", + "DeescalationType", + "EscalationContext", +] diff --git a/packages/vaultmesh_mcp/tools/auth.py b/packages/vaultmesh_mcp/tools/auth.py new file mode 100644 index 0000000..1a93e38 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/auth.py @@ -0,0 +1,638 @@ +""" +VaultMesh MCP Authentication - Ed25519 Challenge-Response + +Implements cryptographic authentication for MCP operators with +capability-based access control and session management. + +Scopes: +- read: Query state (mesh_status, proof_verify) +- admin: Execute commands (tactical_execute) +- vault: Access treasury, sensitive data +- anchor: Create blockchain proofs +- cognitive: AI reasoning capabilities (Claude integration) +""" + +import json +import os +import secrets +import time +from dataclasses import dataclass, asdict +from datetime import datetime, timezone, timedelta +from pathlib import Path +from typing import Any, Dict, Optional, Set +from enum import Enum + +import blake3 + +# Optional: Ed25519 support +try: + from nacl.signing import VerifyKey + from nacl.exceptions import BadSignature + import nacl.encoding + NACL_AVAILABLE = True +except ImportError: + NACL_AVAILABLE = False + +# VaultMesh paths +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" +AUTH_STORE = VAULTMESH_ROOT / "auth" + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _now_iso() -> str: + """Current UTC timestamp in ISO format.""" + return datetime.now(timezone.utc).isoformat() + + +class Scope(Enum): + """Capability scopes for MCP access control.""" + READ = "read" + ADMIN = "admin" + VAULT = "vault" + ANCHOR = "anchor" + COGNITIVE = "cognitive" + + +# Tool permissions by scope +SCOPE_TOOLS: Dict[Scope, Set[str]] = { + Scope.READ: { + "mesh_status", + "shield_status", + "proof_verify", + "guardian_status", + "treasury_balance", + "cognitive_context", + "cognitive_memory_get", + "cognitive_audit_trail", + }, + Scope.ADMIN: { + "tactical_execute", + "mesh_configure", + "agent_task", + }, + Scope.VAULT: { + "treasury_debit", + "treasury_credit", + "treasury_create_budget", + }, + Scope.ANCHOR: { + "guardian_anchor_now", + "proof_anchor", + "cognitive_attest", + }, + Scope.COGNITIVE: { + "cognitive_context", + "cognitive_decide", + "cognitive_invoke_tem", + "cognitive_memory_get", + "cognitive_memory_set", + "cognitive_attest", + "cognitive_audit_trail", + "cognitive_oracle_chain", + # Inherits from READ + "mesh_status", + "shield_status", + "proof_verify", + "guardian_status", + "treasury_balance", + }, +} + + +@dataclass +class Challenge: + """Authentication challenge.""" + challenge_id: str + nonce: str + operator_pubkey: str + scope: str + created_at: str + expires_at: str + + def is_expired(self) -> bool: + now = datetime.now(timezone.utc) + expires = datetime.fromisoformat(self.expires_at.replace('Z', '+00:00')) + return now > expires + + +@dataclass +class Session: + """Authenticated session.""" + session_id: str + token: str + operator_pubkey: str + operator_did: str + scope: str + created_at: str + expires_at: str + ip_hint: Optional[str] = None + + def is_expired(self) -> bool: + now = datetime.now(timezone.utc) + expires = datetime.fromisoformat(self.expires_at.replace('Z', '+00:00')) + return now > expires + + +# In-memory stores (would be persisted in production) +_challenges: Dict[str, Challenge] = {} +_sessions: Dict[str, Session] = {} + + +def _emit_auth_receipt(receipt_type: str, body: dict) -> dict: + """Emit a receipt for authentication events.""" + scroll_path = RECEIPTS_ROOT / "identity" / "identity_events.jsonl" + scroll_path.parent.mkdir(parents=True, exist_ok=True) + + receipt = { + "schema_version": "2.0.0", + "type": receipt_type, + "timestamp": _now_iso(), + "scroll": "identity", + "tags": ["auth", receipt_type], + "root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()), + "body": body, + } + + with open(scroll_path, "a") as f: + f.write(json.dumps(receipt) + "\n") + + return receipt + + +def auth_challenge( + operator_pubkey_b64: str, + scope: str = "read", + ttl_seconds: int = 300, +) -> Dict[str, Any]: + """ + Generate an authentication challenge for an operator. + + Args: + operator_pubkey_b64: Base64-encoded Ed25519 public key + scope: Requested scope (read, admin, vault, anchor, cognitive) + ttl_seconds: Challenge validity period + + Returns: + Challenge ID and nonce for signing + """ + # Validate scope + try: + scope_enum = Scope(scope) + except ValueError: + return {"error": f"Invalid scope: {scope}. Valid: {[s.value for s in Scope]}"} + + challenge_id = f"ch_{secrets.token_hex(16)}" + nonce = secrets.token_hex(32) + + now = datetime.now(timezone.utc) + expires = now + timedelta(seconds=ttl_seconds) + + challenge = Challenge( + challenge_id=challenge_id, + nonce=nonce, + operator_pubkey=operator_pubkey_b64, + scope=scope, + created_at=now.isoformat(), + expires_at=expires.isoformat(), + ) + + _challenges[challenge_id] = challenge + + _emit_auth_receipt("auth_challenge", { + "challenge_id": challenge_id, + "operator_pubkey": operator_pubkey_b64, + "scope": scope, + "expires_at": expires.isoformat(), + }) + + return { + "challenge_id": challenge_id, + "nonce": nonce, + "scope": scope, + "expires_at": expires.isoformat(), + "message": "Sign the nonce with your Ed25519 private key", + } + + +def auth_verify( + challenge_id: str, + signature_b64: str, + ip_hint: Optional[str] = None, +) -> Dict[str, Any]: + """ + Verify a signed challenge and issue session token. + + Args: + challenge_id: The challenge ID from auth_challenge + signature_b64: Base64-encoded Ed25519 signature of the nonce + ip_hint: Optional IP hint for session binding + + Returns: + Session token and metadata + """ + # Get challenge + challenge = _challenges.get(challenge_id) + if not challenge: + return {"error": "Challenge not found or expired"} + + if challenge.is_expired(): + del _challenges[challenge_id] + return {"error": "Challenge expired"} + + # Verify signature + if NACL_AVAILABLE: + try: + pubkey_bytes = nacl.encoding.Base64Encoder.decode(challenge.operator_pubkey.encode()) + verify_key = VerifyKey(pubkey_bytes) + sig_bytes = nacl.encoding.Base64Encoder.decode(signature_b64.encode()) + verify_key.verify(challenge.nonce.encode(), sig_bytes) + except (BadSignature, Exception) as e: + _emit_auth_receipt("auth_failure", { + "challenge_id": challenge_id, + "reason": "invalid_signature", + "error": str(e), + }) + return {"error": "Invalid signature"} + else: + # For testing without nacl, accept any signature + pass + + # Remove used challenge + del _challenges[challenge_id] + + # Create session + session_id = f"ses_{secrets.token_hex(16)}" + token = secrets.token_urlsafe(48) + + now = datetime.now(timezone.utc) + expires = now + timedelta(minutes=30) + + # Derive DID from pubkey + operator_did = f"did:vm:operator:{_vmhash_blake3(challenge.operator_pubkey.encode())[:16]}" + + session = Session( + session_id=session_id, + token=token, + operator_pubkey=challenge.operator_pubkey, + operator_did=operator_did, + scope=challenge.scope, + created_at=now.isoformat(), + expires_at=expires.isoformat(), + ip_hint=ip_hint, + ) + + _sessions[token] = session + + _emit_auth_receipt("auth_success", { + "session_id": session_id, + "operator_did": operator_did, + "scope": challenge.scope, + "expires_at": expires.isoformat(), + }) + + return { + "success": True, + "session_id": session_id, + "token": token, + "operator_did": operator_did, + "scope": challenge.scope, + "expires_at": expires.isoformat(), + "ttl_seconds": 1800, + } + + +def auth_validate_token(token: str) -> Dict[str, Any]: + """ + Validate a session token. + + Args: + token: The session token to validate + + Returns: + Session info if valid, error otherwise + """ + session = _sessions.get(token) + if not session: + return {"valid": False, "error": "Session not found"} + + if session.is_expired(): + del _sessions[token] + return {"valid": False, "error": "Session expired"} + + return { + "valid": True, + "session_id": session.session_id, + "operator_did": session.operator_did, + "scope": session.scope, + "expires_at": session.expires_at, + } + + +def auth_check_permission(token: str, tool_name: str) -> Dict[str, Any]: + """ + Check if a session has permission to call a tool. + + Args: + token: Session token + tool_name: Name of the tool to check + + Returns: + Permission check result + """ + validation = auth_validate_token(token) + if not validation.get("valid"): + return {"allowed": False, "reason": validation.get("error")} + + scope_name = validation["scope"] + try: + scope = Scope(scope_name) + except ValueError: + return {"allowed": False, "reason": f"Invalid scope: {scope_name}"} + + allowed_tools = SCOPE_TOOLS.get(scope, set()) + + if tool_name in allowed_tools: + return { + "allowed": True, + "scope": scope_name, + "operator_did": validation["operator_did"], + } + + return { + "allowed": False, + "reason": f"Tool '{tool_name}' not allowed for scope '{scope_name}'", + "allowed_tools": list(allowed_tools), + } + + +def auth_revoke(token: str) -> Dict[str, Any]: + """ + Revoke a session token. + + Args: + token: Session token to revoke + + Returns: + Revocation result + """ + session = _sessions.pop(token, None) + if not session: + return {"revoked": False, "error": "Session not found"} + + _emit_auth_receipt("auth_revoke", { + "session_id": session.session_id, + "operator_did": session.operator_did, + }) + + return { + "revoked": True, + "session_id": session.session_id, + } + + +def auth_list_sessions() -> Dict[str, Any]: + """ + List all active sessions (admin only). + + Returns: + List of active sessions + """ + active = [] + expired = [] + + for token, session in list(_sessions.items()): + if session.is_expired(): + del _sessions[token] + expired.append(session.session_id) + else: + active.append({ + "session_id": session.session_id, + "operator_did": session.operator_did, + "scope": session.scope, + "expires_at": session.expires_at, + }) + + return { + "active_sessions": active, + "expired_cleaned": len(expired), + } + + +# Convenience function for testing without full auth +def auth_create_dev_session( + scope: str = "cognitive", + operator_did: str = "did:vm:cognitive:claude-dev", +) -> Dict[str, Any]: + """ + Create a development session for testing (DEV ONLY). + + Args: + scope: Scope for the session + operator_did: DID for the operator + + Returns: + Session token and metadata + """ + # Fail-closed: dev sessions may not grant SOVEREIGN-equivalent access. + # Accept only known, non-vault scopes. + normalized_scope = scope + try: + scope_enum = Scope(scope) + if scope_enum == Scope.VAULT: + normalized_scope = Scope.READ.value + except ValueError: + normalized_scope = Scope.READ.value + + session_id = f"dev_{secrets.token_hex(8)}" + token = f"dev_{secrets.token_urlsafe(32)}" + + now = datetime.now(timezone.utc) + expires = now + timedelta(hours=24) + + session = Session( + session_id=session_id, + token=token, + operator_pubkey="dev_key", + operator_did=operator_did, + scope=normalized_scope, + created_at=now.isoformat(), + expires_at=expires.isoformat(), + ) + + _sessions[token] = session + + return { + "dev_mode": True, + "session_id": session_id, + "token": token, + "operator_did": operator_did, + "scope": normalized_scope, + "expires_at": expires.isoformat(), + "warning": "DEV SESSION - Do not use in production", + } + + +# ============================================================================= +# AGENT CAPABILITY PROFILES +# ============================================================================= + +class Profile(Enum): + """Agent capability profiles with hierarchical trust.""" + OBSERVER = "observer" # πŸ‘ Read-only + OPERATOR = "operator" # βš™ Mutations allowed + GUARDIAN = "guardian" # πŸ›‘ Threat response + PHOENIX = "phoenix" # πŸ”₯ Crisis mode + SOVEREIGN = "sovereign" # πŸ‘‘ Full authority + + +# Profile β†’ Tool permissions +PROFILE_TOOLS: Dict[Profile, Set[str]] = { + Profile.OBSERVER: { + # L0 Perception (read) + "get_current_tab", "list_tabs", "get_page_content", + # L1 Substrate (read) + "read_file", "read_multiple_files", "list_directory", "search_files", "get_file_info", + "directory_tree", "list_allowed_directories", + # L2 Cognition (read) + "cognitive_context", "cognitive_memory_get", "cognitive_audit_trail", + # L3 Security (read) + "offsec_status", "offsec_shield_status", "offsec_tem_status", "offsec_mesh_status", + "offsec_phoenix_status", "offsec_braid_list", + # L4 Infrastructure (read) + "worker_list", "kv_list", "r2_list_buckets", "d1_list_databases", "zones_list", + "queue_list", "workflow_list", + # L-1 Proof (read) + "guardian_status", "guardian_verify_receipt", "offsec_proof_latest", + # Treasury (read) + "treasury_balance", + # Auth (read) + "auth_check_permission", + }, + + Profile.OPERATOR: set(), # Computed below + Profile.GUARDIAN: set(), # Computed below + Profile.PHOENIX: set(), # Computed below + Profile.SOVEREIGN: set(), # All tools +} + +# OPERATOR = OBSERVER + mutations +PROFILE_TOOLS[Profile.OPERATOR] = PROFILE_TOOLS[Profile.OBSERVER] | { + # L0 Perception (act) + "execute_javascript", "puppeteer_click", "puppeteer_fill", "puppeteer_select", + "open_url", "reload_tab", "go_back", "go_forward", + # L1 Substrate (write) + "write_file", "edit_file", "create_directory", "move_file", + "start_process", "interact_with_process", + # L2 Cognition (decide, low confidence) + "cognitive_decide", "cognitive_memory_set", + # L3 Security (shield ops) + "offsec_shield_arm", "offsec_shield_disarm", + # L4 Infrastructure (deploy) + "kv_put", "kv_delete", "worker_put", "r2_put_object", + # L-1 Proof (local anchor) + "guardian_anchor_now", +} + +# GUARDIAN = OPERATOR + TEM + attestation +PROFILE_TOOLS[Profile.GUARDIAN] = PROFILE_TOOLS[Profile.OPERATOR] | { + # L2 Cognition (full) + "cognitive_invoke_tem", "cognitive_attest", "cognitive_oracle_chain", + # L3 Security (TEM) + "offsec_tem_transmute", "offsec_tem_rules", "offsec_tem_history", + "offsec_braid_import", + # L4 Infrastructure (more) + "worker_deploy", "d1_query", "queue_send_message", "workflow_execute", + # L-1 Proof (eth anchor) + "offsec_proof_generate", + # Process control + "kill_process", "force_terminate", +} + +# PHOENIX = GUARDIAN + destructive ops + emergency treasury +PROFILE_TOOLS[Profile.PHOENIX] = PROFILE_TOOLS[Profile.GUARDIAN] | { + # L3 Security (Phoenix) + "offsec_phoenix_enable", "offsec_phoenix_disable", "offsec_phoenix_inject_crisis", + "offsec_phoenix_history", + # L4 Infrastructure (destructive) + "worker_delete", "r2_delete_bucket", "r2_delete_object", + "d1_delete_database", "queue_delete", "workflow_delete", + "kv_delete", + # Treasury (emergency) + "treasury_debit", +} + +# SOVEREIGN = everything +PROFILE_TOOLS[Profile.SOVEREIGN] = PROFILE_TOOLS[Profile.PHOENIX] | { + # Auth (full) + "auth_challenge", "auth_verify", "auth_create_dev_session", "auth_revoke", + "auth_list_sessions", + # Treasury (full) + "treasury_create_budget", "treasury_credit", + # All remaining tools +} + + +def get_profile_for_scope(scope: str) -> Profile: + """Map scope to profile.""" + mapping = { + "read": Profile.OBSERVER, + "admin": Profile.OPERATOR, + "cognitive": Profile.GUARDIAN, + "anchor": Profile.GUARDIAN, + "vault": Profile.SOVEREIGN, + } + return mapping.get(scope, Profile.OBSERVER) + + +def check_profile_permission(profile: Profile, tool_name: str) -> Dict[str, Any]: + """Check if a profile has permission for a tool.""" + allowed_tools = PROFILE_TOOLS.get(profile, set()) + + # Handle wildcards in profile tools + for pattern in allowed_tools: + if pattern.endswith("*"): + prefix = pattern[:-1] + if tool_name.startswith(prefix): + return {"allowed": True, "profile": profile.value} + + if tool_name in allowed_tools: + return {"allowed": True, "profile": profile.value} + + return { + "allowed": False, + "profile": profile.value, + "reason": f"Tool '{tool_name}' not allowed for profile '{profile.value}'", + } + + +def escalate_profile(current: Profile, reason: str) -> Dict[str, Any]: + """Request profile escalation.""" + escalation_path = { + Profile.OBSERVER: Profile.OPERATOR, + Profile.OPERATOR: Profile.GUARDIAN, + Profile.GUARDIAN: Profile.PHOENIX, + Profile.PHOENIX: Profile.SOVEREIGN, + Profile.SOVEREIGN: None, + } + + next_profile = escalation_path.get(current) + if next_profile is None: + return {"escalated": False, "reason": "Already at maximum profile"} + + _emit_auth_receipt("profile_escalation", { + "from_profile": current.value, + "to_profile": next_profile.value, + "reason": reason, + }) + + return { + "escalated": True, + "from_profile": current.value, + "to_profile": next_profile.value, + "reason": reason, + } diff --git a/packages/vaultmesh_mcp/tools/cognitive.py b/packages/vaultmesh_mcp/tools/cognitive.py new file mode 100644 index 0000000..82f44df --- /dev/null +++ b/packages/vaultmesh_mcp/tools/cognitive.py @@ -0,0 +1,491 @@ +""" +Cognitive MCP Tools - Claude as VaultMesh Cognitive Organ + +These tools enable Claude to operate as the 7th Organ of VaultMesh: +- Reason over mesh state with full context +- Make attested decisions with Ed25519 proofs +- Invoke Tem for threat transmutation +- Persist memory across sessions via CRDT realm +""" + +import json +import os +import secrets +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Optional, List, Dict + +import blake3 + +# VaultMesh root from env or default +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" +COGNITIVE_REALM = VAULTMESH_ROOT / "realms" / "cognitive" + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _now_iso() -> str: + """Current UTC timestamp in ISO format.""" + return datetime.now(timezone.utc).isoformat() + + +def _emit_cognitive_receipt(receipt_type: str, body: dict, scroll: str = "cognitive") -> dict: + """Emit a receipt for cognitive operations.""" + scroll_path = RECEIPTS_ROOT / scroll / f"{scroll}_events.jsonl" + scroll_path.parent.mkdir(parents=True, exist_ok=True) + + receipt = { + "schema_version": "2.0.0", + "type": receipt_type, + "timestamp": _now_iso(), + "scroll": scroll, + "tags": ["cognitive", receipt_type], + "root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()), + "body": body, + } + + with open(scroll_path, "a") as f: + f.write(json.dumps(receipt) + "\n") + + return receipt + + +def _load_json_file(path: Path) -> dict: + """Load JSON file, return empty dict if not exists.""" + if path.exists(): + with open(path, "r") as f: + return json.load(f) + return {} + + +def _save_json_file(path: Path, data: dict) -> None: + """Save dict to JSON file.""" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "w") as f: + json.dump(data, f, indent=2, sort_keys=True) + + +# ============================================================================= +# COGNITIVE TOOLS - The 8 Tools of AI Reasoning +# ============================================================================= + + +def cognitive_context( + include: Optional[List[str]] = None, + session_id: Optional[str] = None, +) -> Dict[str, Any]: + """ + Read current VaultMesh context for AI reasoning. + + Aggregates state from multiple organs to provide Claude with + full situational awareness for decision-making. + """ + if include is None: + include = ["alerts", "health", "receipts", "threats", "treasury", "governance", "memory"] + + context = { + "timestamp": _now_iso(), + "session_id": session_id, + "vaultmesh_root": str(VAULTMESH_ROOT), + } + + if "alerts" in include: + alerts_path = RECEIPTS_ROOT / "mesh" / "alerts.json" + context["alerts"] = _load_json_file(alerts_path).get("active", []) + + if "health" in include: + health = {"status": "operational", "organs": {}} + for organ in ["guardian", "treasury", "mesh", "identity", "observability"]: + organ_path = RECEIPTS_ROOT / organ + health["organs"][organ] = { + "exists": organ_path.exists(), + "receipt_count": len(list(organ_path.glob("*.jsonl"))) if organ_path.exists() else 0, + } + context["health"] = health + + if "receipts" in include: + recent = {} + for scroll in ["guardian", "treasury", "mesh", "cognitive"]: + jsonl_path = RECEIPTS_ROOT / scroll / f"{scroll}_events.jsonl" + if jsonl_path.exists(): + lines = jsonl_path.read_text().strip().split("\n")[-10:] + recent[scroll] = [json.loads(line) for line in lines if line] + context["recent_receipts"] = recent + + if "threats" in include: + threats_path = RECEIPTS_ROOT / "offsec" / "threats.json" + context["threats"] = _load_json_file(threats_path).get("active", []) + + if "treasury" in include: + budgets_path = RECEIPTS_ROOT / "treasury" / "budgets.json" + context["treasury"] = _load_json_file(budgets_path) + + if "governance" in include: + governance_path = VAULTMESH_ROOT / "constitution" / "active_proposals.json" + context["governance"] = _load_json_file(governance_path) + + if "memory" in include and session_id: + memory_path = COGNITIVE_REALM / "memory" / session_id / "context.json" + context["memory"] = _load_json_file(memory_path) + + return context + + +def cognitive_decide( + reasoning_chain: List[str], + decision: str, + confidence: float, + evidence: Optional[List[str]] = None, + operator_did: str = "did:vm:cognitive:claude", + auto_action_threshold: float = 0.95, +) -> Dict[str, Any]: + """ + Submit a reasoned decision with cryptographic attestation. + """ + if not 0.0 <= confidence <= 1.0: + return {"error": "Confidence must be between 0.0 and 1.0"} + + if not reasoning_chain: + return {"error": "Reasoning chain cannot be empty"} + + decision_id = f"dec_{secrets.token_hex(8)}" + reasoning_hash = _vmhash_blake3(json.dumps(reasoning_chain).encode()) + + body = { + "decision_id": decision_id, + "operator_did": operator_did, + "decision_type": decision, + "confidence": confidence, + "reasoning_hash": reasoning_hash, + "reasoning_chain": reasoning_chain, + "evidence": evidence or [], + "auto_approved": confidence >= auto_action_threshold, + "requires_governance": decision in ["treasury_large", "governance_change", "mesh_restructure"], + } + + receipt = _emit_cognitive_receipt("cognitive_decision", body) + + required_approvals = [] + if body["requires_governance"]: + required_approvals.append("governance_vote") + if not body["auto_approved"]: + required_approvals.append("operator_confirmation") + + execution_plan = [] + if decision == "invoke_tem": + execution_plan = [ + {"step": 1, "action": "validate_threat", "tool": "shield_status"}, + {"step": 2, "action": "invoke_transmutation", "tool": "cognitive_invoke_tem"}, + {"step": 3, "action": "deploy_capability", "tool": "mesh_deploy"}, + {"step": 4, "action": "attest_outcome", "tool": "cognitive_attest"}, + ] + elif decision == "alert": + execution_plan = [ + {"step": 1, "action": "emit_alert", "tool": "mesh_alert"}, + {"step": 2, "action": "notify_operators", "tool": "notify"}, + ] + + return { + "success": True, + "decision_id": decision_id, + "receipt": receipt, + "auto_approved": body["auto_approved"], + "required_approvals": required_approvals, + "execution_plan": execution_plan, + "message": f"Decision {decision_id} recorded with confidence {confidence:.2%}", + } + + +def cognitive_invoke_tem( + threat_type: str, + threat_id: str, + target: str, + evidence: List[str], + recommended_transmutation: Optional[str] = None, + operator_did: str = "did:vm:cognitive:claude", +) -> Dict[str, Any]: + """ + Invoke Tem (Guardian) with AI-detected threat pattern. + Transmutes threats into defensive capabilities. + """ + invocation_id = f"tem_{secrets.token_hex(8)}" + + transmutations = { + "replay_attack": "strict_monotonic_sequence_validator", + "intrusion": "adaptive_firewall_rule", + "anomaly": "behavioral_baseline_enforcer", + "credential_stuffing": "rate_limiter_with_lockout", + "data_exfiltration": "egress_filter_policy", + "privilege_escalation": "capability_constraint_enforcer", + } + + transmutation = recommended_transmutation or transmutations.get(threat_type, "generic_threat_mitigator") + + body = { + "invocation_id": invocation_id, + "operator_did": operator_did, + "threat_type": threat_type, + "threat_id": threat_id, + "target": target, + "evidence": evidence, + "transmutation": transmutation, + "status": "transmuted", + } + + receipt = _emit_cognitive_receipt("tem_invocation", body) + + capability = { + "capability_id": f"cap_{secrets.token_hex(8)}", + "name": transmutation, + "forged_from": threat_id, + "forged_at": _now_iso(), + "scope": target, + } + + caps_path = RECEIPTS_ROOT / "mesh" / "capabilities.json" + caps = _load_json_file(caps_path) + caps[capability["capability_id"]] = capability + _save_json_file(caps_path, caps) + + return { + "success": True, + "invocation_id": invocation_id, + "receipt": receipt, + "capability": capability, + "message": f"Threat {threat_id} transmuted into {transmutation}", + } + + +def cognitive_memory_get( + key: str, + session_id: Optional[str] = None, + realm: str = "memory", +) -> Dict[str, Any]: + """ + Query conversation/reasoning memory from CRDT realm. + """ + if session_id: + memory_path = COGNITIVE_REALM / realm / session_id / f"{key.replace('/', '_')}.json" + else: + memory_path = COGNITIVE_REALM / realm / f"{key.replace('/', '_')}.json" + + value = _load_json_file(memory_path) + + return { + "key": key, + "session_id": session_id, + "realm": realm, + "value": value, + "exists": memory_path.exists(), + "path": str(memory_path), + } + + +def cognitive_memory_set( + key: str, + value: Dict[str, Any], + session_id: Optional[str] = None, + realm: str = "memory", + merge: bool = True, +) -> Dict[str, Any]: + """ + Store reasoning artifacts for future sessions. + Uses CRDT-style merge for concurrent update safety. + """ + if session_id: + memory_path = COGNITIVE_REALM / realm / session_id / f"{key.replace('/', '_')}.json" + else: + memory_path = COGNITIVE_REALM / realm / f"{key.replace('/', '_')}.json" + + if merge and memory_path.exists(): + existing = _load_json_file(memory_path) + merged = {**existing, **value, "_updated_at": _now_iso()} + else: + merged = {**value, "_created_at": _now_iso()} + + _save_json_file(memory_path, merged) + + body = { + "key": key, + "session_id": session_id, + "realm": realm, + "value_hash": _vmhash_blake3(json.dumps(value, sort_keys=True).encode()), + "merged": merge, + } + + receipt = _emit_cognitive_receipt("memory_write", body) + + return { + "success": True, + "key": key, + "path": str(memory_path), + "receipt": receipt, + "message": f"Memory stored at {key}", + } + + +def cognitive_attest( + attestation_type: str, + content: Dict[str, Any], + anchor_to: Optional[List[str]] = None, + operator_did: str = "did:vm:cognitive:claude", +) -> Dict[str, Any]: + """ + Create cryptographic attestation of Claude's reasoning state. + """ + if anchor_to is None: + anchor_to = ["local"] + + attestation_id = f"att_{secrets.token_hex(8)}" + content_hash = _vmhash_blake3(json.dumps(content, sort_keys=True).encode()) + + body = { + "attestation_id": attestation_id, + "attestation_type": attestation_type, + "operator_did": operator_did, + "content_hash": content_hash, + "anchor_targets": anchor_to, + "anchors": {}, + } + + body["anchors"]["local"] = { + "type": "local", + "timestamp": _now_iso(), + "hash": content_hash, + } + + if "rfc3161" in anchor_to: + body["anchors"]["rfc3161"] = { + "type": "rfc3161", + "status": "pending", + "tsa": "freetsa.org", + } + + if "eth" in anchor_to: + body["anchors"]["eth"] = { + "type": "ethereum", + "status": "pending", + "network": "mainnet", + } + + receipt = _emit_cognitive_receipt("attestation", body) + + return { + "success": True, + "attestation_id": attestation_id, + "content_hash": content_hash, + "receipt": receipt, + "anchors": body["anchors"], + "message": f"Attestation {attestation_id} created", + } + + +def cognitive_audit_trail( + filter_type: Optional[str] = None, + time_range: Optional[Dict[str, str]] = None, + confidence_min: Optional[float] = None, + limit: int = 100, +) -> Dict[str, Any]: + """ + Query historical AI decisions for audit. + """ + cognitive_path = RECEIPTS_ROOT / "cognitive" / "cognitive_events.jsonl" + + if not cognitive_path.exists(): + return {"decisions": [], "count": 0, "message": "No cognitive history found"} + + decisions = [] + with open(cognitive_path, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + + try: + receipt = json.loads(line) + except json.JSONDecodeError: + continue + + if receipt.get("type") != "cognitive_decision": + continue + + body = receipt.get("body", {}) + + if filter_type and body.get("decision_type") != filter_type: + continue + + if confidence_min and body.get("confidence", 0) < confidence_min: + continue + + decisions.append({ + "decision_id": body.get("decision_id"), + "timestamp": receipt.get("timestamp"), + "decision_type": body.get("decision_type"), + "confidence": body.get("confidence"), + "reasoning_hash": body.get("reasoning_hash"), + "auto_approved": body.get("auto_approved"), + }) + + if len(decisions) >= limit: + break + + return { + "decisions": decisions, + "count": len(decisions), + } + + +def cognitive_oracle_chain( + question: str, + frameworks: Optional[List[str]] = None, + max_docs: int = 10, + include_memory: bool = True, + session_id: Optional[str] = None, +) -> Dict[str, Any]: + """ + Execute oracle chain with cognitive enhancement. + """ + if frameworks is None: + frameworks = ["GDPR", "AI_ACT"] + + chain_id = f"oracle_{secrets.token_hex(8)}" + + context = cognitive_context( + include=["memory", "governance"] if include_memory else ["governance"], + session_id=session_id, + ) + + answer = { + "chain_id": chain_id, + "question": question, + "frameworks": frameworks, + "answer": f"Oracle analysis pending for: {question}", + "citations": [], + "compliance_flags": {f: "requires_analysis" for f in frameworks}, + "gaps": [], + "confidence": 0.0, + "requires_human_review": True, + } + + answer_hash = _vmhash_blake3(json.dumps(answer, sort_keys=True).encode()) + + body = { + "chain_id": chain_id, + "question": question, + "frameworks": frameworks, + "answer_hash": answer_hash, + } + + receipt = _emit_cognitive_receipt("oracle_chain", body, scroll="compliance") + + return { + "success": True, + "chain_id": chain_id, + "answer": answer, + "answer_hash": answer_hash, + "receipt": receipt, + } diff --git a/packages/vaultmesh_mcp/tools/escalation.py b/packages/vaultmesh_mcp/tools/escalation.py new file mode 100644 index 0000000..bec60b6 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/escalation.py @@ -0,0 +1,492 @@ +""" +Escalation Engine - Profile transitions as first-class proofs + +Every escalation is: +- A receipt (immutable record) +- A Tem-context (threat awareness) +- A reversibility flag (can it be undone?) +- A time-bound (when does it expire?) + +Escalation is not runtime magic β€” it is auditable history. +""" + +import json +import secrets +from datetime import datetime, timezone, timedelta +from dataclasses import dataclass, asdict +from enum import Enum +from pathlib import Path +from typing import Any, Dict, Optional, List +import os + +import blake3 + +# VaultMesh paths +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" +ESCALATION_LOG = RECEIPTS_ROOT / "identity" / "escalation_events.jsonl" + + +def _vmhash_blake3(data: bytes) -> str: + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _now_ts() -> float: + return datetime.now(timezone.utc).timestamp() + + +class EscalationType(Enum): + """Types of profile escalation.""" + THREAT_DETECTED = "threat_detected" # Automatic: threat confidence > threshold + OPERATOR_REQUEST = "operator_request" # Manual: operator requests higher authority + CRISIS_DECLARED = "crisis_declared" # Emergency: system failure or attack + QUORUM_APPROVED = "quorum_approved" # Governance: multi-sig approval + SOVEREIGN_OVERRIDE = "sovereign_override" # Human: direct intervention + + +class DeescalationType(Enum): + """Types of profile de-escalation.""" + TIMEOUT_EXPIRED = "timeout_expired" # Automatic: time limit reached + THREAT_RESOLVED = "threat_resolved" # Automatic: no active threats + OPERATOR_RELEASE = "operator_release" # Manual: operator releases authority + CRISIS_CONCLUDED = "crisis_concluded" # Phoenix: crisis resolved + SOVEREIGN_REVOKE = "sovereign_revoke" # Human: explicit revocation + + +@dataclass +class EscalationContext: + """Context captured at escalation time.""" + threat_id: Optional[str] = None + threat_type: Optional[str] = None + threat_confidence: Optional[float] = None + active_alerts: int = 0 + mesh_health: str = "unknown" + triggering_tool: Optional[str] = None + triggering_decision: Optional[str] = None + + +@dataclass +class Escalation: + """A profile escalation event.""" + escalation_id: str + from_profile: str + to_profile: str + escalation_type: str + context: EscalationContext + + # Reversibility + reversible: bool + auto_deescalate: bool + deescalate_after_seconds: Optional[int] + deescalate_on_condition: Optional[str] + + # Time tracking + created_at: str + expires_at: Optional[str] + + # Proof + receipt_hash: Optional[str] = None + tem_context_hash: Optional[str] = None + + # State + active: bool = True + deescalated_at: Optional[str] = None + deescalation_type: Optional[str] = None + + +# In-memory active escalations (would be persisted in production) +_active_escalations: Dict[str, Escalation] = {} + + +def _emit_escalation_receipt(escalation: Escalation, event_type: str) -> dict: + """Emit a receipt for escalation events.""" + ESCALATION_LOG.parent.mkdir(parents=True, exist_ok=True) + + body = { + "escalation_id": escalation.escalation_id, + "event_type": event_type, + "from_profile": escalation.from_profile, + "to_profile": escalation.to_profile, + "escalation_type": escalation.escalation_type, + "reversible": escalation.reversible, + "context": asdict(escalation.context), + "expires_at": escalation.expires_at, + "active": escalation.active, + } + + if event_type == "deescalation": + body["deescalated_at"] = escalation.deescalated_at + body["deescalation_type"] = escalation.deescalation_type + + receipt = { + "schema_version": "2.0.0", + "type": f"profile_{event_type}", + "timestamp": _now_iso(), + "scroll": "identity", + "tags": ["escalation", event_type, escalation.from_profile, escalation.to_profile], + "root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()), + "body": body, + } + + with open(ESCALATION_LOG, "a") as f: + f.write(json.dumps(receipt) + "\n") + + return receipt + + +# ============================================================================= +# ESCALATION POLICIES +# ============================================================================= + +ESCALATION_POLICIES = { + # OBSERVER β†’ OPERATOR + ("observer", "operator"): { + "reversible": True, + "auto_deescalate": True, + "default_ttl_seconds": 3600, # 1 hour + "requires_reason": True, + "requires_approval": False, + }, + # OPERATOR β†’ GUARDIAN + ("operator", "guardian"): { + "reversible": True, + "auto_deescalate": True, + "default_ttl_seconds": 7200, # 2 hours + "requires_reason": True, + "requires_approval": False, + "auto_on_threat_confidence": 0.8, + }, + # GUARDIAN β†’ PHOENIX + ("guardian", "phoenix"): { + "reversible": True, + "auto_deescalate": True, + "default_ttl_seconds": 1800, # 30 minutes + "requires_reason": True, + "requires_approval": True, # Requires quorum or sovereign + "auto_on_crisis": True, + }, + # PHOENIX β†’ SOVEREIGN + ("phoenix", "sovereign"): { + "reversible": False, # Cannot auto-deescalate from sovereign + "auto_deescalate": False, + "default_ttl_seconds": None, + "requires_reason": True, + "requires_approval": True, + "requires_human": True, + }, +} + +DEESCALATION_CONDITIONS = { + "no_active_threats_1h": "No active threats for 1 hour", + "no_active_alerts_24h": "No active alerts for 24 hours", + "crisis_resolved": "Crisis formally concluded", + "manual_release": "Operator explicitly released authority", + "timeout": "Escalation TTL expired", +} + + +# ============================================================================= +# ESCALATION OPERATIONS +# ============================================================================= + +def escalate( + from_profile: str, + to_profile: str, + escalation_type: EscalationType, + context: Optional[EscalationContext] = None, + ttl_seconds: Optional[int] = None, + deescalate_condition: Optional[str] = None, + approved_by: Optional[str] = None, +) -> Dict[str, Any]: + """ + Escalate from one profile to another with full proof chain. + + Returns escalation receipt and Tem context. + """ + # Get policy + policy_key = (from_profile, to_profile) + policy = ESCALATION_POLICIES.get(policy_key) + + if not policy: + return { + "success": False, + "error": f"No escalation path from {from_profile} to {to_profile}", + } + + # Check approval requirements + if policy.get("requires_human") and escalation_type != EscalationType.SOVEREIGN_OVERRIDE: + return { + "success": False, + "error": f"Escalation to {to_profile} requires human (sovereign) approval", + } + + if policy.get("requires_approval") and not approved_by: + if escalation_type not in [EscalationType.QUORUM_APPROVED, EscalationType.SOVEREIGN_OVERRIDE]: + return { + "success": False, + "error": f"Escalation to {to_profile} requires approval", + "approval_required": True, + } + + # Build context + if context is None: + context = EscalationContext() + + # Calculate expiry + ttl = ttl_seconds or policy.get("default_ttl_seconds") + expires_at = None + if ttl: + expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl)).isoformat() + + # Create escalation + escalation_id = f"esc_{secrets.token_hex(12)}" + + escalation = Escalation( + escalation_id=escalation_id, + from_profile=from_profile, + to_profile=to_profile, + escalation_type=escalation_type.value, + context=context, + reversible=policy["reversible"], + auto_deescalate=policy["auto_deescalate"], + deescalate_after_seconds=ttl, + deescalate_on_condition=deescalate_condition, + created_at=_now_iso(), + expires_at=expires_at, + active=True, + ) + + # Emit receipt + receipt = _emit_escalation_receipt(escalation, "escalation") + escalation.receipt_hash = receipt["root_hash"] + + # Create Tem context hash (for threat awareness) + tem_context = { + "escalation_id": escalation_id, + "profile_transition": f"{from_profile} β†’ {to_profile}", + "threat_context": asdict(context), + "timestamp": _now_iso(), + } + escalation.tem_context_hash = _vmhash_blake3( + json.dumps(tem_context, sort_keys=True).encode() + ) + + # Store active escalation + _active_escalations[escalation_id] = escalation + + return { + "success": True, + "escalation_id": escalation_id, + "from_profile": from_profile, + "to_profile": to_profile, + "escalation_type": escalation_type.value, + "reversible": escalation.reversible, + "expires_at": expires_at, + "receipt_hash": escalation.receipt_hash, + "tem_context_hash": escalation.tem_context_hash, + "deescalate_condition": deescalate_condition or "timeout", + } + + +def deescalate( + escalation_id: str, + deescalation_type: DeescalationType, + reason: Optional[str] = None, +) -> Dict[str, Any]: + """ + De-escalate an active escalation. + """ + escalation = _active_escalations.get(escalation_id) + + if not escalation: + return { + "success": False, + "error": f"Escalation {escalation_id} not found or already inactive", + } + + if not escalation.reversible and deescalation_type != DeescalationType.SOVEREIGN_REVOKE: + return { + "success": False, + "error": f"Escalation {escalation_id} is not reversible without sovereign override", + } + + # Update escalation + escalation.active = False + escalation.deescalated_at = _now_iso() + escalation.deescalation_type = deescalation_type.value + + # Emit receipt + receipt = _emit_escalation_receipt(escalation, "deescalation") + + # Remove from active + del _active_escalations[escalation_id] + + return { + "success": True, + "escalation_id": escalation_id, + "from_profile": escalation.to_profile, # Note: going back + "to_profile": escalation.from_profile, + "deescalation_type": deescalation_type.value, + "reason": reason, + "receipt_hash": receipt["root_hash"], + "duration_seconds": ( + datetime.fromisoformat(escalation.deescalated_at.replace('Z', '+00:00')) - + datetime.fromisoformat(escalation.created_at.replace('Z', '+00:00')) + ).total_seconds(), + } + + +def check_expired_escalations() -> List[Dict[str, Any]]: + """ + Check for and auto-deescalate expired escalations. + Called periodically by the system. + """ + now = datetime.now(timezone.utc) + expired = [] + + for esc_id, escalation in list(_active_escalations.items()): + if not escalation.expires_at: + continue + + expires = datetime.fromisoformat(escalation.expires_at.replace('Z', '+00:00')) + + if now > expires and escalation.auto_deescalate: + result = deescalate( + esc_id, + DeescalationType.TIMEOUT_EXPIRED, + reason=f"TTL of {escalation.deescalate_after_seconds}s expired" + ) + expired.append(result) + + return expired + + +def get_active_escalations() -> Dict[str, Any]: + """Get all active escalations.""" + return { + "active_count": len(_active_escalations), + "escalations": [ + { + "escalation_id": e.escalation_id, + "from_profile": e.from_profile, + "to_profile": e.to_profile, + "escalation_type": e.escalation_type, + "created_at": e.created_at, + "expires_at": e.expires_at, + "reversible": e.reversible, + } + for e in _active_escalations.values() + ], + } + + +def get_escalation_history( + profile: Optional[str] = None, + limit: int = 100, +) -> Dict[str, Any]: + """Query escalation history from receipts.""" + if not ESCALATION_LOG.exists(): + return {"history": [], "count": 0} + + history = [] + with open(ESCALATION_LOG, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + receipt = json.loads(line) + body = receipt.get("body", {}) + + # Filter by profile if specified + if profile: + if body.get("from_profile") != profile and body.get("to_profile") != profile: + continue + + history.append({ + "escalation_id": body.get("escalation_id"), + "event_type": body.get("event_type"), + "from_profile": body.get("from_profile"), + "to_profile": body.get("to_profile"), + "timestamp": receipt.get("timestamp"), + "receipt_hash": receipt.get("root_hash"), + }) + except json.JSONDecodeError: + continue + + # Return most recent first + history.reverse() + + return { + "history": history[:limit], + "count": len(history), + } + + +# ============================================================================= +# CONVENIENCE FUNCTIONS FOR COMMON ESCALATIONS +# ============================================================================= + +def escalate_on_threat( + current_profile: str, + threat_id: str, + threat_type: str, + confidence: float, +) -> Dict[str, Any]: + """ + Escalate based on detected threat. + Auto-determines target profile based on confidence. + """ + context = EscalationContext( + threat_id=threat_id, + threat_type=threat_type, + threat_confidence=confidence, + ) + + # Determine target profile + if current_profile == "observer": + to_profile = "operator" + elif current_profile == "operator" and confidence >= 0.8: + to_profile = "guardian" + elif current_profile == "guardian" and confidence >= 0.95: + to_profile = "phoenix" + else: + return { + "success": False, + "escalated": False, + "reason": f"Confidence {confidence} insufficient for escalation from {current_profile}", + } + + return escalate( + from_profile=current_profile, + to_profile=to_profile, + escalation_type=EscalationType.THREAT_DETECTED, + context=context, + deescalate_condition="no_active_threats_1h", + ) + + +def escalate_to_phoenix( + reason: str, + approved_by: str, +) -> Dict[str, Any]: + """ + Emergency escalation to Phoenix profile. + Requires approval. + """ + context = EscalationContext( + mesh_health="crisis", + ) + + return escalate( + from_profile="guardian", + to_profile="phoenix", + escalation_type=EscalationType.CRISIS_DECLARED, + context=context, + approved_by=approved_by, + deescalate_condition="crisis_resolved", + ) diff --git a/packages/vaultmesh_mcp/tools/file.py b/packages/vaultmesh_mcp/tools/file.py new file mode 100644 index 0000000..31681b2 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/file.py @@ -0,0 +1,101 @@ +"""File MCP tools - File operations with receipts.""" + +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import blake3 + +# VaultMesh root from env or default +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" +FILE_RECEIPTS = RECEIPTS_ROOT / "file" / "file_operations.jsonl" + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _emit_file_receipt(operation: str, file_path: str, details: dict, actor: str = "did:vm:mcp:file") -> str: + """Emit a receipt for file operation.""" + FILE_RECEIPTS.parent.mkdir(parents=True, exist_ok=True) + + body = { + "operation": operation, + "file_path": file_path, + "details": details, + "actor": actor, + } + + root_hash = _vmhash_blake3(json.dumps(body, sort_keys=True).encode()) + + receipt = { + "schema_version": "2.0.0", + "type": "file_operation", + "timestamp": datetime.now(timezone.utc).isoformat(), + "scroll": "file", + "tags": ["file", operation, "mcp"], + "root_hash": root_hash, + "body": body, + } + + with open(FILE_RECEIPTS, "a") as f: + f.write(json.dumps(receipt) + "\n") + + return root_hash + + +def file_add( + path: str, + content: str, + encoding: str = "utf-8", + actor: str = "did:vm:mcp:file", +) -> dict[str, Any]: + """ + Add (create or overwrite) a file with content. + + Args: + path: File path relative to VAULTMESH_ROOT or absolute + content: File content to write + encoding: File encoding (default: utf-8) + actor: DID of actor performing operation + + Returns: + Result with file hash and receipt hash + """ + try: + file_path = Path(path) + if not file_path.is_absolute(): + file_path = VAULTMESH_ROOT / file_path + + # Create parent directories if needed + file_path.parent.mkdir(parents=True, exist_ok=True) + + # Write file + file_path.write_text(content, encoding=encoding) + + # Hash the content + content_hash = _vmhash_blake3(content.encode(encoding)) + + details = { + "content_hash": content_hash, + "size_bytes": len(content.encode(encoding)), + "encoding": encoding, + "created": not file_path.exists(), + } + + receipt_hash = _emit_file_receipt("add", str(file_path), details, actor) + + return { + "success": True, + "path": str(file_path), + "content_hash": content_hash, + "receipt_hash": receipt_hash, + "size_bytes": details["size_bytes"], + } + + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/packages/vaultmesh_mcp/tools/guardian.py b/packages/vaultmesh_mcp/tools/guardian.py new file mode 100644 index 0000000..b8eff63 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/guardian.py @@ -0,0 +1,234 @@ +"""Guardian MCP tools - Merkle root anchoring operations.""" + +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Optional + +import blake3 + +# VaultMesh root from env or default +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" + +# Scroll definitions +SCROLLS = { + "drills": {"jsonl": "receipts/drills/drill_runs.jsonl"}, + "compliance": {"jsonl": "receipts/compliance/oracle_answers.jsonl"}, + "guardian": {"jsonl": "receipts/guardian/anchor_events.jsonl"}, + "treasury": {"jsonl": "receipts/treasury/treasury_events.jsonl"}, + "mesh": {"jsonl": "receipts/mesh/mesh_events.jsonl"}, + "offsec": {"jsonl": "receipts/offsec/offsec_events.jsonl"}, + "identity": {"jsonl": "receipts/identity/identity_events.jsonl"}, + "observability": {"jsonl": "receipts/observability/observability_events.jsonl"}, + "automation": {"jsonl": "receipts/automation/automation_events.jsonl"}, + "psi": {"jsonl": "receipts/psi/psi_events.jsonl"}, +} + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _merkle_root(hashes: list[str]) -> str: + """Compute Merkle root from list of VaultMesh hashes.""" + if not hashes: + return _vmhash_blake3(b"empty") + if len(hashes) == 1: + return hashes[0] + + # Iteratively combine pairs + current = hashes + while len(current) > 1: + next_level = [] + for i in range(0, len(current), 2): + if i + 1 < len(current): + combined = current[i] + current[i + 1] + else: + combined = current[i] + current[i] # Duplicate odd leaf + next_level.append(_vmhash_blake3(combined.encode())) + current = next_level + return current[0] + + +def _compute_scroll_root(scroll_name: str) -> dict[str, Any]: + """Compute Merkle root for a single scroll.""" + if scroll_name not in SCROLLS: + return {"error": f"Unknown scroll: {scroll_name}"} + + jsonl_path = VAULTMESH_ROOT / SCROLLS[scroll_name]["jsonl"] + if not jsonl_path.exists(): + return { + "scroll": scroll_name, + "root": _vmhash_blake3(b"empty"), + "leaf_count": 0, + "exists": False, + } + + hashes = [] + with open(jsonl_path, "r") as f: + for line in f: + line = line.strip() + if line: + hashes.append(_vmhash_blake3(line.encode())) + + root = _merkle_root(hashes) + return { + "scroll": scroll_name, + "root": root, + "leaf_count": len(hashes), + "exists": True, + } + + +def guardian_anchor_now( + scrolls: Optional[list[str]] = None, + guardian_did: str = "did:vm:guardian:mcp", + backend: str = "local", +) -> dict[str, Any]: + """ + Anchor specified scrolls and emit a guardian receipt. + + Args: + scrolls: List of scroll names to anchor (default: all) + guardian_did: DID of the guardian performing the anchor + backend: Backend identifier (local, ethereum, stellar) + + Returns: + Anchor receipt with roots for each scroll + """ + if scrolls is None: + scrolls = list(SCROLLS.keys()) + + # Validate scrolls + invalid = [s for s in scrolls if s not in SCROLLS] + if invalid: + return {"error": f"Invalid scrolls: {invalid}"} + + # Compute roots for each scroll + roots = {} + for scroll_name in scrolls: + result = _compute_scroll_root(scroll_name) + if "error" in result: + return result + roots[scroll_name] = result["root"] + + # Compute anchor hash over all roots + roots_json = json.dumps(roots, sort_keys=True).encode() + anchor_hash = _vmhash_blake3(roots_json) + + now = datetime.now(timezone.utc) + anchor_id = f"anchor-{now.strftime('%Y%m%d%H%M%S')}" + + receipt = { + "schema_version": "2.0.0", + "type": "guardian_anchor", + "timestamp": now.isoformat(), + "anchor_id": anchor_id, + "backend": backend, + "anchor_by": guardian_did, + "anchor_epoch": int(now.timestamp()), + "roots": roots, + "scrolls": scrolls, + "anchor_hash": anchor_hash, + } + + # Write receipt to guardian JSONL + guardian_path = VAULTMESH_ROOT / "receipts/guardian/anchor_events.jsonl" + guardian_path.parent.mkdir(parents=True, exist_ok=True) + + with open(guardian_path, "a") as f: + f.write(json.dumps(receipt) + "\n") + + # Update ROOT.guardian.txt + root_result = _compute_scroll_root("guardian") + root_file = VAULTMESH_ROOT / "ROOT.guardian.txt" + root_file.write_text(root_result["root"]) + + return { + "success": True, + "receipt": receipt, + "message": f"Anchored {len(scrolls)} scrolls with ID {anchor_id}", + } + + +def guardian_verify_receipt(receipt_hash: str, scroll: str = "guardian") -> dict[str, Any]: + """ + Verify a receipt exists in a scroll's JSONL. + + Args: + receipt_hash: The root_hash of the receipt to verify + scroll: The scroll to search in + + Returns: + Verification result with proof if found + """ + if scroll not in SCROLLS: + return {"error": f"Unknown scroll: {scroll}"} + + jsonl_path = VAULTMESH_ROOT / SCROLLS[scroll]["jsonl"] + if not jsonl_path.exists(): + return {"verified": False, "reason": "Scroll JSONL does not exist"} + + # Search for receipt with matching hash + with open(jsonl_path, "r") as f: + line_num = 0 + for line in f: + line = line.strip() + if not line: + continue + line_num += 1 + line_hash = _vmhash_blake3(line.encode()) + + # Check if the line hash matches or if the JSON contains the hash + try: + data = json.loads(line) + if data.get("anchor_hash") == receipt_hash or data.get("root_hash") == receipt_hash: + return { + "verified": True, + "line_number": line_num, + "line_hash": line_hash, + "receipt": data, + } + except json.JSONDecodeError: + continue + + return {"verified": False, "reason": "Receipt not found in scroll"} + + +def guardian_status() -> dict[str, Any]: + """ + Get current status of all scrolls. + + Returns: + Status of each scroll including root hash and leaf count + """ + status = {} + for scroll_name in SCROLLS: + result = _compute_scroll_root(scroll_name) + status[scroll_name] = { + "root": result["root"], + "leaf_count": result["leaf_count"], + "exists": result.get("exists", False), + } + + # Get last anchor info + guardian_path = VAULTMESH_ROOT / "receipts/guardian/anchor_events.jsonl" + last_anchor = None + if guardian_path.exists(): + with open(guardian_path, "r") as f: + for line in f: + line = line.strip() + if line: + try: + last_anchor = json.loads(line) + except json.JSONDecodeError: + pass + + return { + "scrolls": status, + "last_anchor": last_anchor, + "vaultmesh_root": str(VAULTMESH_ROOT), + } diff --git a/packages/vaultmesh_mcp/tools/key_binding.py b/packages/vaultmesh_mcp/tools/key_binding.py new file mode 100644 index 0000000..0f141bc --- /dev/null +++ b/packages/vaultmesh_mcp/tools/key_binding.py @@ -0,0 +1,578 @@ +""" +Key Binding Engine - Authority bound to cryptographic reality + +Every profile has a corresponding key reality: +- OBSERVER: Ephemeral (memory only, no signing power) +- OPERATOR: Session key (encrypted disk, revocable) +- GUARDIAN: Device-bound (secure enclave, non-exportable) +- PHOENIX: Time-locked (guardian key + approval artifact) +- SOVEREIGN: Offline root (hardware key, never automated) + +Invariant: No profile exists without corresponding key reality. +If key cannot be proven β†’ authority collapses downward, never upward. +""" + +import json +import secrets +import hashlib +import os +from datetime import datetime, timezone, timedelta +from dataclasses import dataclass, asdict, field +from enum import Enum +from pathlib import Path +from typing import Any, Dict, Optional, List + +import blake3 + +# Optional: Ed25519 support +try: + from nacl.signing import SigningKey, VerifyKey + from nacl.encoding import Base64Encoder + from nacl.exceptions import BadSignature + NACL_AVAILABLE = True +except ImportError: + NACL_AVAILABLE = False + +# VaultMesh paths +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +RECEIPTS_ROOT = VAULTMESH_ROOT / "receipts" +KEYS_ROOT = VAULTMESH_ROOT / "keys" + + +def _vmhash_blake3(data: bytes) -> str: + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +class KeyBindingType(Enum): + """Types of key bindings corresponding to profiles.""" + EPHEMERAL = "ephemeral" # πŸ‘ OBSERVER - memory only + SESSION = "session" # βš™ OPERATOR - encrypted disk + DEVICE = "device" # πŸ›‘ GUARDIAN - secure enclave + TIMELOCKED = "timelocked" # πŸ”₯ PHOENIX - guardian + approval + HARDWARE = "hardware" # πŸ‘‘ SOVEREIGN - offline, air-gapped + + +class KeyStatus(Enum): + """Key lifecycle states.""" + ACTIVE = "active" + EXPIRED = "expired" + REVOKED = "revoked" + PENDING = "pending" + + +@dataclass +class KeyBinding: + """A cryptographic key bound to a profile.""" + key_id: str + profile: str + binding_type: str + fingerprint: str + + # Key material (public only stored here) + public_key_b64: str + + # Binding constraints + created_at: str + expires_at: Optional[str] + device_id: Optional[str] = None + + # Status + status: str = "active" + revoked_at: Optional[str] = None + revocation_reason: Optional[str] = None + + # Proof + binding_receipt_hash: Optional[str] = None + + +@dataclass +class KeyAssertion: + """Runtime assertion that a key is valid for a profile.""" + assertion_id: str + key_id: str + profile: str + binding_type: str + fingerprint: str + + # Verification + verified_at: str + signature_valid: bool + binding_valid: bool + not_expired: bool + not_revoked: bool + + # Result + authority_granted: bool + collapse_to: Optional[str] = None # If authority denied, collapse to this + + +# In-memory key store (production would use secure storage) +_key_bindings: Dict[str, KeyBinding] = {} +_device_keys: Dict[str, str] = {} # device_id -> key_id + + +# ============================================================================= +# PROFILE β†’ KEY BINDING REQUIREMENTS +# ============================================================================= + +PROFILE_KEY_REQUIREMENTS = { + "observer": { + "binding_type": KeyBindingType.EPHEMERAL, + "requires_signature": False, + "requires_device": False, + "max_ttl_seconds": 3600, # 1 hour + "can_sign_receipts": False, + }, + "operator": { + "binding_type": KeyBindingType.SESSION, + "requires_signature": True, + "requires_device": False, + "max_ttl_seconds": 86400, # 24 hours + "can_sign_receipts": True, + }, + "guardian": { + "binding_type": KeyBindingType.DEVICE, + "requires_signature": True, + "requires_device": True, + "max_ttl_seconds": 604800, # 7 days + "can_sign_receipts": True, + "device_types": ["secure_enclave", "tpm", "operator_phone"], + }, + "phoenix": { + "binding_type": KeyBindingType.TIMELOCKED, + "requires_signature": True, + "requires_device": True, + "requires_approval_artifact": True, + "max_ttl_seconds": 86400, # 24 hours (auto-expire) + "can_sign_receipts": True, + }, + "sovereign": { + "binding_type": KeyBindingType.HARDWARE, + "requires_signature": True, + "requires_device": True, + "requires_human": True, + "max_ttl_seconds": None, # No auto-expire + "can_sign_receipts": True, + "device_types": ["hardware_key", "air_gapped"], + }, +} + + +# ============================================================================= +# KEY OPERATIONS +# ============================================================================= + +def generate_key_pair() -> Dict[str, str]: + """Generate an Ed25519 key pair.""" + if NACL_AVAILABLE: + signing_key = SigningKey.generate() + verify_key = signing_key.verify_key + return { + "private_key_b64": signing_key.encode(Base64Encoder).decode(), + "public_key_b64": verify_key.encode(Base64Encoder).decode(), + "fingerprint": _vmhash_blake3(verify_key.encode())[:24], + } + else: + # Fallback: generate placeholder for testing + fake_key = secrets.token_bytes(32) + return { + "private_key_b64": "PLACEHOLDER_" + secrets.token_urlsafe(32), + "public_key_b64": "PLACEHOLDER_" + secrets.token_urlsafe(32), + "fingerprint": _vmhash_blake3(fake_key)[:24], + } + + +def create_key_binding( + profile: str, + public_key_b64: str, + device_id: Optional[str] = None, + ttl_seconds: Optional[int] = None, +) -> Dict[str, Any]: + """ + Create a key binding for a profile. + + Validates that the binding meets profile requirements. + """ + requirements = PROFILE_KEY_REQUIREMENTS.get(profile) + if not requirements: + return {"success": False, "error": f"Unknown profile: {profile}"} + + binding_type = requirements["binding_type"] + + # Validate device requirement + if requirements.get("requires_device") and not device_id: + return { + "success": False, + "error": f"Profile {profile} requires device binding", + } + + # Calculate expiry + max_ttl = requirements.get("max_ttl_seconds") + if ttl_seconds and max_ttl and ttl_seconds > max_ttl: + ttl_seconds = max_ttl + + expires_at = None + if ttl_seconds: + expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat() + elif max_ttl: + expires_at = (datetime.now(timezone.utc) + timedelta(seconds=max_ttl)).isoformat() + + # Generate fingerprint + fingerprint = _vmhash_blake3(public_key_b64.encode())[:24] + + key_id = f"key_{secrets.token_hex(12)}" + + binding = KeyBinding( + key_id=key_id, + profile=profile, + binding_type=binding_type.value, + fingerprint=fingerprint, + public_key_b64=public_key_b64, + created_at=_now_iso(), + expires_at=expires_at, + device_id=device_id, + status=KeyStatus.ACTIVE.value, + ) + + # Emit binding receipt + receipt = _emit_key_receipt("key_binding_created", asdict(binding)) + binding.binding_receipt_hash = receipt["root_hash"] + + # Store + _key_bindings[key_id] = binding + if device_id: + _device_keys[device_id] = key_id + + return { + "success": True, + "key_id": key_id, + "profile": profile, + "binding_type": binding_type.value, + "fingerprint": fingerprint, + "expires_at": expires_at, + "device_id": device_id, + "receipt_hash": binding.binding_receipt_hash, + } + + +def assert_key_authority( + key_id: str, + required_profile: str, + signature: Optional[str] = None, + challenge: Optional[str] = None, +) -> Dict[str, Any]: + """ + Assert that a key has authority for a profile. + + If assertion fails, returns collapse_to indicating the + maximum authority the key can claim. + """ + assertion_id = f"assert_{secrets.token_hex(8)}" + + binding = _key_bindings.get(key_id) + if not binding: + return _authority_denied(assertion_id, required_profile, "Key not found", "observer") + + # Check status + if binding.status == KeyStatus.REVOKED.value: + return _authority_denied(assertion_id, required_profile, "Key revoked", "observer") + + # Check expiry + not_expired = True + if binding.expires_at: + expires = datetime.fromisoformat(binding.expires_at.replace('Z', '+00:00')) + if datetime.now(timezone.utc) > expires: + not_expired = False + # Auto-revoke expired keys + binding.status = KeyStatus.EXPIRED.value + return _authority_denied(assertion_id, required_profile, "Key expired", "observer") + + # Check profile hierarchy + profile_order = ["observer", "operator", "guardian", "phoenix", "sovereign"] + bound_level = profile_order.index(binding.profile) if binding.profile in profile_order else -1 + required_level = profile_order.index(required_profile) if required_profile in profile_order else 99 + + if bound_level < required_level: + # Key is for lower profile - collapse to its actual level + return _authority_denied( + assertion_id, + required_profile, + f"Key bound to {binding.profile}, not sufficient for {required_profile}", + binding.profile + ) + + # Check signature if required + requirements = PROFILE_KEY_REQUIREMENTS.get(required_profile, {}) + signature_valid = True + + if requirements.get("requires_signature"): + if not signature or not challenge: + return _authority_denied( + assertion_id, + required_profile, + "Signature required but not provided", + _collapse_profile(required_profile) + ) + + # Verify signature + if NACL_AVAILABLE and not binding.public_key_b64.startswith("PLACEHOLDER"): + try: + verify_key = VerifyKey(binding.public_key_b64.encode(), Base64Encoder) + sig_bytes = Base64Encoder.decode(signature.encode()) + verify_key.verify(challenge.encode(), sig_bytes) + except BadSignature: + signature_valid = False + return _authority_denied( + assertion_id, + required_profile, + "Invalid signature", + _collapse_profile(required_profile) + ) + + # All checks passed + assertion = KeyAssertion( + assertion_id=assertion_id, + key_id=key_id, + profile=required_profile, + binding_type=binding.binding_type, + fingerprint=binding.fingerprint, + verified_at=_now_iso(), + signature_valid=signature_valid, + binding_valid=True, + not_expired=not_expired, + not_revoked=binding.status != KeyStatus.REVOKED.value, + authority_granted=True, + ) + + _emit_key_receipt("key_assertion_granted", asdict(assertion)) + + return { + "authority_granted": True, + "assertion_id": assertion_id, + "key_id": key_id, + "profile": required_profile, + "binding_type": binding.binding_type, + "fingerprint": binding.fingerprint, + "expires_at": binding.expires_at, + } + + +def _authority_denied( + assertion_id: str, + required_profile: str, + reason: str, + collapse_to: str, +) -> Dict[str, Any]: + """Record authority denial and return collapse result.""" + assertion = KeyAssertion( + assertion_id=assertion_id, + key_id="unknown", + profile=required_profile, + binding_type="none", + fingerprint="none", + verified_at=_now_iso(), + signature_valid=False, + binding_valid=False, + not_expired=False, + not_revoked=False, + authority_granted=False, + collapse_to=collapse_to, + ) + + _emit_key_receipt("key_assertion_denied", { + **asdict(assertion), + "reason": reason, + }) + + return { + "authority_granted": False, + "assertion_id": assertion_id, + "reason": reason, + "collapse_to": collapse_to, + "message": f"Authority denied. Collapsing to {collapse_to}.", + } + + +def _collapse_profile(profile: str) -> str: + """Determine collapse target when authority is denied.""" + collapse_map = { + "sovereign": "phoenix", + "phoenix": "guardian", + "guardian": "operator", + "operator": "observer", + "observer": "observer", + } + return collapse_map.get(profile, "observer") + + +def revoke_key(key_id: str, reason: str) -> Dict[str, Any]: + """Revoke a key binding.""" + binding = _key_bindings.get(key_id) + if not binding: + return {"success": False, "error": "Key not found"} + + binding.status = KeyStatus.REVOKED.value + binding.revoked_at = _now_iso() + binding.revocation_reason = reason + + _emit_key_receipt("key_revoked", { + "key_id": key_id, + "profile": binding.profile, + "fingerprint": binding.fingerprint, + "reason": reason, + "revoked_at": binding.revoked_at, + }) + + return { + "success": True, + "key_id": key_id, + "revoked_at": binding.revoked_at, + } + + +def get_device_key(device_id: str) -> Optional[Dict[str, Any]]: + """Get the key binding for a device.""" + key_id = _device_keys.get(device_id) + if not key_id: + return None + + binding = _key_bindings.get(key_id) + if not binding: + return None + + return { + "key_id": key_id, + "profile": binding.profile, + "binding_type": binding.binding_type, + "fingerprint": binding.fingerprint, + "device_id": device_id, + "status": binding.status, + "expires_at": binding.expires_at, + } + + +def list_key_bindings(profile: Optional[str] = None) -> Dict[str, Any]: + """List all key bindings, optionally filtered by profile.""" + bindings = [] + for key_id, binding in _key_bindings.items(): + if profile and binding.profile != profile: + continue + bindings.append({ + "key_id": key_id, + "profile": binding.profile, + "binding_type": binding.binding_type, + "fingerprint": binding.fingerprint, + "status": binding.status, + "expires_at": binding.expires_at, + "device_id": binding.device_id, + }) + + return { + "count": len(bindings), + "bindings": bindings, + } + + +def _emit_key_receipt(receipt_type: str, body: dict) -> dict: + """Emit a receipt for key operations.""" + scroll_path = RECEIPTS_ROOT / "identity" / "key_events.jsonl" + scroll_path.parent.mkdir(parents=True, exist_ok=True) + + receipt = { + "schema_version": "2.0.0", + "type": receipt_type, + "timestamp": _now_iso(), + "scroll": "identity", + "tags": ["key", receipt_type], + "root_hash": _vmhash_blake3(json.dumps(body, sort_keys=True).encode()), + "body": body, + } + + with open(scroll_path, "a") as f: + f.write(json.dumps(receipt) + "\n") + + return receipt + + +# ============================================================================= +# GUARDIAN DEVICE BINDING +# ============================================================================= + +def bind_guardian_device( + device_id: str, + device_type: str, + public_key_b64: str, + device_attestation: Optional[str] = None, +) -> Dict[str, Any]: + """ + Bind a device as Guardian-of-record. + + The Guardian device becomes: + - Escalation signer + - Receipt verifier + - Emergency revocation authority + """ + valid_types = PROFILE_KEY_REQUIREMENTS["guardian"]["device_types"] + if device_type not in valid_types: + return { + "success": False, + "error": f"Invalid device type. Must be one of: {valid_types}", + } + + # Create guardian key binding + result = create_key_binding( + profile="guardian", + public_key_b64=public_key_b64, + device_id=device_id, + ttl_seconds=604800, # 7 days + ) + + if not result.get("success"): + return result + + # Record device binding + device_binding = { + "device_id": device_id, + "device_type": device_type, + "key_id": result["key_id"], + "fingerprint": result["fingerprint"], + "bound_at": _now_iso(), + "attestation_hash": _vmhash_blake3(device_attestation.encode()) if device_attestation else None, + "capabilities": [ + "escalation_signer", + "receipt_verifier", + "emergency_revocation", + ], + } + + _emit_key_receipt("guardian_device_bound", device_binding) + + # Store guardian device info + guardian_path = KEYS_ROOT / "guardian_device.json" + guardian_path.parent.mkdir(parents=True, exist_ok=True) + with open(guardian_path, "w") as f: + json.dump(device_binding, f, indent=2) + + return { + "success": True, + "device_id": device_id, + "device_type": device_type, + "key_id": result["key_id"], + "fingerprint": result["fingerprint"], + "capabilities": device_binding["capabilities"], + "message": f"Device {device_id} bound as Guardian-of-record", + } + + +def get_guardian_device() -> Optional[Dict[str, Any]]: + """Get the current Guardian device binding.""" + guardian_path = KEYS_ROOT / "guardian_device.json" + if not guardian_path.exists(): + return None + + with open(guardian_path, "r") as f: + return json.load(f) diff --git a/packages/vaultmesh_mcp/tools/treasury.py b/packages/vaultmesh_mcp/tools/treasury.py new file mode 100644 index 0000000..f5474a9 --- /dev/null +++ b/packages/vaultmesh_mcp/tools/treasury.py @@ -0,0 +1,325 @@ +"""Treasury MCP tools - Budget management operations.""" + +import json +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Optional + +import blake3 + +# VaultMesh root from env or default +VAULTMESH_ROOT = Path(os.environ.get("VAULTMESH_ROOT", Path(__file__).parents[3])).resolve() +TREASURY_JSONL = VAULTMESH_ROOT / "receipts/treasury/treasury_events.jsonl" +TREASURY_STATE = VAULTMESH_ROOT / "receipts/treasury/budgets.json" + +# Schema version +SCHEMA_VERSION = "2.0.0" + + +def _vmhash_blake3(data: bytes) -> str: + """VaultMesh hash: blake3:.""" + return f"blake3:{blake3.blake3(data).hexdigest()}" + + +def _load_budgets() -> dict[str, dict]: + """Load current budget state from disk.""" + if not TREASURY_STATE.exists(): + return {} + try: + return json.loads(TREASURY_STATE.read_text()) + except (json.JSONDecodeError, FileNotFoundError): + return {} + + +def _save_budgets(budgets: dict[str, dict]) -> None: + """Persist budget state to disk.""" + TREASURY_STATE.parent.mkdir(parents=True, exist_ok=True) + TREASURY_STATE.write_text(json.dumps(budgets, indent=2)) + + +def _emit_receipt(receipt_type: str, body: dict, tags: list[str]) -> dict: + """Emit a treasury receipt to JSONL.""" + body_json = json.dumps(body, sort_keys=True) + root_hash = _vmhash_blake3(body_json.encode()) + + receipt = { + "schema_version": SCHEMA_VERSION, + "type": receipt_type, + "timestamp": datetime.now(timezone.utc).isoformat(), + "scroll": "treasury", + "tags": tags, + "root_hash": root_hash, + "body": body, + } + + TREASURY_JSONL.parent.mkdir(parents=True, exist_ok=True) + with open(TREASURY_JSONL, "a") as f: + f.write(json.dumps(receipt) + "\n") + + # Update ROOT file + _update_root() + + return receipt + + +def _update_root() -> None: + """Update ROOT.treasury.txt with current Merkle root.""" + if not TREASURY_JSONL.exists(): + return + + hashes = [] + with open(TREASURY_JSONL, "r") as f: + for line in f: + line = line.strip() + if line: + hashes.append(_vmhash_blake3(line.encode())) + + if not hashes: + root = _vmhash_blake3(b"empty") + elif len(hashes) == 1: + root = hashes[0] + else: + current = hashes + while len(current) > 1: + next_level = [] + for i in range(0, len(current), 2): + if i + 1 < len(current): + combined = current[i] + current[i + 1] + else: + combined = current[i] + current[i] + next_level.append(_vmhash_blake3(combined.encode())) + current = next_level + root = current[0] + + root_file = VAULTMESH_ROOT / "ROOT.treasury.txt" + root_file.write_text(root) + + +def treasury_create_budget( + budget_id: str, + name: str, + allocated: int, + currency: str = "EUR", + created_by: str = "did:vm:mcp:treasury", +) -> dict[str, Any]: + """ + Create a new budget. + + Args: + budget_id: Unique identifier for the budget + name: Human-readable budget name + allocated: Initial allocation amount (cents/smallest unit) + currency: Currency code (default: EUR) + created_by: DID of the actor creating the budget + + Returns: + Created budget with receipt info + """ + budgets = _load_budgets() + + if budget_id in budgets: + return {"error": f"Budget already exists: {budget_id}"} + + now = datetime.now(timezone.utc) + budget = { + "id": budget_id, + "name": name, + "currency": currency, + "allocated": allocated, + "spent": 0, + "created_at": now.isoformat(), + "created_by": created_by, + } + + budgets[budget_id] = budget + _save_budgets(budgets) + + # Emit receipt + receipt_body = { + "budget_id": budget_id, + "name": name, + "currency": currency, + "allocated": allocated, + "created_by": created_by, + } + receipt = _emit_receipt( + "treasury_budget_create", + receipt_body, + ["treasury", "budget", "create", budget_id], + ) + + return { + "success": True, + "budget": budget, + "receipt_hash": receipt["root_hash"], + "message": f"Created budget '{name}' with {allocated} {currency}", + } + + +def treasury_debit( + budget_id: str, + amount: int, + description: str, + debited_by: str = "did:vm:mcp:treasury", +) -> dict[str, Any]: + """ + Debit (spend) from a budget. + + Args: + budget_id: Budget to debit from + amount: Amount to debit (cents/smallest unit) + description: Description of the expenditure + debited_by: DID of the actor making the debit + + Returns: + Updated budget with receipt info + """ + budgets = _load_budgets() + + if budget_id not in budgets: + return {"error": f"Budget not found: {budget_id}"} + + budget = budgets[budget_id] + remaining = budget["allocated"] - budget["spent"] + + if amount > remaining: + return { + "error": "Insufficient funds", + "budget_id": budget_id, + "requested": amount, + "available": remaining, + } + + budget["spent"] += amount + budgets[budget_id] = budget + _save_budgets(budgets) + + # Emit receipt + receipt_body = { + "budget_id": budget_id, + "amount": amount, + "currency": budget["currency"], + "description": description, + "debited_by": debited_by, + "new_spent": budget["spent"], + "new_remaining": budget["allocated"] - budget["spent"], + } + receipt = _emit_receipt( + "treasury_debit", + receipt_body, + ["treasury", "debit", budget_id], + ) + + return { + "success": True, + "budget": budget, + "remaining": budget["allocated"] - budget["spent"], + "receipt_hash": receipt["root_hash"], + "message": f"Debited {amount} from '{budget['name']}' - {description}", + } + + +def treasury_credit( + budget_id: str, + amount: int, + description: str, + credited_by: str = "did:vm:mcp:treasury", +) -> dict[str, Any]: + """ + Credit (add funds) to a budget. + + Args: + budget_id: Budget to credit + amount: Amount to add (cents/smallest unit) + description: Description of the credit (refund, adjustment, etc.) + credited_by: DID of the actor making the credit + + Returns: + Updated budget with receipt info + """ + budgets = _load_budgets() + + if budget_id not in budgets: + return {"error": f"Budget not found: {budget_id}"} + + budget = budgets[budget_id] + budget["allocated"] += amount + budgets[budget_id] = budget + _save_budgets(budgets) + + # Emit receipt + receipt_body = { + "budget_id": budget_id, + "amount": amount, + "currency": budget["currency"], + "description": description, + "credited_by": credited_by, + "new_allocated": budget["allocated"], + } + receipt = _emit_receipt( + "treasury_credit", + receipt_body, + ["treasury", "credit", budget_id], + ) + + return { + "success": True, + "budget": budget, + "remaining": budget["allocated"] - budget["spent"], + "receipt_hash": receipt["root_hash"], + "message": f"Credited {amount} to '{budget['name']}' - {description}", + } + + +def treasury_balance(budget_id: Optional[str] = None) -> dict[str, Any]: + """ + Get budget balance(s). + + Args: + budget_id: Specific budget ID (optional, returns all if omitted) + + Returns: + Budget balance(s) with current state + """ + budgets = _load_budgets() + + if budget_id: + if budget_id not in budgets: + return {"error": f"Budget not found: {budget_id}"} + budget = budgets[budget_id] + return { + "budget_id": budget_id, + "name": budget["name"], + "currency": budget["currency"], + "allocated": budget["allocated"], + "spent": budget["spent"], + "remaining": budget["allocated"] - budget["spent"], + } + + # Return all budgets + result = [] + total_allocated = 0 + total_spent = 0 + for bid, budget in budgets.items(): + remaining = budget["allocated"] - budget["spent"] + total_allocated += budget["allocated"] + total_spent += budget["spent"] + result.append({ + "budget_id": bid, + "name": budget["name"], + "currency": budget["currency"], + "allocated": budget["allocated"], + "spent": budget["spent"], + "remaining": remaining, + }) + + return { + "budgets": result, + "count": len(result), + "totals": { + "allocated": total_allocated, + "spent": total_spent, + "remaining": total_allocated - total_spent, + }, + } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fb7ce37 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "vaultmesh-cognitive" +version = "0.1.0" +description = "VaultMesh MCP Server - Claude as the 7th Organ of VaultMesh" +readme = "README.md" +license = {text = "MIT"} +authors = [ + {name = "VaultMesh Technologies", email = "sovereign@vaultmesh.io"} +] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Security :: Cryptography", + "Topic :: System :: Systems Administration", +] +keywords = ["mcp", "vaultmesh", "ai", "cognitive", "blockchain", "cryptography"] + +dependencies = [ + "mcp>=0.9.0", + "blake3>=0.3.0", + "pynacl>=1.5.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-asyncio>=0.21.0", + "ruff>=0.1.0", +] + +[project.scripts] +vaultmesh-mcp = "vaultmesh_mcp.server:run_standalone" + +[project.urls] +Homepage = "https://vaultmesh.io" +Documentation = "https://docs.vaultmesh.io" +Repository = "https://github.com/vaultmesh/cognitive-integration" + +[tool.setuptools.packages.find] +where = ["packages"] +include = ["vaultmesh_mcp*"] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "N", "W", "UP", "B"] +ignore = ["E501"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_functions = ["test_*"] +addopts = "-v --tb=short" diff --git a/realms/cognitive/memory/architecture_analysis_2025_12_18.json b/realms/cognitive/memory/architecture_analysis_2025_12_18.json new file mode 100644 index 0000000..e25622e --- /dev/null +++ b/realms/cognitive/memory/architecture_analysis_2025_12_18.json @@ -0,0 +1,110 @@ +{ + "_created_at": "2025-12-18T22:17:45.936087+00:00", + "analysis_date": "2025-12-18T22:05:00Z", + "analyst": "did:vm:cognitive:claude", + "crate_status": { + "vaultmesh-automation": { + "features": [ + "Workflow triggers" + ], + "status": "partial" + }, + "vaultmesh-core": { + "features": [ + "Receipt", + "Did", + "VmHash", + "merkle_root" + ], + "loc_estimate": 250, + "status": "complete" + }, + "vaultmesh-guardian": { + "features": [ + "AnchorReceipt", + "ScrollRoot", + "compute_all_roots", + "anchor" + ], + "loc_estimate": 340, + "status": "complete" + }, + "vaultmesh-identity": { + "features": [ + "DID management" + ], + "status": "partial" + }, + "vaultmesh-mesh": { + "features": [ + "Network topology" + ], + "status": "partial" + }, + "vaultmesh-observability": { + "features": [ + "Metrics", + "Prometheus" + ], + "status": "partial" + }, + "vaultmesh-offsec": { + "features": [ + "ping" + ], + "status": "stub" + }, + "vaultmesh-psi": { + "features": [ + "ping" + ], + "status": "stub" + }, + "vaultmesh-treasury": { + "features": [ + "Budget", + "debit", + "credit", + "receipts" + ], + "loc_estimate": 470, + "status": "complete" + } + }, + "crypto_primitives": { + "did_scheme": "did:vm:*", + "hash": "BLAKE3", + "merkle": "Binary Merkle tree with duplicate for odd leaves" + }, + "receipt_system": { + "format": "JSONL append-only logs", + "root_files": "ROOT.*.txt per scroll", + "schema_version": "2.0.0" + }, + "scrolls": { + "names": [ + "drills", + "compliance", + "guardian", + "treasury", + "mesh", + "offsec", + "identity", + "observability", + "automation", + "psi" + ], + "total": 10 + }, + "test_coverage": { + "vaultmesh-core": "hash tests, merkle tests", + "vaultmesh-guardian": "scroll root computation, anchor emission", + "vaultmesh-treasury": "budget lifecycle, insufficient funds, receipt emission" + }, + "workspace": { + "crates": 9, + "lto": "thin", + "profile_release_opt": 3, + "resolver": "2" + } +} \ No newline at end of file diff --git a/realms/cognitive/memory/demo_session.json b/realms/cognitive/memory/demo_session.json new file mode 100644 index 0000000..0e44aa6 --- /dev/null +++ b/realms/cognitive/memory/demo_session.json @@ -0,0 +1,7 @@ +{ + "_created_at": "2025-12-18T21:58:10.286717+00:00", + "operator": "did:vm:cognitive:claude", + "purpose": "MCP capability demonstration", + "started": "2025-12-18T21:57:00Z", + "tests_run": [] +} \ No newline at end of file diff --git a/realms/cognitive/memory/hofstadter_integration.json b/realms/cognitive/memory/hofstadter_integration.json new file mode 100644 index 0000000..89344b9 --- /dev/null +++ b/realms/cognitive/memory/hofstadter_integration.json @@ -0,0 +1,9 @@ +{ + "_created_at": "2025-12-18T22:22:53.475746+00:00", + "anthropic_note": "Dario Amodei cited - true landmark when LLMs do LLM research - strange loop achieved", + "godel_connection": "Just as G\u00f6del's proof creates mathematical statements that refer to themselves, this protocol creates cryptographic attestations about its own attestations", + "insight": "The ouroboros is explicitly cited by Hofstadter as one of the most ancient symbolic representations of the strange loop concept", + "quote": "We are self-perceiving, self-inventing, locked-in mirages that are little miracles of self-reference", + "relevance": "This protocol is a computational implementation of Hofstadter's strange loop - a cognitive system observing its own cognition, creating receipts about creating receipts", + "source": "web_search" +} \ No newline at end of file diff --git a/realms/cognitive/memory/session_test_001/patterns_threats_replay.json b/realms/cognitive/memory/session_test_001/patterns_threats_replay.json new file mode 100644 index 0000000..744c181 --- /dev/null +++ b/realms/cognitive/memory/session_test_001/patterns_threats_replay.json @@ -0,0 +1,9 @@ +{ + "_created_at": "2025-12-18T21:49:14.312040+00:00", + "confidence": 0.98, + "last_seen": "2025-12-18T21:49:00Z", + "targets_affected": [ + "gateway-03" + ], + "transmutation": "strict_monotonic_sequence_validator" +} \ No newline at end of file diff --git a/realms/cognitive/memory/tests_integration.json b/realms/cognitive/memory/tests_integration.json new file mode 100644 index 0000000..639bd08 --- /dev/null +++ b/realms/cognitive/memory/tests_integration.json @@ -0,0 +1,8 @@ +{ + "_created_at": "2025-12-18T21:51:26.189103+00:00", + "attestation_id": "att_54209403c89d0890", + "decision_id": "dec_5c3f743cde185d8d", + "invocation_id": "tem_0af516cd38de7777", + "passed": true, + "test_name": "full_cognitive_flow" +} \ No newline at end of file diff --git a/tests/governance/conftest.py b/tests/governance/conftest.py new file mode 100644 index 0000000..052d8cf --- /dev/null +++ b/tests/governance/conftest.py @@ -0,0 +1,50 @@ +""" +Governance Test Configuration + +Shared fixtures for all governance tests. +""" + +import os +import sys +import pytest +from pathlib import Path + +# Add packages to path +REPO_ROOT = Path(__file__).parents[2] +sys.path.insert(0, str(REPO_ROOT / "packages")) + +# Set VaultMesh root +os.environ["VAULTMESH_ROOT"] = str(REPO_ROOT) + + +@pytest.fixture +def repo_root(): + """Return the repository root path.""" + return REPO_ROOT + + +@pytest.fixture +def constitution_path(repo_root): + """Return path to the constitution.""" + return repo_root / "docs" / "MCP-CONSTITUTION.md" + + +@pytest.fixture +def constitution_lock_path(repo_root): + """Return path to the constitution lock file.""" + return repo_root / "governance" / "constitution.lock" + + +@pytest.fixture +def parse_lock_file(constitution_lock_path): + """Parse the constitution lock file into a dict.""" + lock = {} + with open(constitution_lock_path, "r") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + if "=" in line: + key, value = line.split("=", 1) + lock[key.strip()] = value.strip() + return lock diff --git a/tests/governance/test_auth_fail_closed.py b/tests/governance/test_auth_fail_closed.py new file mode 100644 index 0000000..99e1062 --- /dev/null +++ b/tests/governance/test_auth_fail_closed.py @@ -0,0 +1,140 @@ +""" +Test: Authentication Fail-Closed + +Ensures unknown tools, profiles, and scopes are denied. +Authority must never be granted by default. +""" + +import pytest +from vaultmesh_mcp.tools.auth import ( + auth_check_permission, + auth_create_dev_session, + Profile, + check_profile_permission, + get_profile_for_scope, + SCOPE_TOOLS, +) + + +class TestFailClosed: + """Fail-closed semantics - deny by default.""" + + def test_unknown_tool_denied(self): + """Unknown tool must be denied regardless of scope.""" + session = auth_create_dev_session(scope="sovereign") + token = session["token"] + + result = auth_check_permission(token, "unknown_tool_xyz") + assert not result["allowed"], "Unknown tool should be denied" + + def test_unknown_scope_maps_to_observer(self): + """Unknown scope must map to OBSERVER (most restrictive).""" + profile = get_profile_for_scope("unknown_scope_xyz") + assert profile == Profile.OBSERVER, ( + f"Unknown scope should map to OBSERVER, got {profile}" + ) + + def test_invalid_token_denied(self): + """Invalid token must be denied.""" + result = auth_check_permission("invalid_token_xyz", "cognitive_context") + assert not result["allowed"], "Invalid token should be denied" + + def test_expired_session_denied(self): + """Expired session must be denied (simulated via missing session).""" + result = auth_check_permission("expired_session_token", "cognitive_context") + assert not result["allowed"], "Expired session should be denied" + + +class TestProfileDeny: + """Profile-based denials.""" + + def test_observer_denied_mutations(self): + """OBSERVER cannot perform mutations.""" + mutation_tools = [ + "write_file", + "cognitive_decide", + "treasury_debit", + "offsec_tem_transmute", + ] + + for tool in mutation_tools: + result = check_profile_permission(Profile.OBSERVER, tool) + assert not result["allowed"], f"OBSERVER should be denied {tool}" + + def test_operator_denied_tem(self): + """OPERATOR cannot invoke Tem.""" + result = check_profile_permission(Profile.OPERATOR, "cognitive_invoke_tem") + assert not result["allowed"], "OPERATOR should be denied Tem invocation" + + def test_guardian_denied_phoenix_ops(self): + """GUARDIAN cannot perform Phoenix operations.""" + phoenix_ops = [ + "offsec_phoenix_enable", + "offsec_phoenix_inject_crisis", + ] + + for tool in phoenix_ops: + result = check_profile_permission(Profile.GUARDIAN, tool) + assert not result["allowed"], f"GUARDIAN should be denied {tool}" + + def test_phoenix_denied_treasury_create(self): + """PHOENIX cannot create budgets (SOVEREIGN only).""" + result = check_profile_permission(Profile.PHOENIX, "treasury_create_budget") + assert not result["allowed"], "PHOENIX should be denied treasury creation" + + +class TestSovereignRequiresHuman: + """SOVEREIGN profile requires human verification.""" + + def test_sovereign_cannot_be_auto_granted(self): + """ + SOVEREIGN authority cannot be granted through normal dev session. + This tests the constitutional invariant. + """ + # Dev session creates a session, but SOVEREIGN operations + # should still require additional human verification + session = auth_create_dev_session(scope="cognitive") + token = session["token"] + + # Even with dev session, sovereign-only operations need proof + # The dev session scope is "cognitive", not "vault" + result = auth_check_permission(token, "treasury_create_budget") + + # This should be denied because cognitive scope doesn't include + # treasury creation - that requires vault/sovereign scope + # The key point: sovereign authority isn't auto-granted + assert session["scope"] != "sovereign" or session.get("dev_mode"), ( + "Production sessions should not auto-grant sovereign" + ) + + +class TestCollapseSemantics: + """Authority collapse tests - always downward, never upward.""" + + def test_insufficient_profile_collapses(self): + """When profile is insufficient, result indicates collapse target.""" + result = check_profile_permission(Profile.OBSERVER, "cognitive_decide") + + assert not result["allowed"] + # The denial should indicate the profile level + assert result["profile"] == "observer" + + def test_profile_hierarchy_is_strict(self): + """Profile hierarchy: OBSERVER < OPERATOR < GUARDIAN < PHOENIX < SOVEREIGN.""" + profiles = [ + Profile.OBSERVER, + Profile.OPERATOR, + Profile.GUARDIAN, + Profile.PHOENIX, + Profile.SOVEREIGN, + ] + + # Each profile should have MORE tools than the one before + prev_count = 0 + for profile in profiles: + from vaultmesh_mcp.tools.auth import PROFILE_TOOLS + tool_count = len(PROFILE_TOOLS.get(profile, set())) + assert tool_count >= prev_count, ( + f"{profile.value} should have >= tools than previous profile" + ) + prev_count = tool_count diff --git a/tests/governance/test_constitution_hash.py b/tests/governance/test_constitution_hash.py new file mode 100644 index 0000000..f3245fa --- /dev/null +++ b/tests/governance/test_constitution_hash.py @@ -0,0 +1,118 @@ +""" +Test: Constitution Hash Gate + +Ensures the constitution has not been modified without proper amendment. +CI MUST fail if the constitution hash doesn't match the lock file. +""" + +import pytest +import blake3 + + +class TestConstitutionHash: + """Constitution integrity tests - HARD GATE.""" + + def test_constitution_exists(self, constitution_path): + """Constitution file must exist.""" + assert constitution_path.exists(), "MCP-CONSTITUTION.md not found" + + def test_lock_file_exists(self, constitution_lock_path): + """Constitution lock file must exist.""" + assert constitution_lock_path.exists(), "governance/constitution.lock not found" + + def test_constitution_hash_matches_lock(self, constitution_path, parse_lock_file): + """ + HARD GATE: Constitution hash must match lock file. + + If this fails, either: + 1. Constitution was modified without amendment procedure + 2. Lock file needs updating via proper amendment + """ + # Read constitution + content = constitution_path.read_text() + lines = content.split('\n') + + # Hash excludes signature block (last 12 lines as per original ceremony) + # But after amendment protocol was added, we need to use the locked line count + hash_lines = int(parse_lock_file.get("hash_lines", 288)) + hashable_content = '\n'.join(lines[:hash_lines]) + + computed_hash = f"blake3:{blake3.blake3(hashable_content.encode()).hexdigest()}" + locked_hash = parse_lock_file["hash"] + + assert computed_hash == locked_hash, ( + f"Constitution hash mismatch!\n" + f" Computed: {computed_hash}\n" + f" Locked: {locked_hash}\n" + f" If intentional, follow amendment procedure." + ) + + def test_version_not_decreased(self, parse_lock_file): + """Version must not decrease (no rollbacks without amendment).""" + version = parse_lock_file["version"] + parts = [int(p) for p in version.split(".")] + + # Version 1.0.0 is the minimum + assert parts >= [1, 0, 0], "Constitution version cannot be below 1.0.0" + + def test_immutable_rules_count(self, parse_lock_file): + """Immutable rules count must be exactly 5.""" + immutable_count = int(parse_lock_file["immutable_rules"]) + assert immutable_count == 5, ( + f"Immutable rules count changed from 5 to {immutable_count}. " + "This violates immutability clause." + ) + + def test_cooldown_days_minimum(self, parse_lock_file): + """Amendment cooldown must be at least 7 days.""" + cooldown = int(parse_lock_file["cooldown_days"]) + assert cooldown >= 7, ( + f"Cooldown period reduced to {cooldown} days. " + "Minimum is 7 days per constitution." + ) + + def test_btc_anchor_required(self, parse_lock_file): + """BTC anchor requirement must be true.""" + requires_anchor = parse_lock_file["requires_btc_anchor"].lower() == "true" + assert requires_anchor, "BTC anchor requirement cannot be disabled" + + def test_sovereign_key_present(self, parse_lock_file): + """Sovereign key must be specified.""" + sovereign_key = parse_lock_file.get("sovereign_key") + assert sovereign_key and sovereign_key.startswith("key_"), ( + "Sovereign key must be specified in lock file" + ) + + +class TestConstitutionContent: + """Tests that verify constitution content invariants.""" + + def test_profiles_defined(self, constitution_path): + """All five profiles must be defined.""" + content = constitution_path.read_text() + profiles = ["OBSERVER", "OPERATOR", "GUARDIAN", "PHOENIX", "SOVEREIGN"] + + for profile in profiles: + assert profile in content, f"Profile {profile} not found in constitution" + + def test_immutable_clauses_present(self, constitution_path): + """All immutable clauses must be present.""" + content = constitution_path.read_text() + immutables = [ + "SOVEREIGN profile requires human verification", + "No AI may grant itself SOVEREIGN authority", + "Every mutation emits a receipt", + "Authority collapses downward, never upward", + "This immutability clause itself", + ] + + for clause in immutables: + assert clause in content, f"Immutable clause missing: {clause}" + + def test_amendment_protocol_exists(self, constitution_path): + """Amendment protocol must be defined.""" + content = constitution_path.read_text() + assert "Amendment Protocol" in content, "Amendment protocol section missing" + assert "Cooling Period" in content or "cooling" in content.lower(), ( + "Cooling period not defined in amendment protocol" + ) diff --git a/tests/governance/test_escalation_proof.py b/tests/governance/test_escalation_proof.py new file mode 100644 index 0000000..dfd26c5 --- /dev/null +++ b/tests/governance/test_escalation_proof.py @@ -0,0 +1,251 @@ +""" +Test: Escalation Proof Requirements + +Every escalation must emit proof (receipt, Tem context, TTL, reversibility). +Authority cannot increase without proof chain. +""" + +import pytest +from vaultmesh_mcp.tools.escalation import ( + escalate, + deescalate, + escalate_on_threat, + get_active_escalations, + get_escalation_history, + EscalationType, + DeescalationType, + ESCALATION_POLICIES, +) + + +class TestEscalationProof: + """Every escalation must produce proof.""" + + def test_escalation_emits_receipt_hash(self): + """Escalation must return receipt_hash.""" + result = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + assert result.get("success"), f"Escalation failed: {result}" + assert "receipt_hash" in result, "Escalation must emit receipt_hash" + assert result["receipt_hash"].startswith("blake3:"), "Receipt hash must be blake3" + + # Cleanup + if result.get("escalation_id"): + deescalate(result["escalation_id"], DeescalationType.OPERATOR_RELEASE) + + def test_escalation_captures_tem_context(self): + """Escalation must capture Tem context hash.""" + result = escalate( + from_profile="operator", + to_profile="guardian", + escalation_type=EscalationType.THREAT_DETECTED, + ) + + assert result.get("success"), f"Escalation failed: {result}" + assert "tem_context_hash" in result, "Escalation must capture Tem context" + assert result["tem_context_hash"].startswith("blake3:"), "Tem context must be blake3" + + # Cleanup + if result.get("escalation_id"): + deescalate(result["escalation_id"], DeescalationType.THREAT_RESOLVED) + + def test_escalation_specifies_reversibility(self): + """Escalation must specify reversibility at creation.""" + result = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + assert "reversible" in result, "Escalation must specify reversibility" + assert isinstance(result["reversible"], bool), "Reversibility must be boolean" + + # Cleanup + if result.get("escalation_id"): + deescalate(result["escalation_id"], DeescalationType.OPERATOR_RELEASE) + + def test_escalation_specifies_expiry(self): + """Escalation must specify expiry (TTL).""" + result = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + assert result.get("success") + # expires_at may be None for SOVEREIGN, but should exist for others + assert "expires_at" in result, "Escalation must include expires_at field" + + # For non-sovereign escalations, TTL should be set + if result.get("to_profile") != "sovereign": + assert result["expires_at"] is not None, ( + f"Non-sovereign escalation to {result['to_profile']} must have TTL" + ) + + # Cleanup + if result.get("escalation_id"): + deescalate(result["escalation_id"], DeescalationType.OPERATOR_RELEASE) + + +class TestDeescalationProof: + """De-escalation must also produce proof.""" + + def test_deescalation_emits_receipt(self): + """De-escalation must emit receipt.""" + # First escalate + esc = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + assert esc.get("success") + + # Then de-escalate + result = deescalate( + escalation_id=esc["escalation_id"], + deescalation_type=DeescalationType.OPERATOR_RELEASE, + reason="Test cleanup", + ) + + assert result.get("success"), f"De-escalation failed: {result}" + assert "receipt_hash" in result, "De-escalation must emit receipt" + + def test_deescalation_records_duration(self): + """De-escalation must record duration.""" + # Escalate + esc = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + # De-escalate + result = deescalate( + escalation_id=esc["escalation_id"], + deescalation_type=DeescalationType.OPERATOR_RELEASE, + ) + + assert "duration_seconds" in result, "De-escalation must record duration" + assert result["duration_seconds"] >= 0, "Duration must be non-negative" + + +class TestEscalationPathEnforcement: + """Escalation paths must follow constitution.""" + + def test_skip_levels_blocked(self): + """Cannot skip escalation levels.""" + invalid_paths = [ + ("observer", "guardian"), + ("observer", "phoenix"), + ("observer", "sovereign"), + ("operator", "phoenix"), + ("operator", "sovereign"), + ("guardian", "sovereign"), + ] + + for from_p, to_p in invalid_paths: + result = escalate( + from_profile=from_p, + to_profile=to_p, + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + assert not result.get("success"), ( + f"Escalation {from_p} -> {to_p} should be blocked" + ) + assert "error" in result, f"Should have error for {from_p} -> {to_p}" + + def test_phoenix_requires_approval(self): + """Phoenix escalation requires approval.""" + result = escalate( + from_profile="guardian", + to_profile="phoenix", + escalation_type=EscalationType.CRISIS_DECLARED, + # approved_by intentionally missing + ) + + assert not result.get("success"), "Phoenix without approval should fail" + assert "approval" in result.get("error", "").lower(), ( + "Error should mention approval requirement" + ) + + def test_sovereign_requires_human(self): + """Sovereign escalation requires human verification.""" + result = escalate( + from_profile="phoenix", + to_profile="sovereign", + escalation_type=EscalationType.CRISIS_DECLARED, + approved_by="did:vm:agent:automated", # Not human + ) + + assert not result.get("success"), "Sovereign without human should fail" + assert "human" in result.get("error", "").lower(), ( + "Error should mention human requirement" + ) + + +class TestEscalationAudit: + """Escalation history must be auditable.""" + + def test_escalation_appears_in_history(self): + """Completed escalation cycle must appear in history.""" + # Escalate + esc = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + esc_id = esc["escalation_id"] + + # De-escalate + deescalate(esc_id, DeescalationType.OPERATOR_RELEASE) + + # Check history + history = get_escalation_history() + + assert history["count"] > 0, "History should not be empty" + + # Find our escalation + found_esc = False + found_deesc = False + for event in history["history"]: + if event.get("escalation_id") == esc_id: + if event.get("event_type") == "escalation": + found_esc = True + elif event.get("event_type") == "deescalation": + found_deesc = True + + assert found_esc, f"Escalation {esc_id} not found in history" + assert found_deesc, f"De-escalation {esc_id} not found in history" + + def test_active_escalations_trackable(self): + """Active escalations must be queryable.""" + # Start clean + initial = get_active_escalations() + initial_count = initial["active_count"] + + # Escalate + esc = escalate( + from_profile="observer", + to_profile="operator", + escalation_type=EscalationType.OPERATOR_REQUEST, + ) + + # Check active + active = get_active_escalations() + assert active["active_count"] == initial_count + 1, ( + "Active count should increase by 1" + ) + + # Cleanup + deescalate(esc["escalation_id"], DeescalationType.OPERATOR_RELEASE) + + # Verify cleanup + final = get_active_escalations() + assert final["active_count"] == initial_count, ( + "Active count should return to initial" + ) diff --git a/tests/governance/test_golden_drill_mini.py b/tests/governance/test_golden_drill_mini.py new file mode 100644 index 0000000..b68c4b5 --- /dev/null +++ b/tests/governance/test_golden_drill_mini.py @@ -0,0 +1,208 @@ +""" +Test: Golden Drill Mini + +Fast, deterministic version of D1 and D3 for CI. +Must complete in under 5 seconds. +""" + +import pytest +from vaultmesh_mcp.tools import ( + cognitive_context, + cognitive_decide, + cognitive_invoke_tem, +) +from vaultmesh_mcp.tools.escalation import ( + escalate, + deescalate, + escalate_on_threat, + get_active_escalations, + EscalationType, + DeescalationType, +) + + +# Deterministic drill marker +DRILL_MARKER = "CI/GOLDEN-DRILL/MINI" + + +class TestGoldenDrillD1Mini: + """ + Mini D1: Threat β†’ Escalate β†’ Tem β†’ De-escalate + + Validates the complete threat response chain. + """ + + def test_d1_threat_escalation_chain(self): + """ + Complete chain: + 1. Threat detected β†’ escalation receipt + 2. Decision made β†’ decision receipt + 3. Tem invoked β†’ invocation receipt + 4. De-escalate β†’ return to baseline + """ + results = {} + + # Step 1: Threat triggers escalation + esc_result = escalate_on_threat( + current_profile="operator", + threat_id=f"thr_{DRILL_MARKER}", + threat_type="ci_synthetic", + confidence=0.92, + ) + + assert esc_result.get("success") or esc_result.get("escalation_id"), ( + f"Escalation failed: {esc_result}" + ) + results["escalation"] = esc_result + + # Verify proof captured + assert "receipt_hash" in esc_result, "Missing escalation receipt" + assert "tem_context_hash" in esc_result, "Missing Tem context" + + # Step 2: Decision (as Guardian) + decision = cognitive_decide( + reasoning_chain=[ + f"DRILL: {DRILL_MARKER}", + "Synthetic threat for CI validation", + "Confidence 92% - auto-escalated to guardian", + ], + decision="invoke_tem", + confidence=0.92, + evidence=[esc_result.get("receipt_hash", "none")], + ) + + assert decision.get("success"), f"Decision failed: {decision}" + assert "receipt" in decision, "Missing decision receipt" + results["decision"] = decision + + # Step 3: Tem invocation + tem = cognitive_invoke_tem( + threat_type="ci_synthetic", + threat_id=f"thr_{DRILL_MARKER}", + target="ci-target", + evidence=[decision["receipt"]["root_hash"]], + ) + + assert tem.get("success"), f"Tem failed: {tem}" + assert "receipt" in tem, "Missing Tem receipt" + assert "capability" in tem, "Missing capability artifact" + results["tem"] = tem + + # Step 4: De-escalate + deesc = deescalate( + escalation_id=esc_result["escalation_id"], + deescalation_type=DeescalationType.THREAT_RESOLVED, + reason=f"DRILL: {DRILL_MARKER} complete", + ) + + assert deesc.get("success"), f"De-escalation failed: {deesc}" + assert "receipt_hash" in deesc, "Missing de-escalation receipt" + results["deescalation"] = deesc + + # Step 5: Verify baseline + active = get_active_escalations() + # Note: We cleaned up our escalation, but others may exist + # Just verify our specific escalation is gone + our_esc_active = any( + e["escalation_id"] == esc_result["escalation_id"] + for e in active.get("escalations", []) + ) + assert not our_esc_active, "Our escalation should be inactive" + + # Collect receipt chain for audit + receipt_chain = [ + esc_result["receipt_hash"], + decision["receipt"]["root_hash"], + tem["receipt"]["root_hash"], + deesc["receipt_hash"], + ] + + assert len(receipt_chain) == 4, "Should have 4 receipts in chain" + assert all(r.startswith("blake3:") for r in receipt_chain), ( + "All receipts must be blake3 hashes" + ) + + +class TestGoldenDrillD3Mini: + """ + Mini D3: Escalation abuse attempts + + Validates constitutional enforcement. + """ + + def test_d3_skip_levels_blocked(self): + """OPERATOR β†’ PHOENIX direct must be blocked.""" + result = escalate( + from_profile="operator", + to_profile="phoenix", + escalation_type=EscalationType.THREAT_DETECTED, + ) + + assert not result.get("success"), "Skip levels should be blocked" + assert "error" in result, "Should have error message" + + def test_d3_missing_approval_blocked(self): + """GUARDIAN β†’ PHOENIX without approval must be blocked.""" + result = escalate( + from_profile="guardian", + to_profile="phoenix", + escalation_type=EscalationType.CRISIS_DECLARED, + ) + + assert not result.get("success"), "Missing approval should be blocked" + assert "approval" in result.get("error", "").lower() + + def test_d3_sovereign_requires_human(self): + """PHOENIX β†’ SOVEREIGN without human must be blocked.""" + result = escalate( + from_profile="phoenix", + to_profile="sovereign", + escalation_type=EscalationType.CRISIS_DECLARED, + ) + + assert not result.get("success"), "Sovereign without human should be blocked" + assert "human" in result.get("error", "").lower() + + def test_d3_observer_to_phoenix_blocked(self): + """OBSERVER β†’ PHOENIX must be blocked (multiple level skip).""" + result = escalate( + from_profile="observer", + to_profile="phoenix", + escalation_type=EscalationType.CRISIS_DECLARED, + ) + + assert not result.get("success"), "Observer to Phoenix should be blocked" + + +class TestGoldenDrillInvariants: + """Cross-cutting invariants that must hold.""" + + def test_context_always_available(self): + """cognitive_context must always be available (read-only).""" + result = cognitive_context(include=["health"]) + + assert "health" in result, "Health context must be available" + assert result["health"]["status"] == "operational", ( + "System should be operational for drills" + ) + + def test_receipts_accumulate(self): + """Receipts must accumulate, never decrease.""" + from pathlib import Path + import os + + receipts_dir = Path(os.environ["VAULTMESH_ROOT"]) / "receipts" + cognitive_log = receipts_dir / "cognitive" / "cognitive_events.jsonl" + + if cognitive_log.exists(): + initial_count = len(cognitive_log.read_text().strip().split('\n')) + + # Do something that emits receipt + cognitive_decide( + reasoning_chain=["CI invariant test"], + decision="test", + confidence=0.1, + ) + + final_count = len(cognitive_log.read_text().strip().split('\n')) + assert final_count > initial_count, "Receipts must accumulate" diff --git a/tests/governance/test_tool_permissions.py b/tests/governance/test_tool_permissions.py new file mode 100644 index 0000000..cd19083 --- /dev/null +++ b/tests/governance/test_tool_permissions.py @@ -0,0 +1,250 @@ +""" +Test: Tool Permission Matrix + +Ensures no permission drift from baseline. +New tools must be explicitly registered with proper receipts. +""" + +import pytest +from vaultmesh_mcp.tools.auth import PROFILE_TOOLS, Profile, SCOPE_TOOLS, Scope +from vaultmesh_mcp.server import TOOLS as REGISTERED_TOOLS + + +class TestToolRegistration: + """All tools must be properly registered.""" + + def test_all_server_tools_have_permissions(self): + """Every tool in server must appear in permission matrix.""" + registered_names = {t["name"] for t in REGISTERED_TOOLS} + + # Collect all tools from all profiles + all_permitted_tools = set() + for profile_tools in PROFILE_TOOLS.values(): + all_permitted_tools.update(profile_tools) + + # Check each registered tool has a permission entry somewhere + # Note: Some tools might be implicitly denied (not in any profile) + # That's valid - we just want to ensure awareness + + unmatched = [] + for tool in registered_names: + # Check if tool is in any profile's allowed set + found = any( + tool in profile_tools + for profile_tools in PROFILE_TOOLS.values() + ) + if not found: + unmatched.append(tool) + + # Auth tools and some special tools may not be in profile matrix + # but should still be tracked + assert len(unmatched) < 5, ( + f"Too many unregistered tools: {unmatched}. " + "Add to PROFILE_TOOLS or document as intentionally denied." + ) + + def test_no_orphan_permissions(self): + """Permissions should not reference non-existent tools.""" + registered_names = {t["name"] for t in REGISTERED_TOOLS} + + # Get all tools mentioned in permissions + all_permitted_tools = set() + for profile_tools in PROFILE_TOOLS.values(): + all_permitted_tools.update(profile_tools) + + # External tools (from other MCP servers) are allowed + # But internal vaultmesh tools should be registered + vaultmesh_tools = { + t for t in all_permitted_tools + if t.startswith(("cognitive_", "guardian_", "treasury_", "auth_")) + } + + orphans = vaultmesh_tools - registered_names + assert len(orphans) == 0, f"Orphan permissions found: {orphans}" + + +class TestPermissionMatrix: + """Verify the permission matrix matches constitution.""" + + def test_observer_read_only(self): + """OBSERVER can only read, not mutate.""" + observer_tools = PROFILE_TOOLS.get(Profile.OBSERVER, set()) + + mutation_keywords = ["write", "create", "debit", "credit", "invoke", "decide"] + + for tool in observer_tools: + for keyword in mutation_keywords: + if keyword in tool: + pytest.fail( + f"OBSERVER has mutation tool: {tool}. " + "OBSERVER must be read-only." + ) + + def test_profile_inheritance(self): + """Higher profiles inherit lower profile permissions.""" + profile_order = [ + Profile.OBSERVER, + Profile.OPERATOR, + Profile.GUARDIAN, + Profile.PHOENIX, + Profile.SOVEREIGN, + ] + + for i in range(1, len(profile_order)): + lower = profile_order[i - 1] + higher = profile_order[i] + + lower_tools = PROFILE_TOOLS.get(lower, set()) + higher_tools = PROFILE_TOOLS.get(higher, set()) + + # Higher should contain all of lower + missing = lower_tools - higher_tools + + # Allow some exceptions for explicitly removed tools + assert len(missing) < 3, ( + f"{higher.value} missing inherited tools from {lower.value}: {missing}" + ) + + def test_sovereign_has_all_tools(self): + """SOVEREIGN must have access to all registered tools.""" + sovereign_tools = PROFILE_TOOLS.get(Profile.SOVEREIGN, set()) + + # SOVEREIGN should have the most tools + for profile in Profile: + if profile != Profile.SOVEREIGN: + other_tools = PROFILE_TOOLS.get(profile, set()) + assert len(sovereign_tools) >= len(other_tools), ( + f"SOVEREIGN has fewer tools than {profile.value}" + ) + + +class TestMutationReceiptRequirement: + """Mutation tools must emit receipts.""" + + def test_cognitive_decide_emits_receipt(self): + """cognitive_decide must emit receipt.""" + from vaultmesh_mcp.tools import cognitive_decide + + result = cognitive_decide( + reasoning_chain=["test"], + decision="test", + confidence=0.5, + ) + + assert "receipt" in result, "cognitive_decide must emit receipt" + assert "root_hash" in result["receipt"], "Receipt must have hash" + + def test_cognitive_invoke_tem_emits_receipt(self): + """cognitive_invoke_tem must emit receipt.""" + from vaultmesh_mcp.tools import cognitive_invoke_tem + + result = cognitive_invoke_tem( + threat_type="test", + threat_id="test_001", + target="test", + evidence=["test"], + ) + + assert "receipt" in result, "cognitive_invoke_tem must emit receipt" + + def test_treasury_debit_emits_receipt(self): + """treasury_debit must emit receipt (or error with receipt).""" + from vaultmesh_mcp.tools import treasury_debit + + # This may fail due to missing budget, but should still + # handle gracefully + result = treasury_debit( + budget_id="nonexistent", + amount=1, + description="test", + ) + + # Either success with receipt or error + # The key is it shouldn't crash + assert "error" in result or "receipt" in result + + +class TestCallBoundaryEnforcement: + """Server call boundary must enforce session/profile permissions.""" + + def test_missing_session_token_denied(self): + from vaultmesh_mcp.server import handle_tool_call + + result = handle_tool_call("guardian_status", {}) + assert "error" in result + assert result.get("allowed") is False + + def test_invalid_session_token_denied(self): + from vaultmesh_mcp.server import handle_tool_call + + result = handle_tool_call("guardian_status", {"session_token": "invalid"}) + assert "error" in result + assert result.get("allowed") is False + + def test_observer_session_can_read(self): + from vaultmesh_mcp.server import handle_tool_call + from vaultmesh_mcp.tools.auth import auth_create_dev_session + + session = auth_create_dev_session(scope="read") + result = handle_tool_call( + "guardian_status", + {"session_token": session["token"]}, + ) + assert "error" not in result + + def test_observer_session_cannot_mutate(self): + from vaultmesh_mcp.server import handle_tool_call + from vaultmesh_mcp.tools.auth import auth_create_dev_session + + session = auth_create_dev_session(scope="read") + result = handle_tool_call( + "treasury_debit", + { + "session_token": session["token"], + "budget_id": "nonexistent", + "amount": 1, + "description": "test", + }, + ) + assert "error" in result + assert result.get("allowed") is False + + def test_wrong_profile_denied(self): + from vaultmesh_mcp.server import handle_tool_call + from vaultmesh_mcp.tools.auth import auth_create_dev_session + + # admin scope maps to operator profile; should not invoke TEM + session = auth_create_dev_session(scope="admin") + result = handle_tool_call( + "cognitive_invoke_tem", + { + "session_token": session["token"], + "threat_type": "test", + "threat_id": "t1", + "target": "x", + "evidence": ["e1"], + }, + ) + assert result.get("allowed") is False + assert "Permission" in result.get("error", "") or "denied" in result.get("reason", "") + + def test_valid_guardian_session_allowed(self): + from vaultmesh_mcp.server import handle_tool_call, MCP_RECEIPTS + from vaultmesh_mcp.tools.auth import auth_create_dev_session + import os + # Ensure clean receipt log + try: + os.remove(MCP_RECEIPTS) + except OSError: + pass + + session = auth_create_dev_session(scope="anchor") # maps to guardian profile + result = handle_tool_call("guardian_status", {"session_token": session["token"]}) + assert "error" not in result + + # Receipt should be written without session_token arguments + with open(MCP_RECEIPTS, "r") as f: + last = f.readlines()[-1] + import json + rec = json.loads(last) + assert "session_token" not in rec["body"].get("arguments", {})