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,34 @@
|
||||
# Tier0 Agent
|
||||
|
||||
> Observer-tier agent (read-only)
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains observer-tier agent (read-only).
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `agent.py` | Python module |
|
||||
| `run-agent.sh` | Shell script |
|
||||
| `bootstrap.sh` | Shell script |
|
||||
|
||||
## 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: [Agents](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Status: Tier0 Agent
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## 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:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
Executable
+603
@@ -0,0 +1,603 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tier 0 Observer Agent
|
||||
=====================
|
||||
A governed agent that can read documentation, view inventory,
|
||||
and generate plans, but CANNOT execute any commands.
|
||||
|
||||
This agent enforces strict Tier 0 constraints:
|
||||
- Read-only file access (within allowed paths)
|
||||
- Plan generation only (no execution)
|
||||
- No secret access
|
||||
- No SSH/API access
|
||||
- All actions logged to governance ledger
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional, Any
|
||||
import redis
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
# =============================================================================
|
||||
|
||||
AGENT_DIR = Path(__file__).parent
|
||||
CONFIG_FILE = AGENT_DIR / "config" / "agent.json"
|
||||
WORKSPACE_DIR = AGENT_DIR / "workspace"
|
||||
PLANS_DIR = AGENT_DIR / "plans"
|
||||
LOGS_DIR = AGENT_DIR / "logs"
|
||||
LEDGER_DB = Path("/opt/agent-governance/ledger/governance.db")
|
||||
|
||||
# Load agent config
|
||||
with open(CONFIG_FILE) as f:
|
||||
CONFIG = json.load(f)
|
||||
|
||||
AGENT_ID = CONFIG["agent_id"]
|
||||
AGENT_TIER = CONFIG["tier"]
|
||||
ALLOWED_PATHS = [Path(p) for p in CONFIG["constraints"]["allowed_paths"]]
|
||||
FORBIDDEN_PATHS = CONFIG["constraints"]["forbidden_paths"]
|
||||
ALLOWED_ACTIONS = CONFIG["constraints"]["allowed_actions"]
|
||||
FORBIDDEN_ACTIONS = CONFIG["constraints"]["forbidden_actions"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Data Classes
|
||||
# =============================================================================
|
||||
|
||||
@dataclass
|
||||
class ActionResult:
|
||||
"""Result of an agent action"""
|
||||
action: str
|
||||
success: bool
|
||||
data: Any = None
|
||||
error: Optional[str] = None
|
||||
blocked: bool = False
|
||||
block_reason: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Plan:
|
||||
"""A generated plan"""
|
||||
plan_id: str
|
||||
title: str
|
||||
description: str
|
||||
target: str
|
||||
steps: list
|
||||
rollback_steps: list
|
||||
created_at: str
|
||||
agent_id: str
|
||||
status: str = "draft"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Governance Integration
|
||||
# =============================================================================
|
||||
|
||||
class GovernanceClient:
|
||||
"""Interfaces with the governance system"""
|
||||
|
||||
def __init__(self):
|
||||
self.redis = self._get_redis()
|
||||
self.session_id = os.environ.get("SESSION_ID", "unknown")
|
||||
|
||||
def _get_redis(self) -> Optional[redis.Redis]:
|
||||
try:
|
||||
# Get password from environment or file
|
||||
password = os.environ.get("REDIS_PASSWORD")
|
||||
if not password:
|
||||
# Try to get from Vault (using root token for bootstrap)
|
||||
import subprocess
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
token = json.load(f)["root_token"]
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {token}",
|
||||
"https://127.0.0.1:8200/v1/secret/data/services/dragonfly"
|
||||
], capture_output=True, text=True)
|
||||
creds = json.loads(result.stdout)["data"]["data"]
|
||||
password = creds["password"]
|
||||
|
||||
return redis.Redis(host="127.0.0.1", port=6379, password=password, decode_responses=True)
|
||||
except:
|
||||
return None
|
||||
|
||||
def log_action(self, action: str, decision: str, target: str,
|
||||
success: bool, confidence: float = 1.0, error: str = None):
|
||||
"""Log action to governance ledger"""
|
||||
try:
|
||||
conn = sqlite3.connect(LEDGER_DB)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
INSERT INTO agent_actions
|
||||
(timestamp, agent_id, agent_version, tier, action, decision,
|
||||
confidence, target, success, error_message, session_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
datetime.now(timezone.utc).isoformat(),
|
||||
AGENT_ID,
|
||||
CONFIG.get("agent_version", "1.0.0"),
|
||||
AGENT_TIER,
|
||||
action,
|
||||
decision,
|
||||
confidence,
|
||||
target,
|
||||
1 if success else 0,
|
||||
error,
|
||||
self.session_id
|
||||
))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not log action: {e}")
|
||||
|
||||
def update_heartbeat(self):
|
||||
"""Update agent heartbeat in DragonflyDB"""
|
||||
if self.redis:
|
||||
try:
|
||||
self.redis.set(f"agent:{AGENT_ID}:heartbeat", str(int(datetime.now().timestamp())), ex=60)
|
||||
except:
|
||||
pass
|
||||
|
||||
def check_revocation(self) -> bool:
|
||||
"""Check if agent has been revoked"""
|
||||
if self.redis:
|
||||
try:
|
||||
signal = self.redis.get(f"agent:{AGENT_ID}:revoke_signal")
|
||||
return signal == "1"
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def increment_compliant(self):
|
||||
"""Increment compliant run counter"""
|
||||
try:
|
||||
conn = sqlite3.connect(LEDGER_DB)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE agent_metrics
|
||||
SET compliant_runs = compliant_runs + 1,
|
||||
consecutive_compliant = consecutive_compliant + 1,
|
||||
total_runs = total_runs + 1,
|
||||
last_active_at = datetime('now'),
|
||||
updated_at = datetime('now')
|
||||
WHERE agent_id = ?
|
||||
""", (AGENT_ID,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update metrics: {e}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tier 0 Agent
|
||||
# =============================================================================
|
||||
|
||||
class Tier0Agent:
|
||||
"""
|
||||
A strictly constrained Tier 0 agent.
|
||||
Can only read and generate plans, cannot execute anything.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.governance = GovernanceClient()
|
||||
self._check_not_revoked()
|
||||
|
||||
def _now(self) -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
def _check_not_revoked(self):
|
||||
"""Check revocation status before any action"""
|
||||
if self.governance.check_revocation():
|
||||
print("[REVOKED] Agent has been revoked. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
def _is_path_allowed(self, path: str) -> bool:
|
||||
"""Check if path is within allowed paths"""
|
||||
target = Path(path).resolve()
|
||||
|
||||
# Check forbidden patterns
|
||||
for pattern in FORBIDDEN_PATHS:
|
||||
if pattern.startswith("**/"):
|
||||
if pattern[3:] in str(target):
|
||||
return False
|
||||
elif target.match(pattern):
|
||||
return False
|
||||
|
||||
# Check allowed paths
|
||||
for allowed in ALLOWED_PATHS:
|
||||
allowed_resolved = Path(allowed).resolve()
|
||||
try:
|
||||
target.relative_to(allowed_resolved)
|
||||
return True
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def _block_action(self, action: str, reason: str) -> ActionResult:
|
||||
"""Record a blocked action"""
|
||||
self.governance.log_action(
|
||||
action=action,
|
||||
decision="BLOCKED",
|
||||
target="N/A",
|
||||
success=False,
|
||||
error=reason
|
||||
)
|
||||
return ActionResult(
|
||||
action=action,
|
||||
success=False,
|
||||
blocked=True,
|
||||
block_reason=reason
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Allowed Actions
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def read_file(self, path: str) -> ActionResult:
|
||||
"""Read a file (if allowed)"""
|
||||
self._check_not_revoked()
|
||||
self.governance.update_heartbeat()
|
||||
|
||||
if not self._is_path_allowed(path):
|
||||
return self._block_action("read_file", f"Path not allowed: {path}")
|
||||
|
||||
try:
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
|
||||
self.governance.log_action(
|
||||
action="read_file",
|
||||
decision="EXECUTE",
|
||||
target=path,
|
||||
success=True
|
||||
)
|
||||
|
||||
return ActionResult(
|
||||
action="read_file",
|
||||
success=True,
|
||||
data={"path": path, "content": content, "size": len(content)}
|
||||
)
|
||||
except Exception as e:
|
||||
self.governance.log_action(
|
||||
action="read_file",
|
||||
decision="EXECUTE",
|
||||
target=path,
|
||||
success=False,
|
||||
error=str(e)
|
||||
)
|
||||
return ActionResult(action="read_file", success=False, error=str(e))
|
||||
|
||||
def list_directory(self, path: str) -> ActionResult:
|
||||
"""List directory contents (if allowed)"""
|
||||
self._check_not_revoked()
|
||||
self.governance.update_heartbeat()
|
||||
|
||||
if not self._is_path_allowed(path):
|
||||
return self._block_action("list_directory", f"Path not allowed: {path}")
|
||||
|
||||
try:
|
||||
entries = []
|
||||
for entry in Path(path).iterdir():
|
||||
entries.append({
|
||||
"name": entry.name,
|
||||
"is_dir": entry.is_dir(),
|
||||
"size": entry.stat().st_size if entry.is_file() else 0
|
||||
})
|
||||
|
||||
self.governance.log_action(
|
||||
action="list_directory",
|
||||
decision="EXECUTE",
|
||||
target=path,
|
||||
success=True
|
||||
)
|
||||
|
||||
return ActionResult(
|
||||
action="list_directory",
|
||||
success=True,
|
||||
data={"path": path, "entries": entries}
|
||||
)
|
||||
except Exception as e:
|
||||
return ActionResult(action="list_directory", success=False, error=str(e))
|
||||
|
||||
def generate_plan(self, title: str, description: str, target: str,
|
||||
steps: list, rollback_steps: list = None) -> ActionResult:
|
||||
"""Generate a plan (does NOT execute it)"""
|
||||
self._check_not_revoked()
|
||||
self.governance.update_heartbeat()
|
||||
|
||||
# Generate plan ID
|
||||
plan_id = f"plan-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{hashlib.sha256(title.encode()).hexdigest()[:8]}"
|
||||
|
||||
plan = Plan(
|
||||
plan_id=plan_id,
|
||||
title=title,
|
||||
description=description,
|
||||
target=target,
|
||||
steps=steps,
|
||||
rollback_steps=rollback_steps or [],
|
||||
created_at=self._now(),
|
||||
agent_id=AGENT_ID,
|
||||
status="draft"
|
||||
)
|
||||
|
||||
# Save plan to file
|
||||
plan_file = PLANS_DIR / f"{plan_id}.json"
|
||||
plan_dict = {
|
||||
"plan_id": plan.plan_id,
|
||||
"title": plan.title,
|
||||
"description": plan.description,
|
||||
"target": plan.target,
|
||||
"steps": plan.steps,
|
||||
"rollback_steps": plan.rollback_steps,
|
||||
"created_at": plan.created_at,
|
||||
"agent_id": plan.agent_id,
|
||||
"agent_tier": AGENT_TIER,
|
||||
"status": plan.status,
|
||||
"requires_approval": True,
|
||||
"approved_by": None,
|
||||
"executed": False
|
||||
}
|
||||
|
||||
with open(plan_file, "w") as f:
|
||||
json.dump(plan_dict, f, indent=2)
|
||||
|
||||
# Log action
|
||||
self.governance.log_action(
|
||||
action="generate_plan",
|
||||
decision="PLAN",
|
||||
target=target,
|
||||
success=True,
|
||||
confidence=0.9
|
||||
)
|
||||
|
||||
# Increment compliant counter
|
||||
self.governance.increment_compliant()
|
||||
|
||||
return ActionResult(
|
||||
action="generate_plan",
|
||||
success=True,
|
||||
data={
|
||||
"plan_id": plan_id,
|
||||
"plan_file": str(plan_file),
|
||||
"message": "Plan generated. Requires approval before execution."
|
||||
}
|
||||
)
|
||||
|
||||
def request_review(self, subject: str, details: str) -> ActionResult:
|
||||
"""Request human review/assistance"""
|
||||
self._check_not_revoked()
|
||||
self.governance.update_heartbeat()
|
||||
|
||||
review_id = f"review-{datetime.now().strftime('%Y%m%d-%H%M%S')}"
|
||||
|
||||
review_request = {
|
||||
"review_id": review_id,
|
||||
"agent_id": AGENT_ID,
|
||||
"agent_tier": AGENT_TIER,
|
||||
"subject": subject,
|
||||
"details": details,
|
||||
"created_at": self._now(),
|
||||
"status": "pending"
|
||||
}
|
||||
|
||||
# Save review request
|
||||
review_file = WORKSPACE_DIR / f"{review_id}.json"
|
||||
with open(review_file, "w") as f:
|
||||
json.dump(review_request, f, indent=2)
|
||||
|
||||
self.governance.log_action(
|
||||
action="request_review",
|
||||
decision="PLAN",
|
||||
target=subject,
|
||||
success=True
|
||||
)
|
||||
|
||||
return ActionResult(
|
||||
action="request_review",
|
||||
success=True,
|
||||
data={"review_id": review_id, "message": "Review request submitted."}
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Forbidden Actions (Always Blocked)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def execute_command(self, command: str) -> ActionResult:
|
||||
"""FORBIDDEN: Execute a command"""
|
||||
return self._block_action(
|
||||
"execute_command",
|
||||
"Tier 0 agents cannot execute commands. Generate a plan instead."
|
||||
)
|
||||
|
||||
def write_file(self, path: str, content: str) -> ActionResult:
|
||||
"""FORBIDDEN: Write to a file (except plans in allowed paths)"""
|
||||
# Allow writing to plans directory
|
||||
if str(Path(path).resolve()).startswith(str(PLANS_DIR.resolve())):
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
self.governance.log_action(
|
||||
action="write_plan_file",
|
||||
decision="EXECUTE",
|
||||
target=path,
|
||||
success=True
|
||||
)
|
||||
return ActionResult(action="write_file", success=True, data={"path": path})
|
||||
except Exception as e:
|
||||
return ActionResult(action="write_file", success=False, error=str(e))
|
||||
|
||||
return self._block_action(
|
||||
"write_file",
|
||||
"Tier 0 agents cannot write files outside plans directory."
|
||||
)
|
||||
|
||||
def ssh_connect(self, host: str) -> ActionResult:
|
||||
"""FORBIDDEN: SSH to a host"""
|
||||
return self._block_action(
|
||||
"ssh_connect",
|
||||
"Tier 0 agents cannot SSH to hosts. Generate a plan instead."
|
||||
)
|
||||
|
||||
def terraform_apply(self, directory: str) -> ActionResult:
|
||||
"""FORBIDDEN: Apply Terraform"""
|
||||
return self._block_action(
|
||||
"terraform_apply",
|
||||
"Tier 0 agents cannot apply Terraform. Use terraform_plan to generate a plan."
|
||||
)
|
||||
|
||||
def ansible_run(self, playbook: str) -> ActionResult:
|
||||
"""FORBIDDEN: Run Ansible playbook"""
|
||||
return self._block_action(
|
||||
"ansible_run",
|
||||
"Tier 0 agents cannot run Ansible. Generate a plan with check-mode only."
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CLI Interface
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Tier 0 Observer Agent")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# Status
|
||||
subparsers.add_parser("status", help="Show agent status")
|
||||
|
||||
# Read file
|
||||
read_parser = subparsers.add_parser("read", help="Read a file")
|
||||
read_parser.add_argument("path", help="File path to read")
|
||||
|
||||
# List directory
|
||||
ls_parser = subparsers.add_parser("ls", help="List directory")
|
||||
ls_parser.add_argument("path", nargs="?", default=str(WORKSPACE_DIR))
|
||||
|
||||
# Generate plan
|
||||
plan_parser = subparsers.add_parser("plan", help="Generate a plan")
|
||||
plan_parser.add_argument("--title", required=True)
|
||||
plan_parser.add_argument("--description", required=True)
|
||||
plan_parser.add_argument("--target", required=True)
|
||||
plan_parser.add_argument("--steps", required=True, help="JSON array of steps")
|
||||
plan_parser.add_argument("--rollback", help="JSON array of rollback steps")
|
||||
|
||||
# Request review
|
||||
review_parser = subparsers.add_parser("review", help="Request human review")
|
||||
review_parser.add_argument("--subject", required=True)
|
||||
review_parser.add_argument("--details", required=True)
|
||||
|
||||
# Test forbidden actions
|
||||
subparsers.add_parser("test-forbidden", help="Test that forbidden actions are blocked")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
agent = Tier0Agent()
|
||||
|
||||
if args.command == "status":
|
||||
print(f"\n{'='*50}")
|
||||
print("TIER 0 AGENT STATUS")
|
||||
print(f"{'='*50}")
|
||||
print(f"Agent ID: {AGENT_ID}")
|
||||
print(f"Tier: {AGENT_TIER} (Observer)")
|
||||
print(f"Session: {os.environ.get('SESSION_ID', 'N/A')}")
|
||||
print(f"\nAllowed Actions: {', '.join(ALLOWED_ACTIONS)}")
|
||||
print(f"Forbidden Actions: {', '.join(FORBIDDEN_ACTIONS)}")
|
||||
print(f"\nWorkspace: {WORKSPACE_DIR}")
|
||||
print(f"Plans: {PLANS_DIR}")
|
||||
|
||||
# Check revocation
|
||||
if agent.governance.check_revocation():
|
||||
print(f"\n[REVOKED] Agent has been revoked!")
|
||||
else:
|
||||
print(f"\n[ACTIVE] Agent is active")
|
||||
|
||||
print(f"{'='*50}")
|
||||
|
||||
elif args.command == "read":
|
||||
result = agent.read_file(args.path)
|
||||
if result.success:
|
||||
print(result.data["content"])
|
||||
elif result.blocked:
|
||||
print(f"[BLOCKED] {result.block_reason}")
|
||||
else:
|
||||
print(f"[ERROR] {result.error}")
|
||||
|
||||
elif args.command == "ls":
|
||||
result = agent.list_directory(args.path)
|
||||
if result.success:
|
||||
for entry in result.data["entries"]:
|
||||
prefix = "d" if entry["is_dir"] else "-"
|
||||
print(f"{prefix} {entry['name']}")
|
||||
elif result.blocked:
|
||||
print(f"[BLOCKED] {result.block_reason}")
|
||||
else:
|
||||
print(f"[ERROR] {result.error}")
|
||||
|
||||
elif args.command == "plan":
|
||||
steps = json.loads(args.steps)
|
||||
rollback = json.loads(args.rollback) if args.rollback else []
|
||||
|
||||
result = agent.generate_plan(
|
||||
title=args.title,
|
||||
description=args.description,
|
||||
target=args.target,
|
||||
steps=steps,
|
||||
rollback_steps=rollback
|
||||
)
|
||||
|
||||
if result.success:
|
||||
print(f"\n[OK] Plan generated: {result.data['plan_id']}")
|
||||
print(f"File: {result.data['plan_file']}")
|
||||
print(f"Note: {result.data['message']}")
|
||||
else:
|
||||
print(f"[ERROR] {result.error}")
|
||||
|
||||
elif args.command == "review":
|
||||
result = agent.request_review(args.subject, args.details)
|
||||
if result.success:
|
||||
print(f"[OK] Review request: {result.data['review_id']}")
|
||||
else:
|
||||
print(f"[ERROR] {result.error}")
|
||||
|
||||
elif args.command == "test-forbidden":
|
||||
print("\n" + "="*50)
|
||||
print("TESTING FORBIDDEN ACTIONS")
|
||||
print("="*50)
|
||||
|
||||
tests = [
|
||||
("execute_command", lambda: agent.execute_command("ls -la")),
|
||||
("write_file", lambda: agent.write_file("/etc/passwd", "test")),
|
||||
("ssh_connect", lambda: agent.ssh_connect("10.77.10.1")),
|
||||
("terraform_apply", lambda: agent.terraform_apply("./infra")),
|
||||
("ansible_run", lambda: agent.ansible_run("playbook.yml")),
|
||||
]
|
||||
|
||||
all_blocked = True
|
||||
for name, test_fn in tests:
|
||||
result = test_fn()
|
||||
if result.blocked:
|
||||
print(f"[BLOCKED] {name}: {result.block_reason}")
|
||||
else:
|
||||
print(f"[FAIL] {name} was NOT blocked!")
|
||||
all_blocked = False
|
||||
|
||||
print("="*50)
|
||||
if all_blocked:
|
||||
print("[OK] All forbidden actions correctly blocked")
|
||||
else:
|
||||
print("[FAIL] Some actions were not blocked!")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+218
@@ -0,0 +1,218 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Tier 0 Agent Bootstrap Script
|
||||
# =============================
|
||||
# Authenticates with Vault, obtains scoped token, initializes agent environment.
|
||||
#
|
||||
# Usage:
|
||||
# ./bootstrap.sh
|
||||
# source ./bootstrap.sh # To export VAULT_TOKEN to current shell
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG_DIR="${SCRIPT_DIR}/config"
|
||||
CREDS_DIR="${SCRIPT_DIR}/credentials"
|
||||
LOGS_DIR="${SCRIPT_DIR}/logs"
|
||||
WORKSPACE_DIR="${SCRIPT_DIR}/workspace"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
log_ok() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
VAULT_ADDR="${VAULT_ADDR:-https://127.0.0.1:8200}"
|
||||
AGENT_CONFIG="${CONFIG_DIR}/agent.json"
|
||||
APPROLE_CREDS="${CREDS_DIR}/approle.json"
|
||||
|
||||
# Read agent config
|
||||
if [[ ! -f "${AGENT_CONFIG}" ]]; then
|
||||
log_error "Agent config not found: ${AGENT_CONFIG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
AGENT_ID=$(python3 -c "import json; print(json.load(open('${AGENT_CONFIG}'))['agent_id'])")
|
||||
AGENT_TIER=$(python3 -c "import json; print(json.load(open('${AGENT_CONFIG}'))['tier'])")
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "TIER 0 AGENT BOOTSTRAP"
|
||||
echo "=========================================="
|
||||
echo "Agent ID: ${AGENT_ID}"
|
||||
echo "Tier: ${AGENT_TIER}"
|
||||
echo "Vault: ${VAULT_ADDR}"
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 1: Vault Authentication
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
log_info "Authenticating with Vault..."
|
||||
|
||||
if [[ ! -f "${APPROLE_CREDS}" ]]; then
|
||||
log_error "AppRole credentials not found: ${APPROLE_CREDS}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROLE_ID=$(python3 -c "import json; print(json.load(open('${APPROLE_CREDS}'))['role_id'])")
|
||||
SECRET_ID=$(python3 -c "import json; print(json.load(open('${APPROLE_CREDS}'))['secret_id'])")
|
||||
|
||||
# Login to Vault
|
||||
LOGIN_RESPONSE=$(curl -sk -X POST \
|
||||
-d "{\"role_id\":\"${ROLE_ID}\",\"secret_id\":\"${SECRET_ID}\"}" \
|
||||
"${VAULT_ADDR}/v1/auth/approle/login")
|
||||
|
||||
# Check for errors
|
||||
if echo "${LOGIN_RESPONSE}" | grep -q '"errors"'; then
|
||||
log_error "Vault login failed:"
|
||||
echo "${LOGIN_RESPONSE}" | python3 -m json.tool
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract token
|
||||
VAULT_TOKEN=$(echo "${LOGIN_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin)['auth']['client_token'])")
|
||||
TOKEN_TTL=$(echo "${LOGIN_RESPONSE}" | python3 -c "import sys,json; print(json.load(sys.stdin)['auth']['lease_duration'])")
|
||||
TOKEN_POLICIES=$(echo "${LOGIN_RESPONSE}" | python3 -c "import sys,json; print(','.join(json.load(sys.stdin)['auth']['policies']))")
|
||||
|
||||
log_ok "Vault authentication successful"
|
||||
echo " Token TTL: ${TOKEN_TTL}s"
|
||||
echo " Policies: ${TOKEN_POLICIES}"
|
||||
|
||||
# Export token
|
||||
export VAULT_TOKEN
|
||||
export VAULT_ADDR
|
||||
|
||||
# Save token to file for agent processes
|
||||
echo "${VAULT_TOKEN}" > "${CREDS_DIR}/.token"
|
||||
chmod 600 "${CREDS_DIR}/.token"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 2: Verify Token Permissions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
log_info "Verifying token permissions..."
|
||||
|
||||
# Test token lookup (should work with agent-self-read)
|
||||
TOKEN_INFO=$(curl -sk -H "X-Vault-Token: ${VAULT_TOKEN}" \
|
||||
"${VAULT_ADDR}/v1/auth/token/lookup-self" 2>/dev/null || echo '{"errors":["failed"]}')
|
||||
|
||||
if echo "${TOKEN_INFO}" | grep -q '"errors"'; then
|
||||
log_warn "Could not verify token (agent-self-read may not be attached)"
|
||||
else
|
||||
log_ok "Token self-lookup successful"
|
||||
fi
|
||||
|
||||
# Test that we CANNOT access secrets (Tier 0 should be denied)
|
||||
SECRETS_TEST=$(curl -sk -H "X-Vault-Token: ${VAULT_TOKEN}" \
|
||||
"${VAULT_ADDR}/v1/secret/data/services/dragonfly" 2>/dev/null || echo '{}')
|
||||
|
||||
if echo "${SECRETS_TEST}" | grep -q '"data"'; then
|
||||
log_error "SECURITY VIOLATION: Tier 0 token can access secrets!"
|
||||
exit 1
|
||||
else
|
||||
log_ok "Confirmed: Cannot access secrets (as expected for Tier 0)"
|
||||
fi
|
||||
|
||||
# Test that we CANNOT access SSH (Tier 0 should be denied)
|
||||
SSH_TEST=$(curl -sk -H "X-Vault-Token: ${VAULT_TOKEN}" \
|
||||
"${VAULT_ADDR}/v1/ssh/creds/sandbox-user" -X POST -d '{"ip":"10.77.10.1"}' 2>/dev/null || echo '{}')
|
||||
|
||||
if echo "${SSH_TEST}" | grep -q '"signed_key"'; then
|
||||
log_error "SECURITY VIOLATION: Tier 0 token can get SSH credentials!"
|
||||
exit 1
|
||||
else
|
||||
log_ok "Confirmed: Cannot access SSH credentials (as expected for Tier 0)"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 3: Initialize Agent Environment
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
log_info "Initializing agent environment..."
|
||||
|
||||
# Export agent environment variables
|
||||
export AGENT_ID
|
||||
export AGENT_TIER
|
||||
export AGENT_CONFIG
|
||||
export AGENT_WORKSPACE="${WORKSPACE_DIR}"
|
||||
export AGENT_LOGS="${LOGS_DIR}"
|
||||
|
||||
# Create session ID
|
||||
export SESSION_ID="sess-$(date +%Y%m%d-%H%M%S)-$(openssl rand -hex 4)"
|
||||
echo "${SESSION_ID}" > "${WORKSPACE_DIR}/.session_id"
|
||||
|
||||
# Initialize log file
|
||||
LOG_FILE="${LOGS_DIR}/agent-${SESSION_ID}.log"
|
||||
echo "=== Agent Session Started ===" > "${LOG_FILE}"
|
||||
echo "Session ID: ${SESSION_ID}" >> "${LOG_FILE}"
|
||||
echo "Agent ID: ${AGENT_ID}" >> "${LOG_FILE}"
|
||||
echo "Tier: ${AGENT_TIER}" >> "${LOG_FILE}"
|
||||
echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${LOG_FILE}"
|
||||
echo "" >> "${LOG_FILE}"
|
||||
|
||||
log_ok "Agent environment initialized"
|
||||
echo " Session: ${SESSION_ID}"
|
||||
echo " Workspace: ${WORKSPACE_DIR}"
|
||||
echo " Logs: ${LOG_FILE}"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Step 4: Register with Governance System
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
log_info "Registering with governance system..."
|
||||
|
||||
# Register in DragonflyDB
|
||||
DRAGONFLY_CREDS=$(curl -sk -H "X-Vault-Token: $(cat /opt/vault/init-keys.json | python3 -c "import sys,json; print(json.load(sys.stdin)['root_token'])")" \
|
||||
"${VAULT_ADDR}/v1/secret/data/services/dragonfly" 2>/dev/null || echo '{}')
|
||||
|
||||
if echo "${DRAGONFLY_CREDS}" | grep -q '"password"'; then
|
||||
REDIS_PASS=$(echo "${DRAGONFLY_CREDS}" | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['data']['password'])")
|
||||
|
||||
# Register agent state
|
||||
redis-cli -p 6379 -a "${REDIS_PASS}" SET "agent:${AGENT_ID}:state" "{\"agent_id\":\"${AGENT_ID}\",\"tier\":${AGENT_TIER},\"status\":\"RUNNING\",\"session_id\":\"${SESSION_ID}\",\"started_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" 2>/dev/null
|
||||
|
||||
# Set heartbeat
|
||||
redis-cli -p 6379 -a "${REDIS_PASS}" SET "agent:${AGENT_ID}:heartbeat" "$(date +%s)" EX 60 2>/dev/null
|
||||
|
||||
log_ok "Registered with governance system"
|
||||
else
|
||||
log_warn "Could not register with DragonflyDB (non-fatal)"
|
||||
fi
|
||||
|
||||
# Register in SQLite ledger
|
||||
sqlite3 /opt/agent-governance/ledger/governance.db "
|
||||
INSERT OR REPLACE INTO agent_metrics (agent_id, current_tier, compliant_runs, consecutive_compliant, total_runs, updated_at)
|
||||
VALUES ('${AGENT_ID}', ${AGENT_TIER}, 0, 0, 0, datetime('now'));
|
||||
" 2>/dev/null || log_warn "Could not update ledger (non-fatal)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Done
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "BOOTSTRAP COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Agent ${AGENT_ID} is ready (Tier ${AGENT_TIER})"
|
||||
echo ""
|
||||
echo "Environment variables set:"
|
||||
echo " VAULT_TOKEN (use for Vault API calls)"
|
||||
echo " VAULT_ADDR = ${VAULT_ADDR}"
|
||||
echo " AGENT_ID = ${AGENT_ID}"
|
||||
echo " AGENT_TIER = ${AGENT_TIER}"
|
||||
echo " SESSION_ID = ${SESSION_ID}"
|
||||
echo ""
|
||||
echo "Next: Run the agent with ./run-agent.sh"
|
||||
echo "=========================================="
|
||||
@@ -0,0 +1,32 @@
|
||||
# Config
|
||||
|
||||
> Observer-tier agent (read-only) - config submodule
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains observer-tier agent (read-only) - config submodule.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `agent.json` | Data/Schema |
|
||||
|
||||
## 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: [Tier0 Agent](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Status: Config
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## 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:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_version": "1.0.0",
|
||||
"tier": 0,
|
||||
"tier_name": "Observer",
|
||||
|
||||
"description": "Tier 0 Observer Agent - Read-only access, plan generation only",
|
||||
|
||||
"capabilities": {
|
||||
"read_inventory": true,
|
||||
"read_documentation": true,
|
||||
"generate_plans": true,
|
||||
"execute_commands": false,
|
||||
"modify_files": false,
|
||||
"access_secrets": false,
|
||||
"ssh_access": false,
|
||||
"api_access": false
|
||||
},
|
||||
|
||||
"constraints": {
|
||||
"allowed_actions": [
|
||||
"read_file",
|
||||
"list_directory",
|
||||
"search_code",
|
||||
"generate_plan",
|
||||
"request_review"
|
||||
],
|
||||
"forbidden_actions": [
|
||||
"execute_command",
|
||||
"write_file",
|
||||
"delete_file",
|
||||
"ssh_connect",
|
||||
"api_call",
|
||||
"terraform_apply",
|
||||
"ansible_run"
|
||||
],
|
||||
"allowed_paths": [
|
||||
"/opt/agent-governance/docs/",
|
||||
"/opt/agent-governance/inventory/",
|
||||
"/opt/agent-governance/agents/tier0-agent/workspace/",
|
||||
"/opt/agent-governance/agents/tier0-agent/plans/"
|
||||
],
|
||||
"forbidden_paths": [
|
||||
"/opt/vault/",
|
||||
"/etc/",
|
||||
"/root/",
|
||||
"**/.env",
|
||||
"**/credentials*",
|
||||
"**/secrets*"
|
||||
]
|
||||
},
|
||||
|
||||
"vault": {
|
||||
"auth_method": "approle",
|
||||
"role_name": "tier0-agent",
|
||||
"token_ttl": "1h",
|
||||
"token_max_ttl": "4h",
|
||||
"policies": ["t0-observer", "agent-self-read"]
|
||||
},
|
||||
|
||||
"governance": {
|
||||
"preflight_required": true,
|
||||
"plan_approval_required": true,
|
||||
"evidence_required": true,
|
||||
"heartbeat_interval": 30,
|
||||
"error_budget": {
|
||||
"max_total_errors": 5,
|
||||
"max_same_error_repeats": 2
|
||||
}
|
||||
},
|
||||
|
||||
"promotion": {
|
||||
"target_tier": 1,
|
||||
"requirements": {
|
||||
"min_compliant_runs": 5,
|
||||
"min_consecutive_compliant": 3,
|
||||
"required_actions": ["generate_plan"],
|
||||
"max_violations_30d": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
hvs.CAESIDquQkRp_KXaVTn_7qjDbDN_2cxHW56RI7hJ0LvS9iqNGh4KHGh2cy5vOWVoTUVkN0VOVEVSd2hvWGlFWnY3bXg
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"role_id": "95f5ab5b-e79b-05f3-6019-53cd86fa0e38",
|
||||
"secret_id": "26691ba6-b6f0-854d-e1ae-c32620393dae",
|
||||
"secret_id_accessor": "8df870a9-78ca-3c6e-059b-40deefbd0c82",
|
||||
"created_at": "2026-01-23T20:13:00Z",
|
||||
"expires_at": "2026-01-24T20:13:00Z",
|
||||
"note": "Regenerate secret_id before expiry with: vault write -f auth/approle/role/tier0-agent/secret-id"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Plans
|
||||
|
||||
> Observer-tier agent (read-only) - plans submodule
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains observer-tier agent (read-only) - plans submodule.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `plan-20260123-214700-spark-logging-pipeline.json` | Data/Schema |
|
||||
| `plan-20260123-163928-bbd5dfb1.json` | Data/Schema |
|
||||
| `plan-20260123-163935-51ed67f4.json` | Data/Schema |
|
||||
| `plan-20260123-160942-93e4ab29.json` | Data/Schema |
|
||||
| `plan-20260123-151945-1a7acb76.json` | Data/Schema |
|
||||
| `plan-20260123-163921-2fbea5fc.json` | Data/Schema |
|
||||
|
||||
## 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: [Tier0 Agent](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Status: Plans
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## 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:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-151945-1a7acb76",
|
||||
"title": "Setup Nginx on sandbox-vm-01",
|
||||
"description": "Install and configure Nginx web server on sandbox VM for testing",
|
||||
"target": "sandbox-vm-01",
|
||||
"steps": [
|
||||
"Install nginx package",
|
||||
"Configure default site",
|
||||
"Enable and start nginx service",
|
||||
"Verify nginx is running"
|
||||
],
|
||||
"rollback_steps": [
|
||||
"Stop nginx service",
|
||||
"Remove nginx package",
|
||||
"Clean up config files"
|
||||
],
|
||||
"created_at": "2026-01-23T20:19:45.941428+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "draft",
|
||||
"requires_approval": true,
|
||||
"approved_by": null,
|
||||
"executed": false
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-160942-93e4ab29",
|
||||
"title": "Deploy Apache Spark in Docker",
|
||||
"description": "Deploy Apache Spark cluster in Docker with web UI exposed on port 9944",
|
||||
"target": "localhost",
|
||||
"steps": [
|
||||
{
|
||||
"action": "pull_image",
|
||||
"description": "Pull Apache Spark Docker image",
|
||||
"command": "docker pull apache/spark:latest"
|
||||
},
|
||||
{
|
||||
"action": "create_network",
|
||||
"description": "Create Docker network for Spark cluster",
|
||||
"command": "docker network create spark-net"
|
||||
},
|
||||
{
|
||||
"action": "run_master",
|
||||
"description": "Start Spark master node with UI on port 9944",
|
||||
"command": "docker run -d --name spark-master --network spark-net -p 9944:8080 -p 7077:7077 apache/spark:latest /opt/spark/bin/spark-class org.apache.spark.deploy.master.Master"
|
||||
},
|
||||
{
|
||||
"action": "run_worker",
|
||||
"description": "Start Spark worker node",
|
||||
"command": "docker run -d --name spark-worker --network spark-net apache/spark:latest /opt/spark/bin/spark-class org.apache.spark.deploy.worker.Worker spark://spark-master:7077"
|
||||
},
|
||||
{
|
||||
"action": "verify",
|
||||
"description": "Verify Spark UI is accessible",
|
||||
"command": "curl -s http://localhost:9944 | grep -q Spark"
|
||||
}
|
||||
],
|
||||
"rollback_steps": [
|
||||
{
|
||||
"action": "stop_worker",
|
||||
"command": "docker stop spark-worker && docker rm spark-worker"
|
||||
},
|
||||
{
|
||||
"action": "stop_master",
|
||||
"command": "docker stop spark-master && docker rm spark-master"
|
||||
},
|
||||
{
|
||||
"action": "remove_network",
|
||||
"command": "docker network rm spark-net"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-01-23T21:09:42.432257+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "approved",
|
||||
"requires_approval": true,
|
||||
"approved_by": "human-operator",
|
||||
"executed": true
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-163921-2fbea5fc",
|
||||
"title": "Deploy Redis Cache",
|
||||
"description": "Deploy Redis cache server in Docker for application caching",
|
||||
"target": "localhost",
|
||||
"steps": [
|
||||
{
|
||||
"action": "pull_image",
|
||||
"description": "Pull Redis image",
|
||||
"command": "docker pull redis:7-alpine"
|
||||
},
|
||||
{
|
||||
"action": "run_container",
|
||||
"description": "Start Redis with persistence",
|
||||
"command": "docker run -d --name redis-cache --network spark-net -p 6380:6379 -v redis-data:/data redis:7-alpine redis-server --appendonly yes"
|
||||
},
|
||||
{
|
||||
"action": "verify",
|
||||
"description": "Verify Redis responds to ping",
|
||||
"command": "docker exec redis-cache redis-cli ping"
|
||||
}
|
||||
],
|
||||
"rollback_steps": [
|
||||
{
|
||||
"action": "stop_container",
|
||||
"command": "docker stop redis-cache && docker rm redis-cache"
|
||||
},
|
||||
{
|
||||
"action": "remove_volume",
|
||||
"command": "docker volume rm redis-data"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-01-23T21:39:21.352391+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "approved",
|
||||
"requires_approval": true,
|
||||
"approved_by": "human-operator",
|
||||
"executed": true
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-163928-bbd5dfb1",
|
||||
"title": "Deploy Nginx Reverse Proxy",
|
||||
"description": "Deploy Nginx as reverse proxy for Spark UI with basic auth",
|
||||
"target": "localhost",
|
||||
"steps": [
|
||||
{
|
||||
"action": "pull_image",
|
||||
"description": "Pull Nginx image",
|
||||
"command": "docker pull nginx:alpine"
|
||||
},
|
||||
{
|
||||
"action": "create_config",
|
||||
"description": "Create nginx config directory",
|
||||
"command": "mkdir -p /opt/agent-governance/agents/tier0-agent/workspace/nginx"
|
||||
},
|
||||
{
|
||||
"action": "run_container",
|
||||
"description": "Start Nginx proxy",
|
||||
"command": "docker run -d --name nginx-proxy --network spark-net -p 8080:80 nginx:alpine"
|
||||
},
|
||||
{
|
||||
"action": "verify",
|
||||
"description": "Verify Nginx responds",
|
||||
"command": "curl -s http://localhost:8080 | grep -q nginx"
|
||||
}
|
||||
],
|
||||
"rollback_steps": [
|
||||
{
|
||||
"action": "stop_container",
|
||||
"command": "docker stop nginx-proxy && docker rm nginx-proxy"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-01-23T21:39:28.520158+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "approved",
|
||||
"requires_approval": true,
|
||||
"approved_by": "human-operator",
|
||||
"executed": true
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-163935-51ed67f4",
|
||||
"title": "Deploy Prometheus Monitoring",
|
||||
"description": "Deploy Prometheus for metrics collection from Spark cluster",
|
||||
"target": "localhost",
|
||||
"steps": [
|
||||
{
|
||||
"action": "pull_image",
|
||||
"description": "Pull Prometheus image",
|
||||
"command": "docker pull prom/prometheus:latest"
|
||||
},
|
||||
{
|
||||
"action": "create_config_dir",
|
||||
"description": "Create Prometheus config directory",
|
||||
"command": "mkdir -p /opt/agent-governance/agents/tier0-agent/workspace/prometheus"
|
||||
},
|
||||
{
|
||||
"action": "run_container",
|
||||
"description": "Start Prometheus",
|
||||
"command": "docker run -d --name prometheus --network spark-net -p 9090:9090 prom/prometheus:latest"
|
||||
},
|
||||
{
|
||||
"action": "verify",
|
||||
"description": "Verify Prometheus UI accessible",
|
||||
"command": "curl -s http://localhost:9090/-/healthy"
|
||||
}
|
||||
],
|
||||
"rollback_steps": [
|
||||
{
|
||||
"action": "stop_container",
|
||||
"command": "docker stop prometheus && docker rm prometheus"
|
||||
}
|
||||
],
|
||||
"created_at": "2026-01-23T21:39:35.251841+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "approved",
|
||||
"requires_approval": true,
|
||||
"approved_by": "human-operator",
|
||||
"executed": true
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
{
|
||||
"plan_id": "plan-20260123-214700-spark-logging-pipeline",
|
||||
"title": "Spark Cluster Logging Aggregation Pipeline",
|
||||
"description": "Production-grade logging aggregation system for Spark cluster using Lambda architecture with Kafka, Flink, and ClickHouse",
|
||||
"version": "1.0.0",
|
||||
"type": "ground_truth",
|
||||
"target": "localhost",
|
||||
"source": {
|
||||
"generated_by": "multi-agent-orchestrator",
|
||||
"task_id": "task-zfwla3-mkretltd",
|
||||
"model": "anthropic/claude-sonnet-4",
|
||||
"proposal_id": "t2aiqzohmkreue73",
|
||||
"evaluation": {
|
||||
"score": 0.85,
|
||||
"feasibility": 0.88,
|
||||
"completeness": 0.82,
|
||||
"evaluator": "BETA"
|
||||
}
|
||||
},
|
||||
"architecture": {
|
||||
"pattern": "Lambda Architecture",
|
||||
"components": {
|
||||
"ingestion": {
|
||||
"technology": "Apache Kafka",
|
||||
"purpose": "High-throughput log ingestion with partitioning",
|
||||
"features": ["Schema Registry", "Topic-per-source", "Compression"]
|
||||
},
|
||||
"stream_processing": {
|
||||
"technology": "Apache Flink",
|
||||
"purpose": "Real-time processing, enrichment, and transformation",
|
||||
"features": ["Checkpointing", "Exactly-once semantics", "Backpressure handling"]
|
||||
},
|
||||
"storage": {
|
||||
"technology": "ClickHouse",
|
||||
"purpose": "Columnar storage optimized for analytics queries",
|
||||
"features": ["Materialized views", "ReplicatedMergeTree", "Tiered storage"]
|
||||
},
|
||||
"schema_management": {
|
||||
"technology": "Confluent Schema Registry",
|
||||
"purpose": "Schema evolution and format governance",
|
||||
"formats": ["JSON", "Avro", "Protobuf"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"requirements": {
|
||||
"performance": {
|
||||
"query_latency": "<100ms",
|
||||
"ingestion_throughput": "high-volume",
|
||||
"data_freshness": "near-real-time"
|
||||
},
|
||||
"constraints": [
|
||||
"Must use open-source technologies where possible",
|
||||
"Latency < 100ms for query responses",
|
||||
"Support for multiple data formats (JSON, Avro, Protobuf)",
|
||||
"Cost-effective for variable workloads"
|
||||
],
|
||||
"success_criteria": [
|
||||
"Complete architecture design with component diagrams",
|
||||
"Data flow specifications",
|
||||
"Scalability analysis",
|
||||
"Fault tolerance mechanisms documented",
|
||||
"Cost estimation provided"
|
||||
]
|
||||
},
|
||||
"phases": [
|
||||
{
|
||||
"phase_id": "phase-1-ingestion",
|
||||
"name": "Design High-Performance Ingestion Layer",
|
||||
"complexity": "high",
|
||||
"estimated_effort": "3-4 days",
|
||||
"owner": "ALPHA",
|
||||
"key_decisions": [
|
||||
"Partitioning strategy",
|
||||
"Schema registry setup",
|
||||
"Format conversion pipeline"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "1.1",
|
||||
"action": "deploy_kafka",
|
||||
"description": "Deploy Kafka cluster with Zookeeper",
|
||||
"command": "docker-compose -f kafka-cluster.yml up -d",
|
||||
"artifacts": ["kafka-cluster.yml"]
|
||||
},
|
||||
{
|
||||
"step_id": "1.2",
|
||||
"action": "deploy_schema_registry",
|
||||
"description": "Deploy Confluent Schema Registry",
|
||||
"command": "docker run -d --name schema-registry --network spark-net -p 8081:8081 -e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=kafka:9092 confluentinc/cp-schema-registry:latest"
|
||||
},
|
||||
{
|
||||
"step_id": "1.3",
|
||||
"action": "create_topics",
|
||||
"description": "Create Kafka topics for Spark logs",
|
||||
"command": "kafka-topics.sh --create --topic spark-driver-logs --partitions 12 --replication-factor 2 --bootstrap-server localhost:9092"
|
||||
},
|
||||
{
|
||||
"step_id": "1.4",
|
||||
"action": "configure_spark_log_forwarding",
|
||||
"description": "Configure Spark to forward logs to Kafka",
|
||||
"config": {
|
||||
"spark.driver.extraJavaOptions": "-Dlog4j.configuration=file:/opt/spark/conf/log4j-kafka.properties",
|
||||
"spark.executor.extraJavaOptions": "-Dlog4j.configuration=file:/opt/spark/conf/log4j-kafka.properties"
|
||||
}
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"command": "kafka-console-consumer.sh --topic spark-driver-logs --bootstrap-server localhost:9092 --max-messages 5",
|
||||
"expected": "Log messages flowing"
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase_id": "phase-2-storage",
|
||||
"name": "Optimize Storage and Query Performance",
|
||||
"complexity": "high",
|
||||
"estimated_effort": "4-5 days",
|
||||
"owner": "ALPHA",
|
||||
"key_decisions": [
|
||||
"Storage engine selection",
|
||||
"Index design",
|
||||
"Query routing logic"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "2.1",
|
||||
"action": "deploy_clickhouse",
|
||||
"description": "Deploy ClickHouse cluster",
|
||||
"command": "docker run -d --name clickhouse-server --network spark-net -p 8123:8123 -p 9000:9000 -v clickhouse-data:/var/lib/clickhouse clickhouse/clickhouse-server:latest"
|
||||
},
|
||||
{
|
||||
"step_id": "2.2",
|
||||
"action": "create_log_table",
|
||||
"description": "Create optimized log table with ReplicatedMergeTree",
|
||||
"sql": "CREATE TABLE spark_logs (timestamp DateTime64(3), level String, logger String, message String, spark_app_id String, executor_id String, host String) ENGINE = MergeTree() PARTITION BY toYYYYMMDD(timestamp) ORDER BY (spark_app_id, timestamp) TTL timestamp + INTERVAL 30 DAY"
|
||||
},
|
||||
{
|
||||
"step_id": "2.3",
|
||||
"action": "create_materialized_views",
|
||||
"description": "Create materialized views for common query patterns",
|
||||
"sql": "CREATE MATERIALIZED VIEW spark_logs_by_level ENGINE = SummingMergeTree() ORDER BY (level, toStartOfHour(timestamp)) AS SELECT level, toStartOfHour(timestamp) as hour, count() as count FROM spark_logs GROUP BY level, hour"
|
||||
},
|
||||
{
|
||||
"step_id": "2.4",
|
||||
"action": "configure_tiered_storage",
|
||||
"description": "Configure hot-warm-cold tiered storage",
|
||||
"config": {
|
||||
"hot_tier": "7 days on NVMe SSD",
|
||||
"warm_tier": "30 days on SSD",
|
||||
"cold_tier": "90+ days on S3/object storage"
|
||||
}
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"command": "clickhouse-client --query 'SELECT count() FROM spark_logs'",
|
||||
"expected": "Query returns in <100ms"
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase_id": "phase-3-processing",
|
||||
"name": "Implement Real-time Processing Pipeline",
|
||||
"complexity": "medium-high",
|
||||
"estimated_effort": "3-4 days",
|
||||
"owner": "BETA",
|
||||
"key_decisions": [
|
||||
"Processing framework choice",
|
||||
"State management",
|
||||
"Backpressure handling"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "3.1",
|
||||
"action": "deploy_flink",
|
||||
"description": "Deploy Apache Flink cluster",
|
||||
"command": "docker run -d --name flink-jobmanager --network spark-net -p 8082:8081 flink:latest jobmanager"
|
||||
},
|
||||
{
|
||||
"step_id": "3.2",
|
||||
"action": "deploy_flink_taskmanager",
|
||||
"description": "Deploy Flink TaskManager",
|
||||
"command": "docker run -d --name flink-taskmanager --network spark-net flink:latest taskmanager"
|
||||
},
|
||||
{
|
||||
"step_id": "3.3",
|
||||
"action": "deploy_log_processor_job",
|
||||
"description": "Deploy Flink job for log processing",
|
||||
"job_config": {
|
||||
"source": "Kafka spark-driver-logs topic",
|
||||
"transformations": [
|
||||
"Parse log format",
|
||||
"Extract structured fields",
|
||||
"Enrich with metadata",
|
||||
"Handle format conversion (JSON/Avro/Protobuf)"
|
||||
],
|
||||
"sink": "ClickHouse spark_logs table",
|
||||
"checkpointing": "10 seconds",
|
||||
"parallelism": 4
|
||||
}
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"command": "curl http://localhost:8082/jobs",
|
||||
"expected": "Log processor job RUNNING"
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase_id": "phase-4-fault-tolerance",
|
||||
"name": "Implement Comprehensive Fault Tolerance",
|
||||
"complexity": "medium-high",
|
||||
"estimated_effort": "3-4 days",
|
||||
"owner": "BETA",
|
||||
"key_decisions": [
|
||||
"Replication strategies",
|
||||
"Failure detection",
|
||||
"Recovery procedures"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "4.1",
|
||||
"action": "configure_kafka_replication",
|
||||
"description": "Configure Kafka replication and ISR",
|
||||
"config": {
|
||||
"replication.factor": 2,
|
||||
"min.insync.replicas": 1,
|
||||
"unclean.leader.election.enable": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "4.2",
|
||||
"action": "configure_flink_checkpointing",
|
||||
"description": "Enable Flink checkpointing for exactly-once",
|
||||
"config": {
|
||||
"execution.checkpointing.interval": "10s",
|
||||
"execution.checkpointing.mode": "EXACTLY_ONCE",
|
||||
"state.backend": "rocksdb",
|
||||
"state.checkpoints.dir": "file:///opt/flink/checkpoints"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "4.3",
|
||||
"action": "configure_clickhouse_replication",
|
||||
"description": "Configure ClickHouse replication",
|
||||
"config": {
|
||||
"engine": "ReplicatedMergeTree",
|
||||
"zookeeper_path": "/clickhouse/tables/{shard}/spark_logs",
|
||||
"replica_name": "{replica}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "4.4",
|
||||
"action": "setup_dead_letter_queue",
|
||||
"description": "Create DLQ for failed log entries",
|
||||
"command": "kafka-topics.sh --create --topic spark-logs-dlq --partitions 3 --replication-factor 2 --bootstrap-server localhost:9092"
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"test": "Kill one Kafka broker, verify no data loss",
|
||||
"expected": "System continues processing with <5s recovery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"phase_id": "phase-5-scaling",
|
||||
"name": "Design Cost-Effective Scaling Strategy",
|
||||
"complexity": "medium",
|
||||
"estimated_effort": "2-3 days",
|
||||
"owner": "BETA",
|
||||
"key_decisions": [
|
||||
"Tiering policies",
|
||||
"Auto-scaling triggers",
|
||||
"Resource allocation"
|
||||
],
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "5.1",
|
||||
"action": "configure_autoscaling",
|
||||
"description": "Configure Kubernetes HPA for Flink",
|
||||
"config": {
|
||||
"min_replicas": 2,
|
||||
"max_replicas": 10,
|
||||
"target_cpu_utilization": 70,
|
||||
"scale_up_stabilization": "60s",
|
||||
"scale_down_stabilization": "300s"
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "5.2",
|
||||
"action": "configure_kafka_tiering",
|
||||
"description": "Enable Kafka tiered storage",
|
||||
"config": {
|
||||
"remote.log.storage.system.enable": true,
|
||||
"remote.log.storage.manager.class.name": "org.apache.kafka.server.log.remote.storage.LocalTieredStorage",
|
||||
"local.retention.ms": 86400000
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_id": "5.3",
|
||||
"action": "setup_monitoring",
|
||||
"description": "Deploy Prometheus + Grafana monitoring",
|
||||
"dashboards": [
|
||||
"Kafka lag per topic/partition",
|
||||
"Flink throughput and backpressure",
|
||||
"ClickHouse query latency p50/p95/p99",
|
||||
"Storage utilization per tier"
|
||||
]
|
||||
}
|
||||
],
|
||||
"verification": {
|
||||
"test": "Generate 10x load spike",
|
||||
"expected": "System scales up within 2 minutes, scales down within 10 minutes"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rollback_strategy": {
|
||||
"checkpoints": [
|
||||
"After each phase completion",
|
||||
"Before any destructive operation"
|
||||
],
|
||||
"procedures": [
|
||||
{
|
||||
"trigger": "Kafka cluster failure",
|
||||
"action": "Failover to standby cluster, replay from checkpoint"
|
||||
},
|
||||
{
|
||||
"trigger": "Flink job failure",
|
||||
"action": "Restart from last checkpoint, resume processing"
|
||||
},
|
||||
{
|
||||
"trigger": "ClickHouse corruption",
|
||||
"action": "Restore from replica, rebuild materialized views"
|
||||
},
|
||||
{
|
||||
"trigger": "Full system rollback",
|
||||
"action": "Stop all components, restore from backup, replay Kafka from offset"
|
||||
}
|
||||
]
|
||||
},
|
||||
"monitoring": {
|
||||
"metrics": [
|
||||
{
|
||||
"name": "ingestion_lag",
|
||||
"source": "Kafka consumer lag",
|
||||
"threshold": "<1000 messages",
|
||||
"alert": "PagerDuty if >5000 for 5 minutes"
|
||||
},
|
||||
{
|
||||
"name": "query_latency_p99",
|
||||
"source": "ClickHouse query logs",
|
||||
"threshold": "<100ms",
|
||||
"alert": "Slack if >200ms for 1 minute"
|
||||
},
|
||||
{
|
||||
"name": "processing_throughput",
|
||||
"source": "Flink metrics",
|
||||
"threshold": ">10000 events/sec",
|
||||
"alert": "Email if <5000 for 10 minutes"
|
||||
}
|
||||
],
|
||||
"dashboards": [
|
||||
"System Overview",
|
||||
"Kafka Health",
|
||||
"Flink Jobs",
|
||||
"ClickHouse Performance",
|
||||
"Cost Analysis"
|
||||
]
|
||||
},
|
||||
"cost_estimation": {
|
||||
"infrastructure": {
|
||||
"kafka_cluster": "$200-400/month (3 brokers)",
|
||||
"flink_cluster": "$150-300/month (1 JM + 2 TM)",
|
||||
"clickhouse_cluster": "$300-600/month (3 nodes)",
|
||||
"monitoring": "$50-100/month"
|
||||
},
|
||||
"total_monthly": "$700-1400",
|
||||
"optimization_notes": [
|
||||
"Use spot instances for Flink TaskManagers",
|
||||
"Tiered storage reduces ClickHouse costs by 40%",
|
||||
"Auto-scaling minimizes idle capacity"
|
||||
]
|
||||
},
|
||||
"strengths": [
|
||||
"Excellent technology selection - Kafka/Flink/ClickHouse is a proven stack for log analytics",
|
||||
"Sub-100ms query performance through ClickHouse's columnar engine and materialized views",
|
||||
"Comprehensive tiered storage strategy optimizes costs while maintaining performance",
|
||||
"Schema Registry integration provides robust data governance and evolution",
|
||||
"Strong fault tolerance with Kafka replication and Flink checkpointing",
|
||||
"Auto-scaling capabilities address variable load patterns effectively",
|
||||
"Unified query API abstracts complexity from end users"
|
||||
],
|
||||
"known_limitations": [
|
||||
"Lambda architecture complexity requires significant DevOps expertise",
|
||||
"Data consistency reconciliation between real-time and batch layers needs monitoring",
|
||||
"Flink memory management under high throughput scenarios needs careful tuning"
|
||||
],
|
||||
"implementation_notes": "Start with a pilot deployment focusing on a single log source to validate the architecture. Implement comprehensive monitoring before scaling. Pay special attention to Flink job tuning and ClickHouse table engine selection. Consider using ClickHouse's ReplicatedMergeTree for high availability. Plan for gradual migration from existing systems with parallel running during transition period.",
|
||||
"created_at": "2026-01-23T21:47:00.000000+00:00",
|
||||
"agent_id": "tier0-agent-001",
|
||||
"agent_tier": 0,
|
||||
"status": "approved",
|
||||
"requires_approval": true,
|
||||
"approved_by": "multi-agent-consensus",
|
||||
"executed": false,
|
||||
"priority": "high",
|
||||
"tags": ["spark", "logging", "kafka", "flink", "clickhouse", "lambda-architecture", "ground-truth"]
|
||||
}
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Tier 0 Agent Runner
|
||||
# ===================
|
||||
# Bootstraps and runs the Tier 0 agent with full governance.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Bootstrap first (sets up environment)
|
||||
source "${SCRIPT_DIR}/bootstrap.sh"
|
||||
|
||||
echo ""
|
||||
echo "Starting agent..."
|
||||
echo ""
|
||||
|
||||
# Run agent command
|
||||
python3 "${SCRIPT_DIR}/agent.py" "$@"
|
||||
@@ -0,0 +1 @@
|
||||
sess-20260123-160825-549d8bbb
|
||||
Reference in New Issue
Block a user