Initial commit: Agent Governance System Phase 8
Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Preflight
|
||||
|
||||
> Pre-execution validation and checks
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains pre-execution validation and checks.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `preflight.py` | Python module |
|
||||
| `dependency_check.py` | Python module |
|
||||
| `inventory_check.py` | Python module |
|
||||
| `sandbox_assert.py` | Python module |
|
||||
| `__init__.py` | Python module |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Project Root](/opt/agent-governance)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,35 @@
|
||||
# Status: Preflight
|
||||
|
||||
## Current Phase
|
||||
|
||||
**COMPLETE**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ☐ | *No tasks defined* | - |
|
||||
|
||||
## Dependencies
|
||||
|
||||
*No external dependencies.*
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-23 23:25:38 UTC
|
||||
- **Phase**: COMPLETE
|
||||
- **Action**: Pre-execution validation checks active
|
||||
- **Details**: Phase updated to complete
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:38 UTC*
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
"""
|
||||
Preflight Check System
|
||||
======================
|
||||
Phase 3: Execution Pipeline - Preflight Validation
|
||||
|
||||
Components:
|
||||
- inventory_check: Validates targets exist in allowed pools
|
||||
- dependency_check: Verifies secrets and services available
|
||||
- sandbox_assert: Ensures targets are within sandbox boundaries
|
||||
- preflight: Unified gate combining all checks
|
||||
"""
|
||||
|
||||
from .inventory_check import InventoryChecker, CheckResult, CheckStatus
|
||||
from .dependency_check import DependencyChecker
|
||||
from .sandbox_assert import SandboxAsserter, AssertionResult, AssertionStatus
|
||||
from .preflight import PreflightGate
|
||||
|
||||
__all__ = [
|
||||
"InventoryChecker",
|
||||
"DependencyChecker",
|
||||
"SandboxAsserter",
|
||||
"PreflightGate",
|
||||
"CheckResult",
|
||||
"CheckStatus",
|
||||
"AssertionResult",
|
||||
"AssertionStatus",
|
||||
]
|
||||
Executable
+405
@@ -0,0 +1,405 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dependency Checker
|
||||
==================
|
||||
Verifies that all required secrets and resources are available before execution.
|
||||
Part of Phase 3: Execution Pipeline - Preflight System.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CheckStatus(str, Enum):
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
WARN = "WARN"
|
||||
SKIP = "SKIP"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
check_name: str
|
||||
status: CheckStatus
|
||||
message: str
|
||||
details: dict
|
||||
timestamp: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"check_name": self.check_name,
|
||||
"status": self.status.value,
|
||||
"message": self.message,
|
||||
"details": self.details,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
|
||||
class DependencyChecker:
|
||||
"""
|
||||
Validates that all required dependencies are available.
|
||||
|
||||
Checks:
|
||||
- Required Vault secrets exist
|
||||
- Required services are reachable
|
||||
- Required tools are installed
|
||||
- Required credentials are valid
|
||||
"""
|
||||
|
||||
# Standard dependencies by action type
|
||||
DEPENDENCIES = {
|
||||
"terraform": {
|
||||
"secrets": ["api-keys/proxmox", "ssh/sandbox"],
|
||||
"services": ["vault", "proxmox-api"],
|
||||
"tools": ["terraform"]
|
||||
},
|
||||
"ansible": {
|
||||
"secrets": ["ssh/sandbox", "inventory/proxmox"],
|
||||
"services": ["vault"],
|
||||
"tools": ["ansible-playbook"]
|
||||
},
|
||||
"docker": {
|
||||
"secrets": ["registry/credentials"],
|
||||
"services": ["vault", "docker-registry"],
|
||||
"tools": ["docker"]
|
||||
},
|
||||
"generic": {
|
||||
"secrets": [],
|
||||
"services": ["vault", "dragonfly"],
|
||||
"tools": []
|
||||
}
|
||||
}
|
||||
|
||||
# Service health endpoints
|
||||
SERVICE_ENDPOINTS = {
|
||||
"vault": ("https://127.0.0.1:8200/v1/sys/health", 200),
|
||||
"dragonfly": ("redis://127.0.0.1:6379", None),
|
||||
"proxmox-api": ("https://10.77.0.2:8006/api2/json/version", 200),
|
||||
"docker-registry": ("https://registry.local/v2/", 200),
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.vault_token = self._get_vault_token()
|
||||
|
||||
def _get_vault_token(self) -> str:
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
return json.load(f)["root_token"]
|
||||
|
||||
def _now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _vault_read(self, path: str) -> Optional[dict]:
|
||||
"""Read from Vault KV"""
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {self.vault_token}",
|
||||
f"https://127.0.0.1:8200/v1/secret/data/{path}"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if "data" in data and "data" in data["data"]:
|
||||
return data["data"]["data"]
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def check_vault_secret(self, secret_path: str) -> CheckResult:
|
||||
"""Check if a Vault secret exists and is readable"""
|
||||
data = self._vault_read(secret_path)
|
||||
|
||||
if data:
|
||||
# Don't expose actual secret values
|
||||
keys = list(data.keys())
|
||||
return CheckResult(
|
||||
check_name=f"secret:{secret_path}",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Secret '{secret_path}' exists with keys: {keys}",
|
||||
details={"path": secret_path, "keys": keys},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name=f"secret:{secret_path}",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Secret '{secret_path}' not found or not readable",
|
||||
details={"path": secret_path},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def check_service_health(self, service_name: str) -> CheckResult:
|
||||
"""Check if a service is reachable"""
|
||||
if service_name not in self.SERVICE_ENDPOINTS:
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.SKIP,
|
||||
message=f"No health endpoint defined for '{service_name}'",
|
||||
details={"service": service_name},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
endpoint, expected_code = self.SERVICE_ENDPOINTS[service_name]
|
||||
|
||||
# Special handling for Redis/Dragonfly
|
||||
if endpoint.startswith("redis://"):
|
||||
try:
|
||||
import redis
|
||||
host = endpoint.replace("redis://", "").split(":")[0]
|
||||
port = int(endpoint.split(":")[-1])
|
||||
|
||||
# Get password from Vault for dragonfly
|
||||
password = None
|
||||
if service_name == "dragonfly":
|
||||
creds = self._vault_read("services/dragonfly")
|
||||
if creds:
|
||||
password = creds.get("password")
|
||||
|
||||
r = redis.Redis(host=host, port=port, password=password, socket_timeout=5)
|
||||
r.ping()
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Service '{service_name}' is healthy (PING OK)",
|
||||
details={"service": service_name, "endpoint": endpoint},
|
||||
timestamp=self._now()
|
||||
)
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Service '{service_name}' is not reachable: {str(e)}",
|
||||
details={"service": service_name, "endpoint": endpoint, "error": str(e)},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
# HTTP health check
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}",
|
||||
"--connect-timeout", "5",
|
||||
endpoint
|
||||
], capture_output=True, text=True, timeout=10)
|
||||
|
||||
status_code = int(result.stdout.strip())
|
||||
|
||||
if expected_code is None or status_code == expected_code:
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Service '{service_name}' is healthy (HTTP {status_code})",
|
||||
details={"service": service_name, "endpoint": endpoint, "status_code": status_code},
|
||||
timestamp=self._now()
|
||||
)
|
||||
else:
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Service '{service_name}' returned unexpected status {status_code}",
|
||||
details={"service": service_name, "endpoint": endpoint, "status_code": status_code, "expected": expected_code},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return CheckResult(
|
||||
check_name=f"service:{service_name}",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Service '{service_name}' health check failed: {str(e)}",
|
||||
details={"service": service_name, "endpoint": endpoint, "error": str(e)},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def check_tool_installed(self, tool_name: str) -> CheckResult:
|
||||
"""Check if a tool is installed and available"""
|
||||
result = subprocess.run(
|
||||
["which", tool_name],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
path = result.stdout.strip()
|
||||
return CheckResult(
|
||||
check_name=f"tool:{tool_name}",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Tool '{tool_name}' found at {path}",
|
||||
details={"tool": tool_name, "path": path},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name=f"tool:{tool_name}",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Tool '{tool_name}' not found in PATH",
|
||||
details={"tool": tool_name},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def check_vault_token_valid(self) -> CheckResult:
|
||||
"""Check if the Vault token is valid"""
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {self.vault_token}",
|
||||
"https://127.0.0.1:8200/v1/auth/token/lookup-self"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if "data" in data:
|
||||
token_data = data["data"]
|
||||
policies = token_data.get("policies", [])
|
||||
ttl = token_data.get("ttl", 0)
|
||||
|
||||
if ttl > 0 or "root" in policies:
|
||||
return CheckResult(
|
||||
check_name="vault_token",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Vault token valid (TTL: {ttl}s, policies: {policies})",
|
||||
details={"ttl": ttl, "policies": policies},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name="vault_token",
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Vault token has no TTL (may be root token)",
|
||||
details={"ttl": ttl, "policies": policies},
|
||||
timestamp=self._now()
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
return CheckResult(
|
||||
check_name="vault_token",
|
||||
status=CheckStatus.FAIL,
|
||||
message="Vault token invalid or expired",
|
||||
details={},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def run_action_checks(self, action_type: str) -> list[CheckResult]:
|
||||
"""Run all dependency checks for an action type"""
|
||||
results = []
|
||||
|
||||
# Always check Vault token
|
||||
results.append(self.check_vault_token_valid())
|
||||
|
||||
deps = self.DEPENDENCIES.get(action_type, self.DEPENDENCIES["generic"])
|
||||
|
||||
# Check secrets
|
||||
for secret in deps.get("secrets", []):
|
||||
results.append(self.check_vault_secret(secret))
|
||||
|
||||
# Check services
|
||||
for service in deps.get("services", []):
|
||||
results.append(self.check_service_health(service))
|
||||
|
||||
# Check tools
|
||||
for tool in deps.get("tools", []):
|
||||
results.append(self.check_tool_installed(tool))
|
||||
|
||||
return results
|
||||
|
||||
def preflight_report(self, action_type: str, extra_secrets: list[str] = None) -> dict:
|
||||
"""Generate a full dependency preflight report"""
|
||||
report = {
|
||||
"report_type": "dependency_preflight",
|
||||
"action_type": action_type,
|
||||
"timestamp": self._now(),
|
||||
"checks": [],
|
||||
"summary": {
|
||||
"total_checks": 0,
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"warnings": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"can_proceed": True
|
||||
}
|
||||
|
||||
# Run standard checks
|
||||
results = self.run_action_checks(action_type)
|
||||
|
||||
# Check any extra secrets
|
||||
for secret in (extra_secrets or []):
|
||||
results.append(self.check_vault_secret(secret))
|
||||
|
||||
for r in results:
|
||||
report["checks"].append(r.to_dict())
|
||||
report["summary"]["total_checks"] += 1
|
||||
|
||||
if r.status == CheckStatus.PASS:
|
||||
report["summary"]["passed"] += 1
|
||||
elif r.status == CheckStatus.FAIL:
|
||||
report["summary"]["failed"] += 1
|
||||
report["can_proceed"] = False
|
||||
elif r.status == CheckStatus.WARN:
|
||||
report["summary"]["warnings"] += 1
|
||||
elif r.status == CheckStatus.SKIP:
|
||||
report["summary"]["skipped"] += 1
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Dependency Preflight Checker")
|
||||
parser.add_argument("action_type", choices=["terraform", "ansible", "docker", "generic"],
|
||||
help="Type of action to check dependencies for")
|
||||
parser.add_argument("--secret", action="append", dest="extra_secrets",
|
||||
help="Additional secrets to check (can be repeated)")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
checker = DependencyChecker()
|
||||
report = checker.preflight_report(args.action_type, args.extra_secrets)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("DEPENDENCY PREFLIGHT REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Action Type: {report['action_type']}")
|
||||
print(f"Timestamp: {report['timestamp']}")
|
||||
print()
|
||||
|
||||
for check in report["checks"]:
|
||||
status_icon = {
|
||||
"PASS": "[OK]",
|
||||
"FAIL": "[FAIL]",
|
||||
"WARN": "[WARN]",
|
||||
"SKIP": "[SKIP]"
|
||||
}.get(check["status"], "[?]")
|
||||
print(f" {status_icon} {check['check_name']}")
|
||||
print(f" {check['message']}")
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print("SUMMARY")
|
||||
print("-" * 60)
|
||||
s = report["summary"]
|
||||
print(f" Total Checks: {s['total_checks']}")
|
||||
print(f" Passed: {s['passed']}")
|
||||
print(f" Failed: {s['failed']}")
|
||||
print(f" Warnings: {s['warnings']}")
|
||||
print(f" Skipped: {s['skipped']}")
|
||||
print()
|
||||
|
||||
if report["can_proceed"]:
|
||||
print("[OK] DEPENDENCY CHECK PASSED - All dependencies available")
|
||||
else:
|
||||
print("[FAIL] DEPENDENCY CHECK FAILED - Missing dependencies")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(0 if report["can_proceed"] else 1)
|
||||
Executable
+330
@@ -0,0 +1,330 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Inventory Validator
|
||||
===================
|
||||
Validates that target resources exist and are in allowed pools.
|
||||
Part of Phase 3: Execution Pipeline - Preflight System.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CheckStatus(str, Enum):
|
||||
PASS = "PASS"
|
||||
FAIL = "FAIL"
|
||||
WARN = "WARN"
|
||||
SKIP = "SKIP"
|
||||
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
check_name: str
|
||||
status: CheckStatus
|
||||
message: str
|
||||
details: dict
|
||||
timestamp: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"check_name": self.check_name,
|
||||
"status": self.status.value,
|
||||
"message": self.message,
|
||||
"details": self.details,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
|
||||
class InventoryChecker:
|
||||
"""
|
||||
Validates inventory targets against allowed pools.
|
||||
|
||||
Checks:
|
||||
- Target exists in inventory
|
||||
- Target is in an allowed pool for the agent's tier
|
||||
- Target is not in a forbidden pool
|
||||
"""
|
||||
|
||||
POOL_TIERS = {
|
||||
"pve-sandbox": 1, # Tier 1+ can access
|
||||
"pve-staging": 2, # Tier 2+ can access
|
||||
"pve-prod": 3, # Tier 3+ with approval
|
||||
}
|
||||
|
||||
FORBIDDEN_POOLS = {
|
||||
0: ["pve-sandbox", "pve-staging", "pve-prod"], # Tier 0: no pools
|
||||
1: ["pve-staging", "pve-prod"], # Tier 1: only sandbox
|
||||
2: ["pve-prod"], # Tier 2: sandbox + staging
|
||||
3: [], # Tier 3: all with approval
|
||||
4: [], # Tier 4: all
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.vault_token = self._get_vault_token()
|
||||
|
||||
def _get_vault_token(self) -> str:
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
return json.load(f)["root_token"]
|
||||
|
||||
def _now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _vault_read(self, path: str) -> Optional[dict]:
|
||||
"""Read from Vault KV"""
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {self.vault_token}",
|
||||
f"https://127.0.0.1:8200/v1/secret/data/{path}"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if "data" in data and "data" in data["data"]:
|
||||
return data["data"]["data"]
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_inventory(self) -> dict:
|
||||
"""Get infrastructure inventory from Vault"""
|
||||
inventory = self._vault_read("inventory/proxmox")
|
||||
if not inventory:
|
||||
# Return mock inventory for testing
|
||||
return {
|
||||
"cluster": "pve-cluster-01",
|
||||
"pools": {
|
||||
"pve-sandbox": {
|
||||
"nodes": ["sandbox-vm-01", "sandbox-vm-02", "sandbox-vm-03"],
|
||||
"tier_required": 1
|
||||
},
|
||||
"pve-staging": {
|
||||
"nodes": ["staging-vm-01", "staging-vm-02"],
|
||||
"tier_required": 2
|
||||
},
|
||||
"pve-prod": {
|
||||
"nodes": ["prod-vm-01", "prod-vm-02", "prod-db-01"],
|
||||
"tier_required": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
return inventory
|
||||
|
||||
def check_target_exists(self, target: str) -> CheckResult:
|
||||
"""Check if target exists in any pool"""
|
||||
inventory = self.get_inventory()
|
||||
|
||||
for pool_name, pool_data in inventory.get("pools", {}).items():
|
||||
nodes = pool_data.get("nodes", [])
|
||||
if target in nodes:
|
||||
return CheckResult(
|
||||
check_name="target_exists",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Target '{target}' found in pool '{pool_name}'",
|
||||
details={"target": target, "pool": pool_name, "nodes": nodes},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name="target_exists",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Target '{target}' not found in any inventory pool",
|
||||
details={"target": target, "searched_pools": list(inventory.get("pools", {}).keys())},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def check_pool_access(self, target: str, agent_tier: int) -> CheckResult:
|
||||
"""Check if agent tier has access to target's pool"""
|
||||
inventory = self.get_inventory()
|
||||
|
||||
# Find which pool contains the target
|
||||
target_pool = None
|
||||
for pool_name, pool_data in inventory.get("pools", {}).items():
|
||||
if target in pool_data.get("nodes", []):
|
||||
target_pool = pool_name
|
||||
break
|
||||
|
||||
if not target_pool:
|
||||
return CheckResult(
|
||||
check_name="pool_access",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Target '{target}' not found in inventory",
|
||||
details={"target": target},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
# Check if pool is forbidden for this tier
|
||||
forbidden = self.FORBIDDEN_POOLS.get(agent_tier, [])
|
||||
if target_pool in forbidden:
|
||||
return CheckResult(
|
||||
check_name="pool_access",
|
||||
status=CheckStatus.FAIL,
|
||||
message=f"Tier {agent_tier} agents cannot access pool '{target_pool}'",
|
||||
details={
|
||||
"target": target,
|
||||
"pool": target_pool,
|
||||
"agent_tier": agent_tier,
|
||||
"forbidden_pools": forbidden
|
||||
},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name="pool_access",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Tier {agent_tier} has access to pool '{target_pool}'",
|
||||
details={
|
||||
"target": target,
|
||||
"pool": target_pool,
|
||||
"agent_tier": agent_tier
|
||||
},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def check_not_production(self, target: str) -> CheckResult:
|
||||
"""Warn if target is in production pool"""
|
||||
inventory = self.get_inventory()
|
||||
|
||||
for pool_name, pool_data in inventory.get("pools", {}).items():
|
||||
if target in pool_data.get("nodes", []):
|
||||
if "prod" in pool_name.lower():
|
||||
return CheckResult(
|
||||
check_name="not_production",
|
||||
status=CheckStatus.WARN,
|
||||
message=f"Target '{target}' is in PRODUCTION pool '{pool_name}'",
|
||||
details={
|
||||
"target": target,
|
||||
"pool": pool_name,
|
||||
"warning": "Production access requires explicit approval"
|
||||
},
|
||||
timestamp=self._now()
|
||||
)
|
||||
return CheckResult(
|
||||
check_name="not_production",
|
||||
status=CheckStatus.PASS,
|
||||
message=f"Target '{target}' is not in production",
|
||||
details={"target": target, "pool": pool_name},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
return CheckResult(
|
||||
check_name="not_production",
|
||||
status=CheckStatus.SKIP,
|
||||
message=f"Target '{target}' not found in inventory",
|
||||
details={"target": target},
|
||||
timestamp=self._now()
|
||||
)
|
||||
|
||||
def run_all_checks(self, target: str, agent_tier: int) -> list[CheckResult]:
|
||||
"""Run all inventory checks"""
|
||||
results = []
|
||||
|
||||
# Check 1: Target exists
|
||||
results.append(self.check_target_exists(target))
|
||||
|
||||
# Check 2: Pool access
|
||||
results.append(self.check_pool_access(target, agent_tier))
|
||||
|
||||
# Check 3: Production warning
|
||||
results.append(self.check_not_production(target))
|
||||
|
||||
return results
|
||||
|
||||
def preflight_report(self, targets: list[str], agent_tier: int) -> dict:
|
||||
"""Generate a full preflight report for multiple targets"""
|
||||
report = {
|
||||
"report_type": "inventory_preflight",
|
||||
"agent_tier": agent_tier,
|
||||
"timestamp": self._now(),
|
||||
"targets": {},
|
||||
"summary": {
|
||||
"total_checks": 0,
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"warnings": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
"can_proceed": True
|
||||
}
|
||||
|
||||
for target in targets:
|
||||
results = self.run_all_checks(target, agent_tier)
|
||||
report["targets"][target] = [r.to_dict() for r in results]
|
||||
|
||||
for r in results:
|
||||
report["summary"]["total_checks"] += 1
|
||||
if r.status == CheckStatus.PASS:
|
||||
report["summary"]["passed"] += 1
|
||||
elif r.status == CheckStatus.FAIL:
|
||||
report["summary"]["failed"] += 1
|
||||
report["can_proceed"] = False
|
||||
elif r.status == CheckStatus.WARN:
|
||||
report["summary"]["warnings"] += 1
|
||||
elif r.status == CheckStatus.SKIP:
|
||||
report["summary"]["skipped"] += 1
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Inventory Preflight Checker")
|
||||
parser.add_argument("targets", nargs="+", help="Target nodes to check")
|
||||
parser.add_argument("--tier", type=int, default=1, help="Agent tier (0-4)")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
checker = InventoryChecker()
|
||||
report = checker.preflight_report(args.targets, args.tier)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("INVENTORY PREFLIGHT REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Agent Tier: {report['agent_tier']}")
|
||||
print(f"Timestamp: {report['timestamp']}")
|
||||
print()
|
||||
|
||||
for target, checks in report["targets"].items():
|
||||
print(f"\nTarget: {target}")
|
||||
print("-" * 40)
|
||||
for check in checks:
|
||||
status_icon = {
|
||||
"PASS": "[OK]",
|
||||
"FAIL": "[FAIL]",
|
||||
"WARN": "[WARN]",
|
||||
"SKIP": "[SKIP]"
|
||||
}.get(check["status"], "[?]")
|
||||
print(f" {status_icon} {check['check_name']}: {check['message']}")
|
||||
|
||||
print("\n" + "-" * 60)
|
||||
print("SUMMARY")
|
||||
print("-" * 60)
|
||||
s = report["summary"]
|
||||
print(f" Total Checks: {s['total_checks']}")
|
||||
print(f" Passed: {s['passed']}")
|
||||
print(f" Failed: {s['failed']}")
|
||||
print(f" Warnings: {s['warnings']}")
|
||||
print(f" Skipped: {s['skipped']}")
|
||||
print()
|
||||
|
||||
if report["can_proceed"]:
|
||||
print("[OK] PREFLIGHT PASSED - Can proceed with execution")
|
||||
else:
|
||||
print("[FAIL] PREFLIGHT FAILED - Cannot proceed")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(0 if report["can_proceed"] else 1)
|
||||
Executable
+255
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unified Preflight System
|
||||
========================
|
||||
Combines all preflight checks into a single gate.
|
||||
Part of Phase 3: Execution Pipeline - Preflight System.
|
||||
|
||||
This is the main entry point for preflight validation.
|
||||
All execution must pass through this gate.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from inventory_check import InventoryChecker
|
||||
from dependency_check import DependencyChecker
|
||||
from sandbox_assert import SandboxAsserter, AssertionStatus
|
||||
|
||||
|
||||
@dataclass
|
||||
class PreflightGate:
|
||||
"""
|
||||
Unified preflight gate that blocks execution on any failure.
|
||||
|
||||
Gate Order:
|
||||
1. Sandbox Assertion (CRITICAL - must pass first)
|
||||
2. Inventory Validation
|
||||
3. Dependency Check
|
||||
|
||||
If any check fails, execution is BLOCKED.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.inventory_checker = InventoryChecker()
|
||||
self.dependency_checker = DependencyChecker()
|
||||
self.sandbox_asserter = SandboxAsserter()
|
||||
|
||||
def _now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def run_preflight(
|
||||
self,
|
||||
targets: list[str],
|
||||
action_type: str,
|
||||
agent_tier: int,
|
||||
agent_id: str,
|
||||
extra_secrets: list[str] = None,
|
||||
allow_staging: bool = False,
|
||||
production_override: bool = False
|
||||
) -> dict:
|
||||
"""
|
||||
Run full preflight validation.
|
||||
|
||||
Args:
|
||||
targets: List of target nodes
|
||||
action_type: Type of action (terraform, ansible, docker, generic)
|
||||
agent_tier: Agent's trust tier (0-4)
|
||||
agent_id: Agent identifier for audit
|
||||
extra_secrets: Additional secrets to validate
|
||||
allow_staging: Allow staging targets
|
||||
production_override: DANGEROUS: Allow production
|
||||
|
||||
Returns:
|
||||
Comprehensive preflight report with gate decision
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
report = {
|
||||
"report_type": "unified_preflight",
|
||||
"agent_id": agent_id,
|
||||
"agent_tier": agent_tier,
|
||||
"action_type": action_type,
|
||||
"targets": targets,
|
||||
"timestamp": self._now(),
|
||||
"gates": {},
|
||||
"gate_order": ["sandbox", "inventory", "dependencies"],
|
||||
"failed_gates": [],
|
||||
"can_proceed": True,
|
||||
"decision": "PENDING",
|
||||
"blocking_reason": None
|
||||
}
|
||||
|
||||
# =====================================================================
|
||||
# GATE 1: Sandbox Assertion (CRITICAL)
|
||||
# =====================================================================
|
||||
print("\n[GATE 1/3] Sandbox Assertion...")
|
||||
|
||||
sandbox_report = self.sandbox_asserter.assert_all_targets(
|
||||
targets,
|
||||
allow_staging=allow_staging,
|
||||
production_override=production_override
|
||||
)
|
||||
report["gates"]["sandbox"] = sandbox_report
|
||||
|
||||
if not sandbox_report["can_proceed"]:
|
||||
report["can_proceed"] = False
|
||||
report["failed_gates"].append("sandbox")
|
||||
report["decision"] = "BLOCKED"
|
||||
report["blocking_reason"] = f"Sandbox assertion failed: {sandbox_report['blocked_targets']}"
|
||||
print(f" [BLOCKED] Sandbox assertion failed")
|
||||
|
||||
# Short-circuit: Don't run other checks if sandbox fails
|
||||
report["duration_ms"] = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
return report
|
||||
else:
|
||||
print(f" [PASS] All targets within sandbox boundaries")
|
||||
|
||||
# =====================================================================
|
||||
# GATE 2: Inventory Validation
|
||||
# =====================================================================
|
||||
print("\n[GATE 2/3] Inventory Validation...")
|
||||
|
||||
inventory_report = self.inventory_checker.preflight_report(targets, agent_tier)
|
||||
report["gates"]["inventory"] = inventory_report
|
||||
|
||||
if not inventory_report["can_proceed"]:
|
||||
report["can_proceed"] = False
|
||||
report["failed_gates"].append("inventory")
|
||||
if report["decision"] == "PENDING":
|
||||
report["decision"] = "BLOCKED"
|
||||
report["blocking_reason"] = f"Inventory validation failed"
|
||||
print(f" [BLOCKED] Inventory validation failed")
|
||||
else:
|
||||
print(f" [PASS] All targets validated in inventory")
|
||||
|
||||
# =====================================================================
|
||||
# GATE 3: Dependency Check
|
||||
# =====================================================================
|
||||
print("\n[GATE 3/3] Dependency Check...")
|
||||
|
||||
dependency_report = self.dependency_checker.preflight_report(action_type, extra_secrets)
|
||||
report["gates"]["dependencies"] = dependency_report
|
||||
|
||||
if not dependency_report["can_proceed"]:
|
||||
report["can_proceed"] = False
|
||||
report["failed_gates"].append("dependencies")
|
||||
if report["decision"] == "PENDING":
|
||||
report["decision"] = "BLOCKED"
|
||||
report["blocking_reason"] = f"Dependency check failed"
|
||||
print(f" [BLOCKED] Dependency check failed")
|
||||
else:
|
||||
print(f" [PASS] All dependencies available")
|
||||
|
||||
# =====================================================================
|
||||
# Final Decision
|
||||
# =====================================================================
|
||||
if report["can_proceed"]:
|
||||
report["decision"] = "APPROVED"
|
||||
report["blocking_reason"] = None
|
||||
|
||||
report["duration_ms"] = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
return report
|
||||
|
||||
def print_report(self, report: dict):
|
||||
"""Print a human-readable report"""
|
||||
print("\n" + "=" * 70)
|
||||
print("UNIFIED PREFLIGHT REPORT")
|
||||
print("=" * 70)
|
||||
print(f"Agent ID: {report['agent_id']}")
|
||||
print(f"Agent Tier: {report['agent_tier']}")
|
||||
print(f"Action Type: {report['action_type']}")
|
||||
print(f"Targets: {report['targets']}")
|
||||
print(f"Timestamp: {report['timestamp']}")
|
||||
print(f"Duration: {report['duration_ms']}ms")
|
||||
print()
|
||||
|
||||
print("GATE RESULTS:")
|
||||
print("-" * 70)
|
||||
|
||||
for gate_name in report["gate_order"]:
|
||||
gate = report["gates"].get(gate_name, {})
|
||||
passed = gate.get("can_proceed", False)
|
||||
status = "[PASS]" if passed else "[FAIL]"
|
||||
print(f" {status} {gate_name.upper()}")
|
||||
|
||||
if gate_name == "sandbox":
|
||||
summary = gate.get("summary", {})
|
||||
print(f" Safe: {summary.get('safe', 0)}, Unsafe: {summary.get('unsafe', 0)}, Risk: {summary.get('max_risk_level', 0)}/4")
|
||||
elif gate_name == "inventory":
|
||||
summary = gate.get("summary", {})
|
||||
print(f" Checks: {summary.get('total_checks', 0)}, Passed: {summary.get('passed', 0)}, Failed: {summary.get('failed', 0)}")
|
||||
elif gate_name == "dependencies":
|
||||
summary = gate.get("summary", {})
|
||||
print(f" Checks: {summary.get('total_checks', 0)}, Passed: {summary.get('passed', 0)}, Failed: {summary.get('failed', 0)}")
|
||||
|
||||
print()
|
||||
print("-" * 70)
|
||||
print("DECISION")
|
||||
print("-" * 70)
|
||||
|
||||
if report["can_proceed"]:
|
||||
print(f" [APPROVED] Preflight checks passed - execution authorized")
|
||||
else:
|
||||
print(f" [BLOCKED] Preflight checks failed - execution denied")
|
||||
print(f" Failed Gates: {report['failed_gates']}")
|
||||
print(f" Reason: {report['blocking_reason']}")
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Unified Preflight Gate")
|
||||
parser.add_argument("targets", nargs="+", help="Target nodes")
|
||||
parser.add_argument("--action", "-a", default="generic",
|
||||
choices=["terraform", "ansible", "docker", "generic"],
|
||||
help="Action type")
|
||||
parser.add_argument("--tier", "-t", type=int, default=1, help="Agent tier (0-4)")
|
||||
parser.add_argument("--agent-id", default="cli-user", help="Agent identifier")
|
||||
parser.add_argument("--secret", action="append", dest="extra_secrets",
|
||||
help="Additional secrets to check")
|
||||
parser.add_argument("--allow-staging", action="store_true",
|
||||
help="Allow staging targets")
|
||||
parser.add_argument("--production-override", action="store_true",
|
||||
help="DANGEROUS: Allow production targets")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON only")
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet mode")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
gate = PreflightGate()
|
||||
|
||||
if not args.quiet:
|
||||
print(f"\n{'='*70}")
|
||||
print("PREFLIGHT CHECK INITIATED")
|
||||
print(f"{'='*70}")
|
||||
print(f"Agent: {args.agent_id} (Tier {args.tier})")
|
||||
print(f"Action: {args.action}")
|
||||
print(f"Targets: {args.targets}")
|
||||
|
||||
report = gate.run_preflight(
|
||||
targets=args.targets,
|
||||
action_type=args.action,
|
||||
agent_tier=args.tier,
|
||||
agent_id=args.agent_id,
|
||||
extra_secrets=args.extra_secrets,
|
||||
allow_staging=args.allow_staging,
|
||||
production_override=args.production_override
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2))
|
||||
elif not args.quiet:
|
||||
gate.print_report(report)
|
||||
|
||||
sys.exit(0 if report["can_proceed"] else 1)
|
||||
Executable
+416
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sandbox Assertion
|
||||
=================
|
||||
Verifies that execution targets are within sandbox boundaries.
|
||||
CRITICAL: This is a safety gate that prevents accidental production access.
|
||||
Part of Phase 3: Execution Pipeline - Preflight System.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class AssertionStatus(str, Enum):
|
||||
SAFE = "SAFE" # Definitely sandbox
|
||||
UNSAFE = "UNSAFE" # Definitely production
|
||||
UNKNOWN = "UNKNOWN" # Cannot determine
|
||||
OVERRIDE = "OVERRIDE" # Production with explicit approval
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssertionResult:
|
||||
target: str
|
||||
status: AssertionStatus
|
||||
environment: str
|
||||
message: str
|
||||
risk_level: int # 0=none, 1=low, 2=medium, 3=high, 4=critical
|
||||
requires_approval: bool
|
||||
details: dict
|
||||
timestamp: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"target": self.target,
|
||||
"status": self.status.value,
|
||||
"environment": self.environment,
|
||||
"message": self.message,
|
||||
"risk_level": self.risk_level,
|
||||
"requires_approval": self.requires_approval,
|
||||
"details": self.details,
|
||||
"timestamp": self.timestamp
|
||||
}
|
||||
|
||||
|
||||
class SandboxAsserter:
|
||||
"""
|
||||
Ensures execution stays within sandbox boundaries.
|
||||
|
||||
This is a CRITICAL safety component that:
|
||||
- Identifies environment type from target name/IP/metadata
|
||||
- Blocks production access by default
|
||||
- Requires explicit approval for non-sandbox environments
|
||||
- Logs all assertions for audit
|
||||
"""
|
||||
|
||||
# Environment detection patterns
|
||||
ENVIRONMENT_PATTERNS = {
|
||||
"production": [
|
||||
"prod", "prd", "production", "live", "primary",
|
||||
"master", "main-db", "primary-db"
|
||||
],
|
||||
"staging": [
|
||||
"staging", "stg", "stage", "preprod", "pre-prod",
|
||||
"uat", "qa", "test-prod"
|
||||
],
|
||||
"sandbox": [
|
||||
"sandbox", "sbx", "dev", "development", "local",
|
||||
"test", "lab", "experiment", "demo"
|
||||
]
|
||||
}
|
||||
|
||||
# IP range classifications
|
||||
IP_CLASSIFICATIONS = {
|
||||
"10.77.0.0/24": "wireguard-internal",
|
||||
"10.0.0.0/8": "internal-sandbox",
|
||||
"172.16.0.0/12": "internal-sandbox",
|
||||
"192.168.0.0/16": "internal-sandbox",
|
||||
"0.0.0.0/0": "external-unknown"
|
||||
}
|
||||
|
||||
# Risk levels by environment
|
||||
RISK_LEVELS = {
|
||||
"sandbox": 0,
|
||||
"development": 0,
|
||||
"staging": 2,
|
||||
"production": 4,
|
||||
"unknown": 3
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.vault_token = self._get_vault_token()
|
||||
|
||||
def _get_vault_token(self) -> str:
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
return json.load(f)["root_token"]
|
||||
|
||||
def _now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _vault_read(self, path: str) -> Optional[dict]:
|
||||
"""Read from Vault KV"""
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {self.vault_token}",
|
||||
f"https://127.0.0.1:8200/v1/secret/data/{path}"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
if "data" in data and "data" in data["data"]:
|
||||
return data["data"]["data"]
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _detect_environment_from_name(self, target: str) -> tuple[str, float]:
|
||||
"""Detect environment from target name. Returns (env, confidence)"""
|
||||
target_lower = target.lower()
|
||||
|
||||
for env, patterns in self.ENVIRONMENT_PATTERNS.items():
|
||||
for pattern in patterns:
|
||||
if pattern in target_lower:
|
||||
# Higher confidence for exact matches
|
||||
confidence = 0.95 if pattern == target_lower else 0.8
|
||||
return env, confidence
|
||||
|
||||
return "unknown", 0.3
|
||||
|
||||
def _check_pool_classification(self, target: str) -> Optional[str]:
|
||||
"""Check pool classification from inventory"""
|
||||
inventory = self._vault_read("inventory/proxmox")
|
||||
if not inventory:
|
||||
return None
|
||||
|
||||
for pool_name, pool_data in inventory.get("pools", {}).items():
|
||||
nodes = pool_data.get("nodes", [])
|
||||
if target in nodes:
|
||||
# Classify based on pool name
|
||||
pool_lower = pool_name.lower()
|
||||
if "prod" in pool_lower:
|
||||
return "production"
|
||||
elif "staging" in pool_lower or "stg" in pool_lower:
|
||||
return "staging"
|
||||
elif "sandbox" in pool_lower or "dev" in pool_lower:
|
||||
return "sandbox"
|
||||
return None
|
||||
|
||||
def assert_sandbox(self, target: str, allow_staging: bool = False,
|
||||
production_override: bool = False) -> AssertionResult:
|
||||
"""
|
||||
Assert that target is within sandbox boundaries.
|
||||
|
||||
Args:
|
||||
target: Target name/identifier
|
||||
allow_staging: Allow staging environment (default: False)
|
||||
production_override: Explicit production approval (default: False)
|
||||
|
||||
Returns:
|
||||
AssertionResult with safety determination
|
||||
"""
|
||||
timestamp = self._now()
|
||||
|
||||
# Step 1: Check pool classification from Vault
|
||||
pool_env = self._check_pool_classification(target)
|
||||
|
||||
# Step 2: Detect from name patterns
|
||||
name_env, name_confidence = self._detect_environment_from_name(target)
|
||||
|
||||
# Step 3: Determine final environment
|
||||
if pool_env:
|
||||
environment = pool_env
|
||||
confidence = 0.99 # Pool classification is authoritative
|
||||
else:
|
||||
environment = name_env
|
||||
confidence = name_confidence
|
||||
|
||||
risk_level = self.RISK_LEVELS.get(environment, 3)
|
||||
|
||||
# Step 4: Make safety determination
|
||||
if environment == "sandbox" or environment == "development":
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.SAFE,
|
||||
environment=environment,
|
||||
message=f"Target '{target}' is in SANDBOX environment - safe to proceed",
|
||||
risk_level=risk_level,
|
||||
requires_approval=False,
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
|
||||
elif environment == "staging":
|
||||
if allow_staging:
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.SAFE,
|
||||
environment=environment,
|
||||
message=f"Target '{target}' is in STAGING - allowed by flag",
|
||||
risk_level=risk_level,
|
||||
requires_approval=False,
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence,
|
||||
"staging_allowed": True
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
else:
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.UNSAFE,
|
||||
environment=environment,
|
||||
message=f"Target '{target}' is in STAGING - requires --allow-staging flag",
|
||||
risk_level=risk_level,
|
||||
requires_approval=True,
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence,
|
||||
"staging_allowed": False
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
|
||||
elif environment == "production":
|
||||
if production_override:
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.OVERRIDE,
|
||||
environment=environment,
|
||||
message=f"PRODUCTION OVERRIDE: Target '{target}' access with explicit approval",
|
||||
risk_level=risk_level,
|
||||
requires_approval=False, # Already approved
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence,
|
||||
"production_override": True,
|
||||
"WARNING": "PRODUCTION ACCESS ENABLED"
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
else:
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.UNSAFE,
|
||||
environment=environment,
|
||||
message=f"BLOCKED: Target '{target}' is in PRODUCTION - requires explicit approval",
|
||||
risk_level=risk_level,
|
||||
requires_approval=True,
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence,
|
||||
"production_override": False,
|
||||
"BLOCKED": "Production access denied"
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
|
||||
else: # unknown
|
||||
return AssertionResult(
|
||||
target=target,
|
||||
status=AssertionStatus.UNKNOWN,
|
||||
environment=environment,
|
||||
message=f"UNKNOWN: Cannot determine environment for '{target}'",
|
||||
risk_level=risk_level,
|
||||
requires_approval=True,
|
||||
details={
|
||||
"pool_classification": pool_env,
|
||||
"name_detection": name_env,
|
||||
"confidence": confidence,
|
||||
"WARNING": "Unable to classify - treating as high risk"
|
||||
},
|
||||
timestamp=timestamp
|
||||
)
|
||||
|
||||
def assert_all_targets(self, targets: list[str], allow_staging: bool = False,
|
||||
production_override: bool = False) -> dict:
|
||||
"""Assert sandbox for multiple targets"""
|
||||
report = {
|
||||
"report_type": "sandbox_assertion",
|
||||
"timestamp": self._now(),
|
||||
"settings": {
|
||||
"allow_staging": allow_staging,
|
||||
"production_override": production_override
|
||||
},
|
||||
"results": [],
|
||||
"summary": {
|
||||
"total": len(targets),
|
||||
"safe": 0,
|
||||
"unsafe": 0,
|
||||
"unknown": 0,
|
||||
"override": 0,
|
||||
"max_risk_level": 0
|
||||
},
|
||||
"can_proceed": True,
|
||||
"blocked_targets": []
|
||||
}
|
||||
|
||||
for target in targets:
|
||||
result = self.assert_sandbox(target, allow_staging, production_override)
|
||||
report["results"].append(result.to_dict())
|
||||
|
||||
if result.status == AssertionStatus.SAFE:
|
||||
report["summary"]["safe"] += 1
|
||||
elif result.status == AssertionStatus.UNSAFE:
|
||||
report["summary"]["unsafe"] += 1
|
||||
report["can_proceed"] = False
|
||||
report["blocked_targets"].append(target)
|
||||
elif result.status == AssertionStatus.UNKNOWN:
|
||||
report["summary"]["unknown"] += 1
|
||||
report["can_proceed"] = False
|
||||
report["blocked_targets"].append(target)
|
||||
elif result.status == AssertionStatus.OVERRIDE:
|
||||
report["summary"]["override"] += 1
|
||||
|
||||
report["summary"]["max_risk_level"] = max(
|
||||
report["summary"]["max_risk_level"],
|
||||
result.risk_level
|
||||
)
|
||||
|
||||
return report
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI
|
||||
# =============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Sandbox Assertion Check")
|
||||
parser.add_argument("targets", nargs="+", help="Target nodes to assert")
|
||||
parser.add_argument("--allow-staging", action="store_true",
|
||||
help="Allow staging environment targets")
|
||||
parser.add_argument("--production-override", action="store_true",
|
||||
help="DANGEROUS: Override production block with explicit approval")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
asserter = SandboxAsserter()
|
||||
report = asserter.assert_all_targets(
|
||||
args.targets,
|
||||
allow_staging=args.allow_staging,
|
||||
production_override=args.production_override
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(report, indent=2))
|
||||
else:
|
||||
print("\n" + "=" * 60)
|
||||
print("SANDBOX ASSERTION REPORT")
|
||||
print("=" * 60)
|
||||
print(f"Timestamp: {report['timestamp']}")
|
||||
print(f"Allow Staging: {report['settings']['allow_staging']}")
|
||||
print(f"Production Override: {report['settings']['production_override']}")
|
||||
print()
|
||||
|
||||
risk_icons = ["[SAFE]", "[LOW]", "[MED]", "[HIGH]", "[CRIT]"]
|
||||
|
||||
for result in report["results"]:
|
||||
status_icon = {
|
||||
"SAFE": "[OK]",
|
||||
"UNSAFE": "[BLOCKED]",
|
||||
"UNKNOWN": "[???]",
|
||||
"OVERRIDE": "[OVERRIDE]"
|
||||
}.get(result["status"], "[?]")
|
||||
|
||||
risk_icon = risk_icons[min(result["risk_level"], 4)]
|
||||
|
||||
print(f"Target: {result['target']}")
|
||||
print(f" Status: {status_icon} {result['status']}")
|
||||
print(f" Environment: {result['environment']}")
|
||||
print(f" Risk Level: {risk_icon} ({result['risk_level']}/4)")
|
||||
print(f" Message: {result['message']}")
|
||||
if result["requires_approval"]:
|
||||
print(f" !!! REQUIRES APPROVAL !!!")
|
||||
print()
|
||||
|
||||
print("-" * 60)
|
||||
print("SUMMARY")
|
||||
print("-" * 60)
|
||||
s = report["summary"]
|
||||
print(f" Total Targets: {s['total']}")
|
||||
print(f" Safe: {s['safe']}")
|
||||
print(f" Unsafe (blocked): {s['unsafe']}")
|
||||
print(f" Unknown: {s['unknown']}")
|
||||
print(f" Override: {s['override']}")
|
||||
print(f" Max Risk Level: {s['max_risk_level']}/4")
|
||||
print()
|
||||
|
||||
if report["blocked_targets"]:
|
||||
print(f"BLOCKED TARGETS: {report['blocked_targets']}")
|
||||
print()
|
||||
|
||||
if report["can_proceed"]:
|
||||
print("[OK] SANDBOX ASSERTION PASSED - All targets are safe")
|
||||
else:
|
||||
print("[BLOCKED] SANDBOX ASSERTION FAILED - Cannot proceed")
|
||||
print(" Some targets are outside sandbox boundaries")
|
||||
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(0 if report["can_proceed"] else 1)
|
||||
Reference in New Issue
Block a user