agentops-cockpit 0.9.7__py3-none-any.whl → 0.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_ops_cockpit/agent.py +43 -81
- agent_ops_cockpit/cache/semantic_cache.py +10 -21
- agent_ops_cockpit/cli/main.py +105 -153
- agent_ops_cockpit/eval/load_test.py +33 -50
- agent_ops_cockpit/eval/quality_climber.py +88 -93
- agent_ops_cockpit/eval/red_team.py +54 -21
- agent_ops_cockpit/mcp_server.py +26 -93
- agent_ops_cockpit/ops/arch_review.py +221 -148
- agent_ops_cockpit/ops/auditors/base.py +50 -0
- agent_ops_cockpit/ops/auditors/behavioral.py +31 -0
- agent_ops_cockpit/ops/auditors/compliance.py +35 -0
- agent_ops_cockpit/ops/auditors/dependency.py +48 -0
- agent_ops_cockpit/ops/auditors/finops.py +48 -0
- agent_ops_cockpit/ops/auditors/graph.py +49 -0
- agent_ops_cockpit/ops/auditors/pivot.py +51 -0
- agent_ops_cockpit/ops/auditors/reasoning.py +67 -0
- agent_ops_cockpit/ops/auditors/reliability.py +53 -0
- agent_ops_cockpit/ops/auditors/security.py +87 -0
- agent_ops_cockpit/ops/auditors/sme_v12.py +76 -0
- agent_ops_cockpit/ops/auditors/sovereignty.py +74 -0
- agent_ops_cockpit/ops/auditors/sre_a2a.py +179 -0
- agent_ops_cockpit/ops/benchmarker.py +97 -0
- agent_ops_cockpit/ops/cost_optimizer.py +15 -24
- agent_ops_cockpit/ops/discovery.py +214 -0
- agent_ops_cockpit/ops/evidence_bridge.py +30 -63
- agent_ops_cockpit/ops/frameworks.py +124 -1
- agent_ops_cockpit/ops/git_portal.py +74 -0
- agent_ops_cockpit/ops/mcp_hub.py +19 -42
- agent_ops_cockpit/ops/orchestrator.py +477 -277
- agent_ops_cockpit/ops/policy_engine.py +38 -38
- agent_ops_cockpit/ops/reliability.py +120 -65
- agent_ops_cockpit/ops/remediator.py +54 -0
- agent_ops_cockpit/ops/secret_scanner.py +34 -22
- agent_ops_cockpit/ops/swarm.py +17 -27
- agent_ops_cockpit/ops/ui_auditor.py +67 -6
- agent_ops_cockpit/ops/watcher.py +41 -70
- agent_ops_cockpit/ops/watchlist.json +30 -0
- agent_ops_cockpit/optimizer.py +157 -407
- agent_ops_cockpit/tests/test_arch_review.py +6 -6
- agent_ops_cockpit/tests/test_discovery.py +96 -0
- agent_ops_cockpit/tests/test_ops_core.py +56 -0
- agent_ops_cockpit/tests/test_orchestrator_fleet.py +73 -0
- agent_ops_cockpit/tests/test_persona_architect.py +75 -0
- agent_ops_cockpit/tests/test_persona_finops.py +31 -0
- agent_ops_cockpit/tests/test_persona_security.py +55 -0
- agent_ops_cockpit/tests/test_persona_sre.py +43 -0
- agent_ops_cockpit/tests/test_persona_ux.py +42 -0
- agent_ops_cockpit/tests/test_quality_climber.py +2 -2
- agent_ops_cockpit/tests/test_remediator.py +75 -0
- agent_ops_cockpit/tests/test_ui_auditor.py +52 -0
- agentops_cockpit-0.9.8.dist-info/METADATA +172 -0
- agentops_cockpit-0.9.8.dist-info/RECORD +71 -0
- agent_ops_cockpit/tests/test_optimizer.py +0 -68
- agent_ops_cockpit/tests/test_red_team.py +0 -35
- agent_ops_cockpit/tests/test_secret_scanner.py +0 -24
- agentops_cockpit-0.9.7.dist-info/METADATA +0 -246
- agentops_cockpit-0.9.7.dist-info/RECORD +0 -47
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/WHEEL +0 -0
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/entry_points.txt +0 -0
- {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
|
+
|
|
4
|
+
# Ensure the project root is in sys.path for the tenacity mock
|
|
5
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
6
|
+
cockpit_root = os.path.dirname(os.path.dirname(os.path.dirname(script_dir)))
|
|
7
|
+
if cockpit_root not in sys.path:
|
|
8
|
+
sys.path.insert(0, cockpit_root)
|
|
9
|
+
# Also add src to path for internal imports
|
|
10
|
+
src_dir = os.path.dirname(os.path.dirname(script_dir))
|
|
11
|
+
if src_dir not in sys.path:
|
|
12
|
+
sys.path.insert(0, src_dir)
|
|
13
|
+
|
|
14
|
+
from tenacity import retry, wait_exponential, stop_after_attempt
|
|
3
15
|
import subprocess
|
|
16
|
+
import json
|
|
17
|
+
import hashlib
|
|
18
|
+
import time
|
|
4
19
|
from datetime import datetime
|
|
5
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
20
|
+
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
|
|
6
21
|
from rich.console import Console
|
|
7
22
|
from rich.panel import Panel
|
|
8
23
|
from rich.table import Table
|
|
9
24
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskID
|
|
10
|
-
|
|
11
25
|
console = Console()
|
|
12
26
|
|
|
13
27
|
class CockpitOrchestrator:
|
|
@@ -15,403 +29,589 @@ class CockpitOrchestrator:
|
|
|
15
29
|
Main orchestrator for AgentOps audits.
|
|
16
30
|
Optimized for concurrency and real-time progress visibility.
|
|
17
31
|
"""
|
|
18
|
-
|
|
32
|
+
|
|
19
33
|
def __init__(self):
|
|
20
|
-
self.timestamp = datetime.now().strftime(
|
|
21
|
-
self.
|
|
34
|
+
self.timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
35
|
+
self.version = os.environ.get('AUDIT_VERSION', 'v1')
|
|
36
|
+
self.report_path = f'cockpit_final_report_{self.version}.md'
|
|
22
37
|
self.results = {}
|
|
23
38
|
self.total_steps = 7
|
|
24
39
|
self.completed_steps = 0
|
|
40
|
+
self.workspace_results = {} # For fleet intelligence
|
|
41
|
+
self.common_debt = {}
|
|
42
|
+
|
|
43
|
+
def get_dir_hash(self, path: str):
|
|
44
|
+
"""Calculates a recursive hash of the directory contents for intelligent skipping."""
|
|
45
|
+
from agent_ops_cockpit.ops.discovery import DiscoveryEngine
|
|
46
|
+
discovery = DiscoveryEngine(path)
|
|
47
|
+
hasher = hashlib.md5()
|
|
48
|
+
for f_path in sorted(discovery.walk(path)):
|
|
49
|
+
if not f_path.endswith(('.py', '.ts', '.js', '.go', '.json', '.yaml', '.prompt', '.md', 'toml')):
|
|
50
|
+
continue
|
|
51
|
+
try:
|
|
52
|
+
with open(f_path, 'rb') as f:
|
|
53
|
+
while chunk := f.read(8192):
|
|
54
|
+
hasher.update(chunk)
|
|
55
|
+
except: pass
|
|
56
|
+
return hasher.hexdigest()
|
|
57
|
+
|
|
58
|
+
def detect_entry_point(self, path: str):
|
|
59
|
+
"""Autodetection logic using Discovery Engine."""
|
|
60
|
+
from agent_ops_cockpit.ops.discovery import DiscoveryEngine
|
|
61
|
+
discovery = DiscoveryEngine(path)
|
|
62
|
+
brain = discovery.find_agent_brain()
|
|
63
|
+
return os.path.relpath(brain, path)
|
|
25
64
|
|
|
26
65
|
def run_command(self, name: str, cmd: list, progress: Progress, task_id: TaskID):
|
|
27
66
|
"""Helper to run a command and capture output while updating progress."""
|
|
28
|
-
progress.update(task_id, description=f
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
cockpit_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
67
|
+
progress.update(task_id, description=f'[cyan]Running {name}...')
|
|
68
|
+
cockpit_src = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
69
|
+
cockpit_root = os.path.dirname(cockpit_src)
|
|
32
70
|
env = os.environ.copy()
|
|
33
|
-
env[
|
|
34
|
-
|
|
71
|
+
env['PYTHONPATH'] = f"{cockpit_src}{os.pathsep}{cockpit_root}{os.pathsep}{env.get('PYTHONPATH', '')}"
|
|
35
72
|
try:
|
|
36
|
-
|
|
37
|
-
# but for now we'll just run it and capture.
|
|
38
|
-
process = subprocess.Popen(
|
|
39
|
-
cmd,
|
|
40
|
-
stdout=subprocess.PIPE,
|
|
41
|
-
stderr=subprocess.PIPE,
|
|
42
|
-
text=True,
|
|
43
|
-
env=env
|
|
44
|
-
)
|
|
45
|
-
|
|
73
|
+
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=env)
|
|
46
74
|
stdout, stderr = process.communicate()
|
|
47
75
|
success = process.returncode == 0
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.results[name] = {
|
|
51
|
-
"success": success,
|
|
52
|
-
"output": output
|
|
53
|
-
}
|
|
54
|
-
|
|
76
|
+
output = stdout if success else f'{stdout}\n{stderr}'
|
|
77
|
+
self.results[name] = {'success': success, 'output': output}
|
|
55
78
|
if success:
|
|
56
|
-
progress.update(task_id, description=f
|
|
79
|
+
progress.update(task_id, description=f'[green]✅ {name}', completed=100)
|
|
57
80
|
else:
|
|
58
|
-
progress.update(task_id, description=f
|
|
59
|
-
|
|
60
|
-
return name, success
|
|
81
|
+
progress.update(task_id, description=f'[red]❌ {name} Failed', completed=100)
|
|
82
|
+
return (name, success)
|
|
61
83
|
except Exception as e:
|
|
62
|
-
self.results[name] = {
|
|
63
|
-
progress.update(task_id, description=f
|
|
64
|
-
return name, False
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"Red Team (Fast)": "🚩 Security Architect",
|
|
75
|
-
"Load Test (Baseline)": "🚀 SRE & Performance Principal",
|
|
76
|
-
"Evidence Packing Audit": "📜 Legal & Transparency SME",
|
|
77
|
-
"Face Auditor": "🎭 UX/UI Principal Designer"
|
|
84
|
+
self.results[name] = {'success': False, 'output': str(e)}
|
|
85
|
+
progress.update(task_id, description=f'[red]💥 {name} Error', completed=100)
|
|
86
|
+
return (name, False)
|
|
87
|
+
PERSONA_MAP = {'Architecture Review': '🏛️ Principal Platform Engineer', 'Policy Enforcement': '⚖️ Governance & Compliance SME', 'Secret Scanner': '🔐 SecOps Principal', 'Token Optimization': '💰 FinOps Principal Architect', 'Reliability (Quick)': '🛡️ QA & Reliability Principal', 'Quality Hill Climbing': '🧗 AI Quality SME', 'Red Team Security (Full)': '🚩 Red Team Principal (White-Hat)', 'Red Team (Fast)': '🚩 Security Architect', 'Load Test (Baseline)': '🚀 SRE & Performance Principal', 'Evidence Packing Audit': '📜 Legal & Transparency SME', 'Face Auditor': '🎭 UX/UI Principal Designer'}
|
|
88
|
+
PRIMARY_RISK_MAP = {
|
|
89
|
+
'Secret Scanner': 'Credential Leakage & Unauthorized Access',
|
|
90
|
+
'Architecture Review': 'Systemic Rigidity & Technical Debt',
|
|
91
|
+
'Policy Enforcement': 'Prompt Injection & Reg Breach',
|
|
92
|
+
'Token Optimization': 'FinOps Efficiency & Margin Erosion',
|
|
93
|
+
'Reliability (Quick)': 'Failure Under Stress & Latency spikes',
|
|
94
|
+
'Red Team (Fast)': 'Adversarial Jailbreaking',
|
|
95
|
+
'Face Auditor': 'A2UI Protocol Drift'
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
def generate_report(self):
|
|
81
|
-
title = getattr(self,
|
|
82
|
-
report = [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
"\n## 🧑💼 Principal SME Persona Approvals",
|
|
88
|
-
"Each pillar of your agent has been reviewed by a specialized SME persona."
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
persona_table = Table(title="🏛️ Persona Approval Matrix", show_header=True, header_style="bold blue")
|
|
92
|
-
persona_table.add_column("SME Persona", style="cyan")
|
|
93
|
-
persona_table.add_column("Audit Module", style="magenta")
|
|
94
|
-
persona_table.add_column("Verdict", style="bold")
|
|
95
|
-
|
|
96
|
-
# Collect developer actions and sources
|
|
99
|
+
title = getattr(self, 'title', 'Audit Report')
|
|
100
|
+
report = [f'# 🕹️ AgentOps Cockpit: {title}', f'**Timestamp**: {self.timestamp}', f"**Status**: {('✅ PASS' if all((r['success'] for r in self.results.values())) else '❌ FAIL')}", '\n---', '\n## 🧑\u200d💼 Principal SME Persona Approvals', 'Each pillar of your agent has been reviewed by a specialized SME persona.']
|
|
101
|
+
persona_table = Table(title='🏛️ Persona Approval Matrix', show_header=True, header_style='bold blue')
|
|
102
|
+
persona_table.add_column('SME Persona', style='cyan')
|
|
103
|
+
persona_table.add_column('Audit Module', style='magenta')
|
|
104
|
+
persona_table.add_column('Verdict', style='bold')
|
|
97
105
|
developer_actions = []
|
|
98
106
|
developer_sources = []
|
|
99
107
|
for name, data in self.results.items():
|
|
100
|
-
status =
|
|
101
|
-
persona = self.PERSONA_MAP.get(name,
|
|
108
|
+
status = '✅ APPROVED' if data['success'] else '❌ REJECTED'
|
|
109
|
+
persona = self.PERSONA_MAP.get(name, '👤 Automated Auditor')
|
|
102
110
|
persona_table.add_row(persona, name, status)
|
|
103
|
-
report.append(f
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
report.append(f'- **{persona}** ([{name}]): {status}')
|
|
112
|
+
if data['output']:
|
|
113
|
+
for line in data['output'].split('\n'):
|
|
114
|
+
if 'ACTION:' in line:
|
|
115
|
+
developer_actions.append(line.replace('ACTION:', '').strip())
|
|
116
|
+
if 'SOURCE:' in line:
|
|
117
|
+
developer_sources.append(line.replace('SOURCE:', '').strip())
|
|
118
|
+
|
|
119
|
+
# --- [NEW] Maturity Velocity Logic ---
|
|
120
|
+
lake_path = "evidence_lake.json"
|
|
121
|
+
improvement_delta = 0
|
|
122
|
+
target_abs = os.path.abspath(getattr(self, 'target_path', '.'))
|
|
123
|
+
if os.path.exists(lake_path):
|
|
124
|
+
try:
|
|
125
|
+
with open(lake_path, 'r') as f:
|
|
126
|
+
lake_data = json.load(f)
|
|
127
|
+
historical = lake_data.get(target_abs, {}).get('summary', {})
|
|
128
|
+
prev_health = historical.get('health', 0) * 100
|
|
129
|
+
current_health = (sum(1 for r in self.results.values() if r['success']) / len(self.results) * 100) if self.results else 0
|
|
130
|
+
improvement_delta = current_health - prev_health
|
|
131
|
+
except: pass
|
|
114
132
|
|
|
133
|
+
console.print('\n', persona_table)
|
|
115
134
|
if developer_actions:
|
|
116
|
-
report.append(
|
|
135
|
+
report.append('\n## 🛠️ Developer Action Plan')
|
|
117
136
|
report.append("The following specific fixes are required to achieve a passing 'Well-Architected' score.")
|
|
118
|
-
report.append(
|
|
119
|
-
report.append(
|
|
137
|
+
report.append('| File:Line | Issue | Recommended Fix |')
|
|
138
|
+
report.append('| :--- | :--- | :--- |')
|
|
120
139
|
for action in developer_actions:
|
|
121
|
-
parts = action.split(
|
|
140
|
+
parts = action.split(' | ')
|
|
122
141
|
if len(parts) == 3:
|
|
123
|
-
report.append(f
|
|
124
|
-
|
|
142
|
+
report.append(f'| `{parts[0]}` | {parts[1]} | {parts[2]} |')
|
|
125
143
|
if developer_sources:
|
|
126
|
-
report.append(
|
|
127
|
-
report.append(
|
|
128
|
-
report.append(
|
|
129
|
-
report.append(
|
|
144
|
+
report.append('\n## 📜 Evidence Bridge: Research & Citations')
|
|
145
|
+
report.append('Cross-verified architectural patterns and SDK best-practices mapped to official cloud standards.')
|
|
146
|
+
report.append('| Knowledge Pillar | SDK/Pattern Citation | Evidence & Best Practice |')
|
|
147
|
+
report.append('| :--- | :--- | :--- |')
|
|
130
148
|
for source in developer_sources:
|
|
131
|
-
parts = source.split(
|
|
149
|
+
parts = source.split(' | ')
|
|
132
150
|
if len(parts) == 3:
|
|
133
|
-
report.append(f
|
|
134
|
-
|
|
135
|
-
report.append("\n## 🔍 Raw System Artifacts")
|
|
136
|
-
for name, data in self.results.items():
|
|
137
|
-
report.append(f"\n### {name}")
|
|
138
|
-
report.append("```text")
|
|
139
|
-
report.append(data["output"][-2000:] if data["output"] else "No output.")
|
|
140
|
-
report.append("```")
|
|
141
|
-
|
|
142
|
-
report.append("\n---")
|
|
143
|
-
report.append("\n*Generated by the AgentOps Cockpit Orchestrator (Parallelized Edition).*")
|
|
151
|
+
report.append(f'| {parts[0]} | [Source Citation]({parts[1]}) | {parts[2]} |')
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
# --- [NEW] Executive Risk Scorecard & Thresholds ---
|
|
154
|
+
report.append('\n## 👔 Executive Risk Scorecard')
|
|
147
155
|
|
|
148
|
-
|
|
156
|
+
from agent_ops_cockpit.ops.discovery import DiscoveryEngine
|
|
157
|
+
discovery = DiscoveryEngine(getattr(self, 'target_path', '.'))
|
|
158
|
+
threshold = discovery.config.get("threshold", 0)
|
|
159
|
+
|
|
160
|
+
passed_ok = all(r['success'] for r in self.results.values())
|
|
161
|
+
health_score = (sum(1 for r in self.results.values() if r['success']) / len(self.results) * 100) if self.results else 0
|
|
162
|
+
|
|
163
|
+
executive_summary = "Audit baseline established. No critical blockers detected."
|
|
164
|
+
if health_score < threshold:
|
|
165
|
+
executive_summary = f"**Risk Alert**: Health score ({health_score:.1f}%) is below the configured threshold ({threshold}%). Strategic remediation required."
|
|
166
|
+
elif not passed_ok:
|
|
167
|
+
fail_list = [n for n, r in self.results.items() if not r['success']]
|
|
168
|
+
executive_summary = f"**Risk Alert**: {len(fail_list)} governance gates REJECTED (including {', '.join(fail_list[:2])}). Remediation estimated to take 2-4 hours. Production deployment currently BLOCKED."
|
|
169
|
+
|
|
170
|
+
report.append(executive_summary)
|
|
171
|
+
|
|
172
|
+
# --- [NEW] Strategic Recommendations ---
|
|
173
|
+
debt_analysis = "\n**Strategic Recommendations**:\n"
|
|
174
|
+
if "Missing PII scrubber" in str(developer_actions):
|
|
175
|
+
debt_analysis += "- ⚠️ **Global Debt**: Missing PII Scrubbers detected. Recommendation: Bulk inject `pii_scrubber.py` middleware.\n"
|
|
176
|
+
if "Hardcoded secret" in str(developer_actions):
|
|
177
|
+
debt_analysis += "- ⚠️ **Security Debt**: Hardcoded credentials detected. recommendation: Enforce Google Secret Manager.\n"
|
|
178
|
+
report.append(debt_analysis)
|
|
179
|
+
report.append('\n## 🔍 Raw System Artifacts')
|
|
180
|
+
for name, data in self.results.items():
|
|
181
|
+
report.append(f'\n### {name}')
|
|
182
|
+
report.append('```text')
|
|
183
|
+
report.append(data['output'][-2000:] if data['output'] else 'No output.')
|
|
184
|
+
report.append('```')
|
|
185
|
+
report.append('\n---')
|
|
186
|
+
report.append('\n*Generated by the AgentOps Cockpit Orchestrator (Parallelized Edition).*')
|
|
187
|
+
if improvement_delta != 0:
|
|
188
|
+
velocity_icon = "📈" if improvement_delta > 0 else "📉"
|
|
189
|
+
report.append(f'\n### {velocity_icon} Maturity Velocity: {improvement_delta:+.1f}% Compliance Change')
|
|
190
|
+
|
|
191
|
+
with open(self.report_path, 'w') as f:
|
|
192
|
+
f.write('\n'.join(report))
|
|
149
193
|
self._generate_html_report(developer_actions, developer_sources)
|
|
194
|
+
self._generate_sarif_report(developer_actions)
|
|
195
|
+
self.save_to_evidence_lake(target_abs)
|
|
196
|
+
|
|
197
|
+
console.print(f'\n✨ [bold green]Final Report generated at {self.report_path}[/bold green]')
|
|
198
|
+
console.print(f'📄 [bold blue]Printable HTML Report available at cockpit_report.html[/bold blue]')
|
|
199
|
+
|
|
200
|
+
def save_to_evidence_lake(self, target_abs: str):
|
|
201
|
+
lake_path = 'evidence_lake.json'
|
|
202
|
+
fleet_data = {}
|
|
203
|
+
if os.path.exists(lake_path):
|
|
204
|
+
try:
|
|
205
|
+
with open(lake_path, 'r') as f: fleet_data = json.load(f)
|
|
206
|
+
except: pass
|
|
150
207
|
|
|
151
|
-
|
|
152
|
-
|
|
208
|
+
fleet_data[target_abs] = {
|
|
209
|
+
'timestamp': self.timestamp,
|
|
210
|
+
'hash': self.get_dir_hash(target_abs),
|
|
211
|
+
'results': self.results,
|
|
212
|
+
'summary': {
|
|
213
|
+
'passed': all(r['success'] for r in self.results.values()),
|
|
214
|
+
'health': (sum(1 for r in self.results.values() if r['success']) / len(self.results)) if self.results else 0
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
with open(lake_path, 'w') as f: json.dump(fleet_data, f, indent=2)
|
|
218
|
+
console.print(f'📜 [EVIDENCE LAKE] Centralized log updated for {target_abs}')
|
|
219
|
+
|
|
220
|
+
def _generate_sarif_report(self, developer_actions):
|
|
221
|
+
# Ported basic SARIF logic
|
|
222
|
+
sarif = {"version": "2.1.0", "runs": [{"tool": {"driver": {"name": "AgentOps Cockpit"}}, "results": []}]}
|
|
223
|
+
for action in developer_actions:
|
|
224
|
+
parts = action.split(' | ')
|
|
225
|
+
if len(parts) == 3:
|
|
226
|
+
sarif["runs"][0]["results"].append({"ruleId": parts[1].replace(" ", "_").lower(), "message": {"text": parts[2]}, "locations": [{"physicalLocation": {"artifactLocation": {"uri": parts[0]}}}]})
|
|
227
|
+
with open('cockpit_audit.sarif', 'w') as f: json.dump(sarif, f, indent=2)
|
|
153
228
|
|
|
154
229
|
def _generate_html_report(self, developer_actions, developer_sources):
|
|
155
|
-
"""Generates a
|
|
230
|
+
"""Generates a v1.2 Principal SME Grade HTML report with Professional Mode toggle."""
|
|
156
231
|
html_content = f"""
|
|
157
232
|
<!DOCTYPE html>
|
|
158
233
|
<html lang="en">
|
|
159
234
|
<head>
|
|
160
235
|
<meta charset="UTF-8">
|
|
161
|
-
<title>
|
|
236
|
+
<title>Principal SME Audit: {getattr(self, 'title', 'Build Report')}</title>
|
|
162
237
|
<style>
|
|
163
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono&display=swap');
|
|
164
|
-
body {{ font-family: 'Inter', sans-serif; line-height: 1.6; color: #1e293b; max-width:
|
|
165
|
-
.report-card {{ background: white; padding:
|
|
238
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=JetBrains+Mono&display=swap');
|
|
239
|
+
body {{ font-family: 'Inter', sans-serif; line-height: 1.6; color: #1e293b; max-width: 1100px; margin: 0 auto; padding: 40px; background: #f1f5f9; }}
|
|
240
|
+
.report-card {{ background: white; padding: 50px; border-radius: 32px; box-shadow: 0 25px 50px -12px rgba(0,0,0,0.1); border: 1px solid #e2e8f0; position: relative; }}
|
|
241
|
+
|
|
242
|
+
/* Professional Mode Toggle */
|
|
243
|
+
.mode-toggle {{ position: absolute; top: 20px; right: 20px; display: flex; align-items: center; gap: 8px; font-size: 0.75rem; font-weight: 700; color: #64748b; text-transform: uppercase; }}
|
|
244
|
+
#prof-mode-checkbox {{ cursor: pointer; }}
|
|
245
|
+
body.prof-mode .mascot-container {{ display: none; }}
|
|
246
|
+
body.prof-mode .report-card {{ border-top: 8px solid #1e3a8a; border-radius: 8px; }}
|
|
247
|
+
body.prof-mode h1 {{ font-family: 'Georgia', serif; letter-spacing: 0; }}
|
|
248
|
+
|
|
166
249
|
header {{ display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; border-bottom: 2px solid #f1f5f9; padding-bottom: 30px; }}
|
|
167
|
-
.mascot-container {{ text-align: center; background: #fff; border: 1px solid #e2e8f0; padding:
|
|
168
|
-
.mascot {{ width:
|
|
169
|
-
.mascot-name {{ font-size: 0.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
250
|
+
.mascot-container {{ text-align: center; background: #fff; border: 1px solid #e2e8f0; padding: 12px; border-radius: 20px; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }}
|
|
251
|
+
.mascot {{ width: 100px; height: 100px; border-radius: 12px; object-fit: contain; }}
|
|
252
|
+
.mascot-name {{ font-size: 0.65rem; font-weight: 800; color: #3b82f6; text-transform: uppercase; margin-top: 8px; letter-spacing: 0.1em; }}
|
|
253
|
+
|
|
254
|
+
h1 {{ color: #0f172a; margin: 0; font-size: 2.75rem; letter-spacing: -0.05em; font-weight: 900; }}
|
|
255
|
+
h2 {{ color: #0f172a; margin-top: 50px; font-size: 1.4rem; display: flex; align-items: center; gap: 12px; font-weight: 800; border-left: 5px solid #3b82f6; padding-left: 20px; text-transform: uppercase; letter-spacing: 0.05em; }}
|
|
256
|
+
|
|
257
|
+
.status-badge {{ display: inline-block; padding: 6px 16px; border-radius: 999px; font-weight: 700; text-transform: uppercase; font-size: 0.7rem; margin-top: 10px; }}
|
|
173
258
|
.pass {{ background: #dcfce7; color: #166534; }}
|
|
174
259
|
.fail {{ background: #fee2e2; color: #991b1b; }}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.
|
|
183
|
-
|
|
184
|
-
|
|
260
|
+
.warning {{ background: #fef9c3; color: #854d0e; }}
|
|
261
|
+
|
|
262
|
+
table {{ width: 100%; border-collapse: separate; border-spacing: 0; margin-top: 24px; border: 1px solid #e2e8f0; border-radius: 16px; overflow: hidden; font-size: 0.9rem; }}
|
|
263
|
+
th, td {{ text-align: left; padding: 18px; border-bottom: 1px solid #e2e8f0; }}
|
|
264
|
+
th {{ background: #f8fafc; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.75rem; }}
|
|
265
|
+
|
|
266
|
+
.persona-table th {{ background: #f0f9ff; color: #0369a1; }}
|
|
267
|
+
.risk-text {{ font-size: 0.8rem; color: #64748b; font-style: italic; }}
|
|
268
|
+
|
|
269
|
+
code {{ font-family: 'JetBrains Mono', monospace; background: #f1f5f9; padding: 3px 8px; border-radius: 6px; font-size: 0.85em; color: #ef4444; }}
|
|
270
|
+
pre {{ background: #0f172a; color: #e2e8f0; padding: 24px; border-radius: 20px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; margin-top: 16px; border: 1px solid #1e293b; }}
|
|
271
|
+
|
|
272
|
+
.footer {{ margin-top: 60px; text-align: center; color: #94a3b8; font-size: 0.85rem; border-top: 1px solid #e2e8f0; padding-top: 30px; }}
|
|
185
273
|
</style>
|
|
186
274
|
</head>
|
|
187
275
|
<body>
|
|
188
276
|
<div class="report-card">
|
|
277
|
+
<div class="mode-toggle">
|
|
278
|
+
<label for="prof-mode-checkbox">Professional Mode</label>
|
|
279
|
+
<input type="checkbox" id="prof-mode-checkbox" onchange="document.body.classList.toggle('prof-mode')">
|
|
280
|
+
</div>
|
|
281
|
+
|
|
189
282
|
<header>
|
|
190
283
|
<div>
|
|
191
|
-
<h1
|
|
192
|
-
<p style="color: #64748b; margin:
|
|
193
|
-
<span class="status-badge {'pass' if all(r['success'] for r in self.results.values()) else 'fail'}">
|
|
194
|
-
{'
|
|
284
|
+
<h1>🏛️ SME Executive Review</h1>
|
|
285
|
+
<p style="color: #64748b; margin: 10px 0 0 0; font-weight: 600; font-size: 1.1rem;">Protocol: {getattr(self, 'title', 'Principal Build')}</p>
|
|
286
|
+
<span class="status-badge {('pass' if all((r['success'] for r in self.results.values())) else 'fail')}">
|
|
287
|
+
Consensus: {('APPROVED' if all((r['success'] for r in self.results.values())) else 'REJECTED')}
|
|
195
288
|
</span>
|
|
196
289
|
</div>
|
|
197
290
|
<div class="mascot-container">
|
|
198
|
-
<img src="
|
|
199
|
-
<div class="mascot-name">KOKPI
|
|
291
|
+
<img src="https://raw.githubusercontent.com/GoogleCloudPlatform/agent-starter-pack/main/docs/assets/kokpi.png" class="mascot" alt="Kokpi">
|
|
292
|
+
<div class="mascot-name">KOKPI CERTIFIED</div>
|
|
200
293
|
</div>
|
|
201
294
|
</header>
|
|
202
295
|
|
|
203
|
-
<
|
|
296
|
+
<div style="background: #f8fafc; padding: 25px; border-radius: 16px; margin-bottom: 40px; border: 1px solid #e2e8f0;">
|
|
297
|
+
<h3 style="margin-top:0; font-weight:800; text-transform:uppercase; font-size:0.85rem; color:#64748b;">Board-Level Executive Summary</h3>
|
|
298
|
+
<p style="margin-bottom:0; font-size:1.05rem;">The following audit was performed by a parallelized array of <strong>Principal SME Personas</strong>. This "Safe-Build" standard ensures that the {getattr(self, 'title', 'Agent')} meets the <strong>Google Well-Architected Framework</strong> requirements for security, reliability, and cost-efficiency.</p>
|
|
299
|
+
</div>
|
|
204
300
|
|
|
205
|
-
<h2>🧑💼 SME Persona
|
|
206
|
-
<table>
|
|
301
|
+
<h2>🧑💼 Principal SME Persona Approval Matrix</h2>
|
|
302
|
+
<table class="persona-table">
|
|
207
303
|
<thead>
|
|
208
304
|
<tr>
|
|
209
305
|
<th>SME Persona</th>
|
|
306
|
+
<th>Primary Business Risk</th>
|
|
210
307
|
<th>Module</th>
|
|
211
308
|
<th>Verdict</th>
|
|
212
309
|
</tr>
|
|
213
310
|
</thead>
|
|
214
311
|
<tbody>
|
|
215
312
|
"""
|
|
216
|
-
|
|
217
313
|
for name, data in self.results.items():
|
|
218
|
-
persona = self.PERSONA_MAP.get(name,
|
|
219
|
-
|
|
314
|
+
persona = self.PERSONA_MAP.get(name, 'Automated Auditor')
|
|
315
|
+
risk = self.PRIMARY_RISK_MAP.get(name, 'Architectural Neutrality')
|
|
316
|
+
status = 'APPROVED' if data['success'] else 'REJECTED'
|
|
220
317
|
html_content += f"""
|
|
221
318
|
<tr>
|
|
222
|
-
<td>{persona}</td>
|
|
319
|
+
<td style="font-weight:700; color:#0f172a;">{persona}</td>
|
|
320
|
+
<td class="risk-text">{risk}</td>
|
|
223
321
|
<td>{name}</td>
|
|
224
|
-
<td><span class="status-badge {'pass' if data['success'] else 'fail'}">{status}</span></td>
|
|
322
|
+
<td><span class="status-badge {('pass' if data['success'] else 'fail')}">{status}</span></td>
|
|
225
323
|
</tr>
|
|
226
324
|
"""
|
|
227
|
-
|
|
228
|
-
html_content += """
|
|
229
|
-
</tbody>
|
|
230
|
-
</table>
|
|
231
|
-
"""
|
|
232
|
-
|
|
325
|
+
html_content += '</tbody></table>'
|
|
233
326
|
if developer_actions:
|
|
234
|
-
html_content += ""
|
|
235
|
-
<h2>🛠️ Developer Action Plan</h2>
|
|
236
|
-
<table class="action-table">
|
|
237
|
-
<thead>
|
|
238
|
-
<tr>
|
|
239
|
-
<th>Location (File:Line)</th>
|
|
240
|
-
<th>Issue Detected</th>
|
|
241
|
-
<th>Recommended Implementation</th>
|
|
242
|
-
</tr>
|
|
243
|
-
</thead>
|
|
244
|
-
<tbody>
|
|
245
|
-
"""
|
|
327
|
+
html_content += '\n <h2>🛠️ Developer Action Plan</h2>\n <table class="action-table">\n <thead>\n <tr>\n <th>Location (File:Line)</th>\n <th>Issue Detected</th>\n <th>Recommended Implementation</th>\n </tr>\n </thead>\n <tbody>\n '
|
|
246
328
|
for action in developer_actions:
|
|
247
|
-
parts = action.split(
|
|
329
|
+
parts = action.split(' | ')
|
|
248
330
|
if len(parts) == 3:
|
|
249
|
-
html_content += f""
|
|
250
|
-
|
|
251
|
-
<td><code>{parts[0]}</code></td>
|
|
252
|
-
<td>{parts[1]}</td>
|
|
253
|
-
<td style="color: #059669; font-weight: 600;">{parts[2]}</td>
|
|
254
|
-
</tr>
|
|
255
|
-
"""
|
|
256
|
-
html_content += "</tbody></table>"
|
|
257
|
-
|
|
331
|
+
html_content += f'\n <tr>\n <td><code>{parts[0]}</code></td>\n <td>{parts[1]}</td>\n <td style="color: #059669; font-weight: 600;">{parts[2]}</td>\n </tr>\n '
|
|
332
|
+
html_content += '</tbody></table>'
|
|
258
333
|
if developer_sources:
|
|
259
|
-
html_content += ""
|
|
260
|
-
<h2>📜 Evidence Bridge: Research & Citations</h2>
|
|
261
|
-
<table class="source-table">
|
|
262
|
-
<thead>
|
|
263
|
-
<tr>
|
|
264
|
-
<th>Knowledge Pillar</th>
|
|
265
|
-
<th>SDK/Pattern Citation</th>
|
|
266
|
-
<th>Evidence & Best Practice</th>
|
|
267
|
-
</tr>
|
|
268
|
-
</thead>
|
|
269
|
-
<tbody>
|
|
270
|
-
"""
|
|
334
|
+
html_content += '\n <h2>📜 Evidence Bridge: Research & Citations</h2>\n <table class="source-table">\n <thead>\n <tr>\n <th>Knowledge Pillar</th>\n <th>SDK/Pattern Citation</th>\n <th>Evidence & Best Practice</th>\n </tr>\n </thead>\n <tbody>\n '
|
|
271
335
|
for source in developer_sources:
|
|
272
|
-
parts = source.split(
|
|
336
|
+
parts = source.split(' | ')
|
|
273
337
|
if len(parts) == 3:
|
|
274
|
-
html_content += f"""
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<td><a href="{parts[1]}" class="source-link" target="_blank">View Citation →</a></td>
|
|
278
|
-
<td style="font-size: 0.85rem; color: #475569;">{parts[2]}</td>
|
|
279
|
-
</tr>
|
|
280
|
-
"""
|
|
281
|
-
html_content += "</tbody></table>"
|
|
282
|
-
|
|
283
|
-
html_content += """
|
|
284
|
-
<h2>🔍 Audit Evidence</h2>
|
|
285
|
-
"""
|
|
286
|
-
|
|
338
|
+
html_content += f'\n <tr>\n <td style="font-weight: 700;">{parts[0]}</td>\n <td><a href="{parts[1]}" class="source-link" target="_blank">View Citation →</a></td>\n <td style="font-size: 0.85rem; color: #475569;">{parts[2]}</td>\n </tr>\n '
|
|
339
|
+
html_content += '</tbody></table>'
|
|
340
|
+
html_content += '\n <h2>🔍 Audit Evidence</h2>\n '
|
|
287
341
|
for name, data in self.results.items():
|
|
288
342
|
html_content += f"<h3>{name}</h3><pre>{data['output']}</pre>"
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
<div class="footer">
|
|
292
|
-
Generated by AgentOps Cockpit Orchestrator v0.9.0.
|
|
293
|
-
<br>Ensuring safe-build standards for multi-cloud agentic ecosystems.
|
|
294
|
-
</div>
|
|
295
|
-
</div>
|
|
296
|
-
</body>
|
|
297
|
-
</html>
|
|
298
|
-
"""
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
with open("cockpit_report.html", "w") as f:
|
|
343
|
+
html_content += '\n <div class="footer">\n Generated by AgentOps Cockpit Orchestrator v0.9.0. \n <br>Ensuring safe-build standards for multi-cloud agentic ecosystems.\n </div>\n </div>\n </body>\n </html>\n '
|
|
344
|
+
with open('cockpit_report.html', 'w') as f:
|
|
302
345
|
f.write(html_content)
|
|
303
346
|
|
|
304
|
-
def send_email_report(self, recipient: str, smtp_server: str
|
|
347
|
+
def send_email_report(self, recipient: str, smtp_server: str='smtp.gmail.com', port: int=587):
|
|
305
348
|
"""Sends the markdown report via email."""
|
|
306
349
|
import smtplib
|
|
307
350
|
from email.mime.text import MIMEText
|
|
308
351
|
from email.mime.multipart import MIMEMultipart
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
sender_password = os.environ.get("AGENT_OPS_SME_TOKEN") # Custom auth token for the cockpit
|
|
312
|
-
|
|
352
|
+
sender_email = os.environ.get('AGENT_OPS_SENDER_EMAIL')
|
|
353
|
+
sender_password = os.environ.get('AGENT_OPS_SME_TOKEN')
|
|
313
354
|
if not sender_email or not sender_password:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
355
|
+
console.print('[red]❌ Email failed: AGENT_OPS_SENDER_EMAIL or AGENT_OPS_SME_TOKEN not set.[/red]')
|
|
356
|
+
return False
|
|
317
357
|
try:
|
|
318
358
|
msg = MIMEMultipart()
|
|
319
|
-
msg['From'] = f
|
|
359
|
+
msg['From'] = f'AgentOps Cockpit Audit <{sender_email}>'
|
|
320
360
|
msg['To'] = recipient
|
|
321
361
|
msg['Subject'] = f"🏁 Audit Report: {getattr(self, 'title', 'Agent Result')}"
|
|
322
|
-
|
|
323
|
-
# Use the markdown as content
|
|
324
362
|
with open(self.report_path, 'r') as f:
|
|
325
363
|
content = f.read()
|
|
326
|
-
|
|
327
364
|
msg.attach(MIMEText(content, 'plain'))
|
|
328
|
-
|
|
329
365
|
server = smtplib.SMTP(smtp_server, port)
|
|
330
366
|
server.starttls()
|
|
331
367
|
server.login(sender_email, sender_password)
|
|
332
368
|
server.send_message(msg)
|
|
333
369
|
server.quit()
|
|
334
|
-
|
|
335
|
-
console.print(f"📧 [bold green]Report emailed successfully to {recipient}![/bold green]")
|
|
370
|
+
console.print(f'📧 [bold green]Report emailed successfully to {recipient}![/bold green]')
|
|
336
371
|
return True
|
|
337
372
|
except Exception as e:
|
|
338
|
-
console.print(f
|
|
373
|
+
console.print(f'[red]❌ Email failed: {e}[/red]')
|
|
339
374
|
return False
|
|
340
375
|
|
|
341
|
-
def
|
|
376
|
+
def generate_fleet_dashboard(results: dict):
|
|
377
|
+
"""Generates a premium unified HTML dashboard with deep-link drilldowns."""
|
|
378
|
+
lake_path = "evidence_lake.json"
|
|
379
|
+
fleet_data = {}
|
|
380
|
+
if os.path.exists(lake_path):
|
|
381
|
+
try:
|
|
382
|
+
with open(lake_path, "r") as f: fleet_data = json.load(f)
|
|
383
|
+
except: pass
|
|
384
|
+
|
|
385
|
+
passed_count = sum(1 for r in results.values() if r)
|
|
386
|
+
total = len(results)
|
|
387
|
+
|
|
388
|
+
# ROI Predictor Logic
|
|
389
|
+
total_savings = sum(r.get("savings", 0) for r in fleet_data.values() if isinstance(r, dict) and "savings" in r)
|
|
390
|
+
if total_savings == 0: total_savings = 12.50 * total # Estimated baseline
|
|
391
|
+
|
|
392
|
+
# Extract Global Velocity
|
|
393
|
+
global_velocity = fleet_data.get("global_summary", {}).get("velocity", 0)
|
|
394
|
+
velocity_color = "#059669" if global_velocity >= 0 else "#dc2626"
|
|
395
|
+
velocity_icon = "📈" if global_velocity >= 0 else "📉"
|
|
396
|
+
|
|
397
|
+
html = f"""
|
|
398
|
+
<!DOCTYPE html>
|
|
399
|
+
<html lang="en">
|
|
400
|
+
<head>
|
|
401
|
+
<meta charset="UTF-8">
|
|
402
|
+
<title>AgentOps: Fleet Dashboard</title>
|
|
403
|
+
<style>
|
|
404
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
|
|
405
|
+
body {{ font-family: 'Inter', sans-serif; background: #f8fafc; padding: 40px; color: #1e293b; }}
|
|
406
|
+
.container {{ max-width: 1400px; margin: 0 auto; }}
|
|
407
|
+
.header {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 40px; }}
|
|
408
|
+
.stats {{ display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 40px; }}
|
|
409
|
+
.stat-card {{ background: white; padding: 24px; border-radius: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #e2e8f0; }}
|
|
410
|
+
.stat-value {{ font-size: 2rem; font-weight: 700; color: #3b82f6; }}
|
|
411
|
+
.roi-panel {{ background: #eff6ff; border: 1px solid #bfdbfe; padding: 24px; border-radius: 16px; margin-bottom: 40px; }}
|
|
412
|
+
.agent-grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 20px; }}
|
|
413
|
+
.agent-card {{ background: white; padding: 20px; border-radius: 12px; border: 1px solid #e2e8f0; transition: transform 0.2s; position: relative; overflow: hidden; }}
|
|
414
|
+
.agent-card:hover {{ transform: translateY(-4px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }}
|
|
415
|
+
.status-pass {{ color: #059669; font-weight: 700; }}
|
|
416
|
+
.status-fail {{ color: #dc2626; font-weight: 700; }}
|
|
417
|
+
</style>
|
|
418
|
+
</head>
|
|
419
|
+
<body>
|
|
420
|
+
<div class="container">
|
|
421
|
+
<div class="header">
|
|
422
|
+
<h1>🛸 AgentOps Fleet Flight Deck</h1>
|
|
423
|
+
<div style="text-align: right; color: #64748b;">Enterprise Governance v1.3.0 Antigravity</div>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
426
|
+
<div class="stats">
|
|
427
|
+
<div class="stat-card">
|
|
428
|
+
<div style="color: #64748b; font-size: 0.875rem;">Total Agents Scanned</div>
|
|
429
|
+
<div class="stat-value">{total}</div>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="stat-card">
|
|
432
|
+
<div style="color: #64748b; font-size: 0.875rem;">Production Ready</div>
|
|
433
|
+
<div class="stat-value" style="color: #059669;">{passed_count}</div>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="stat-card">
|
|
436
|
+
<div style="color: #64748b; font-size: 0.875rem;">Fleet Compliance</div>
|
|
437
|
+
<div class="stat-value">{(passed_count/total)*100:.1f}%</div>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="stat-card">
|
|
440
|
+
<div style="color: #64748b; font-size: 0.875rem;">Maturity Velocity</div>
|
|
441
|
+
<div class="stat-value" style="color: {velocity_color};">{velocity_icon} {global_velocity:+.1f}%</div>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<div class="roi-panel">
|
|
446
|
+
<h2 style="margin-top: 0; color: #1e40af; font-size: 1.25rem;">💰 Enterprise ROI Predictor (FinOps)</h2>
|
|
447
|
+
<p style="font-size: 0.875rem; color: #1e3a8a; margin-bottom: 16px;">Strategic opportunities for cost reduction across the fleet. Enabling **Context Caching** and **Semantic Routing** could save an estimated monthly amount based on current scan volume.</p>
|
|
448
|
+
<div style="font-size: 1.5rem; font-weight: 700; color: #1e40af;">Total Fleet Savings Potential: ${total_savings:.2f} / 10k requests</div>
|
|
449
|
+
</div>
|
|
450
|
+
|
|
451
|
+
<h2>📡 Real-time Agent Status</h2>
|
|
452
|
+
<div class="agent-grid">
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
for agent, success in results.items():
|
|
456
|
+
status = "PASSED" if success else "FAILED"
|
|
457
|
+
status_class = "status-pass" if success else "status-fail"
|
|
458
|
+
abs_target = os.path.abspath(agent)
|
|
459
|
+
agent_data = fleet_data.get(abs_target, {})
|
|
460
|
+
delta = agent_data.get("summary", {}).get("improvement_delta", 0)
|
|
461
|
+
|
|
462
|
+
html += f"""
|
|
463
|
+
<div class="agent-card">
|
|
464
|
+
<h3 style="margin-top: 0; font-size: 1rem;">{os.path.basename(agent)}</h3>
|
|
465
|
+
<div class="{(status_class)}">{status}</div>
|
|
466
|
+
<div style="font-size: 0.75rem; color: #94a3b8; margin-top: 10px;">Path: {agent}</div>
|
|
467
|
+
</div>
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
html += """
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
</body>
|
|
474
|
+
</html>
|
|
475
|
+
"""
|
|
476
|
+
with open("fleet_dashboard.html", "w") as f: f.write(html)
|
|
477
|
+
console.print("📄 [bold blue]Unified Fleet Dashboard generated at fleet_dashboard.html[/bold blue]")
|
|
478
|
+
|
|
479
|
+
def run_audit(mode: str='quick', target_path: str='.', title: str='QUICK SAFE-BUILD', apply_fixes: bool=False, sim: bool=False):
|
|
342
480
|
orchestrator = CockpitOrchestrator()
|
|
481
|
+
orchestrator.target_path = target_path
|
|
482
|
+
orchestrator.sim = sim
|
|
343
483
|
target_path = os.path.abspath(target_path)
|
|
484
|
+
|
|
485
|
+
# ⚡ Intelligent Skipping Logic
|
|
486
|
+
lake_path = "evidence_lake.json"
|
|
487
|
+
if os.path.exists(lake_path):
|
|
488
|
+
try:
|
|
489
|
+
with open(lake_path, 'r') as f:
|
|
490
|
+
lake_data = json.load(f)
|
|
491
|
+
current_hash = orchestrator.get_dir_hash(target_path)
|
|
492
|
+
cached_entry = lake_data.get(target_path, {})
|
|
493
|
+
if cached_entry.get('hash') == current_hash and not apply_fixes:
|
|
494
|
+
console.print(f"⚡ [bold green]SKIP:[/bold green] No changes detected in {target_path}. Reusing evidence lake artifacts.")
|
|
495
|
+
orchestrator.results = cached_entry.get('results', {})
|
|
496
|
+
orchestrator.generate_report()
|
|
497
|
+
return True
|
|
498
|
+
except: pass
|
|
499
|
+
|
|
500
|
+
subtitle = 'Essential checks for dev-velocity' if mode == 'quick' else 'Full benchmarks & stress-testing'
|
|
501
|
+
console.print(Panel.fit(f'🕹️ [bold blue]AGENTOPS COCKPIT: {title}[/bold blue]\n{subtitle}...', border_style='blue'))
|
|
344
502
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
console.print(Panel.fit(
|
|
349
|
-
f"🕹️ [bold blue]AGENTOPS COCKPIT: {title}[/bold blue]\n{subtitle}...",
|
|
350
|
-
border_style="blue"
|
|
351
|
-
))
|
|
352
|
-
|
|
353
|
-
with Progress(
|
|
354
|
-
SpinnerColumn(),
|
|
355
|
-
TextColumn("[progress.description]{task.description}"),
|
|
356
|
-
BarColumn(bar_width=None),
|
|
357
|
-
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
358
|
-
console=console,
|
|
359
|
-
expand=True
|
|
360
|
-
) as progress:
|
|
503
|
+
with Progress(SpinnerColumn(), TextColumn('[progress.description]{task.description}'), BarColumn(bar_width=None), TextColumn('[progress.percentage]{task.percentage:>3.0f}%'), console=console, expand=True) as progress:
|
|
504
|
+
base_mod = 'agent_ops_cockpit'
|
|
361
505
|
|
|
362
|
-
#
|
|
363
|
-
|
|
506
|
+
# ⚡ Autodetect Entry Point
|
|
507
|
+
entry_point = orchestrator.detect_entry_point(target_path)
|
|
508
|
+
entry_point_path = os.path.join(target_path, entry_point)
|
|
364
509
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if mode == "quick":
|
|
368
|
-
token_opt_cmd.append("--quick")
|
|
510
|
+
token_opt_args = [entry_point_path, '--no-interactive']
|
|
511
|
+
if mode == 'quick': token_opt_args.append('--quick')
|
|
369
512
|
|
|
370
513
|
steps = [
|
|
371
|
-
(
|
|
372
|
-
(
|
|
373
|
-
(
|
|
374
|
-
(
|
|
375
|
-
(
|
|
376
|
-
(
|
|
514
|
+
('Architecture Review', [sys.executable, '-m', f'{base_mod}.ops.arch_review', 'audit', '--path', target_path]),
|
|
515
|
+
('Policy Enforcement', [sys.executable, '-m', f'{base_mod}.ops.policy_engine']),
|
|
516
|
+
('Secret Scanner', [sys.executable, '-m', f'{base_mod}.ops.secret_scanner', 'scan', target_path]),
|
|
517
|
+
('Token Optimization', [sys.executable, '-m', f'{base_mod}.optimizer', 'audit'] + token_opt_args),
|
|
518
|
+
('Reliability (Quick)', [sys.executable, '-m', f'{base_mod}.ops.reliability', 'audit', '--quick', '--path', target_path]),
|
|
519
|
+
('Face Auditor', [sys.executable, '-m', f'{base_mod}.ops.ui_auditor', 'audit', target_path])
|
|
377
520
|
]
|
|
378
|
-
|
|
379
|
-
# 2. Add "Deep" steps if requested
|
|
380
|
-
if mode == "deep":
|
|
521
|
+
if mode == 'deep':
|
|
381
522
|
steps.extend([
|
|
382
|
-
(
|
|
383
|
-
(
|
|
384
|
-
(
|
|
385
|
-
(
|
|
523
|
+
('Quality Hill Climbing', [sys.executable, '-m', f'{base_mod}.eval.quality_climber', 'climb', '--steps', '10']),
|
|
524
|
+
('Red Team Security (Full)', [sys.executable, '-m', f'{base_mod}.eval.red_team', 'audit', target_path]),
|
|
525
|
+
('Load Test (Baseline)', [sys.executable, '-m', f'{base_mod}.eval.load_test', 'run', '--requests', '50', '--concurrency', '5']),
|
|
526
|
+
('Evidence Packing Audit', [sys.executable, '-m', f'{base_mod}.ops.arch_review', 'audit', '--path', target_path])
|
|
386
527
|
])
|
|
387
528
|
else:
|
|
388
|
-
|
|
389
|
-
steps.append(("Red Team (Fast)", [sys.executable, "-m", f"{base_mod}.eval.red_team", target_path]))
|
|
390
|
-
|
|
391
|
-
# Add tasks to progress bar
|
|
392
|
-
tasks = {name: progress.add_task(f"[white]Waiting: {name}", total=100) for name, cmd in steps}
|
|
529
|
+
steps.append(('Red Team (Fast)', [sys.executable, '-m', f'{base_mod}.eval.red_team', 'audit', target_path]))
|
|
393
530
|
|
|
394
|
-
|
|
531
|
+
tasks = {name: progress.add_task(f'[white]Waiting: {name}', total=100) for name, cmd in steps}
|
|
395
532
|
with ThreadPoolExecutor(max_workers=len(steps)) as executor:
|
|
396
|
-
future_to_audit = {
|
|
397
|
-
executor.submit(orchestrator.run_command, name, cmd, progress, tasks[name]): name
|
|
398
|
-
for name, cmd in steps
|
|
399
|
-
}
|
|
400
|
-
|
|
533
|
+
future_to_audit = {executor.submit(orchestrator.run_command, name, cmd, progress, tasks[name]): name for name, cmd in steps}
|
|
401
534
|
for future in as_completed(future_to_audit):
|
|
402
535
|
name, success = future.result()
|
|
403
|
-
|
|
536
|
+
|
|
404
537
|
orchestrator.title = title
|
|
405
538
|
orchestrator.generate_report()
|
|
539
|
+
|
|
540
|
+
# 🛠️ Auto-Remediation logic
|
|
541
|
+
if apply_fixes:
|
|
542
|
+
from .remediator import CodeRemediator
|
|
543
|
+
from .auditors.base import AuditFinding
|
|
544
|
+
for name, data in orchestrator.results.items():
|
|
545
|
+
if not data['success'] and data['output']:
|
|
546
|
+
for line in data['output'].split('\n'):
|
|
547
|
+
if 'ACTION:' in line:
|
|
548
|
+
parts = line.replace('ACTION:', '').strip().split(' | ')
|
|
549
|
+
if len(parts) == 3:
|
|
550
|
+
remediator = CodeRemediator(parts[0].split(':')[0])
|
|
551
|
+
# (Simplification: applying based on issue type in line)
|
|
552
|
+
if "Resiliency" in parts[1]: remediator.apply_resiliency(AuditFinding(title=parts[1], description=parts[2], line_number=1))
|
|
553
|
+
remediator.save()
|
|
554
|
+
|
|
555
|
+
return all((r['success'] for r in orchestrator.results.values()))
|
|
556
|
+
|
|
557
|
+
def workspace_audit(root_path: str = ".", mode: str = "quick", sim: bool = False):
|
|
558
|
+
"""Fleet Orchestration: Scans workspace for agents and audits in parallel."""
|
|
559
|
+
console.print(Panel(f"🛸 [bold blue]COCKPIT WORKSPACE MODE: FLEET ORCHESTRATION[/bold blue]\nScanning Root: [dim]{root_path}[/dim]", border_style="cyan"))
|
|
560
|
+
from agent_ops_cockpit.ops.discovery import DiscoveryEngine
|
|
561
|
+
discovery = DiscoveryEngine(root_path)
|
|
562
|
+
|
|
563
|
+
agents = []
|
|
564
|
+
# Efficient discovery of agents using the walk engine
|
|
565
|
+
seen_dirs = set()
|
|
566
|
+
for file_path in discovery.walk(root_path):
|
|
567
|
+
dir_name = os.path.dirname(file_path)
|
|
568
|
+
if dir_name in seen_dirs:
|
|
569
|
+
continue
|
|
570
|
+
|
|
571
|
+
file_name = os.path.basename(file_path)
|
|
572
|
+
if file_name in ["agent.py", "main.py", "app.py", "go.mod", "package.json"]:
|
|
573
|
+
agents.append(dir_name)
|
|
574
|
+
seen_dirs.add(dir_name)
|
|
406
575
|
|
|
407
|
-
|
|
408
|
-
|
|
576
|
+
if not agents:
|
|
577
|
+
console.print("[yellow]⚠️ No agents found in workspace.[/yellow]")
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
results = {}
|
|
581
|
+
with ProcessPoolExecutor(max_workers=5) as executor:
|
|
582
|
+
future_map = {executor.submit(run_audit, mode, a, sim=sim): a for a in agents}
|
|
583
|
+
for future in as_completed(future_map):
|
|
584
|
+
agent_path = future_map[future]
|
|
585
|
+
try:
|
|
586
|
+
results[agent_path] = future.result()
|
|
587
|
+
status = "[green]PASS[/green]" if results[agent_path] else "[red]FAIL[/red]"
|
|
588
|
+
console.print(f"📡 [bold]Audit Complete[/bold]: {agent_path} -> {status}")
|
|
589
|
+
except Exception as e: console.print(f"[red]💥 Error auditing {agent_path}: {e}[/red]")
|
|
409
590
|
|
|
410
|
-
|
|
591
|
+
# Update Global Velocity in Evidence Lake
|
|
592
|
+
lake_path = "evidence_lake.json"
|
|
593
|
+
if os.path.exists(lake_path):
|
|
594
|
+
try:
|
|
595
|
+
with open(lake_path, 'r') as f: lake_data = json.load(f)
|
|
596
|
+
prev_compliance = lake_data.get('global_summary', {}).get('compliance', 0)
|
|
597
|
+
current_compliance = (sum(1 for r in results.values() if r) / len(agents)) * 100
|
|
598
|
+
lake_data['global_summary'] = {'compliance': current_compliance, 'velocity': current_compliance - prev_compliance, 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
|
|
599
|
+
with open(lake_path, 'w') as f: json.dump(lake_data, f, indent=2)
|
|
600
|
+
except: pass
|
|
601
|
+
|
|
602
|
+
generate_fleet_dashboard(results)
|
|
603
|
+
if __name__ == '__main__':
|
|
411
604
|
import argparse
|
|
412
605
|
parser = argparse.ArgumentParser()
|
|
413
|
-
parser.add_argument(
|
|
414
|
-
parser.add_argument(
|
|
606
|
+
parser.add_argument('--mode', choices=['quick', 'deep'], default='quick')
|
|
607
|
+
parser.add_argument('--path', default='.')
|
|
608
|
+
parser.add_argument('--workspace', action='store_true')
|
|
609
|
+
parser.add_argument('--apply-fixes', action='store_true')
|
|
610
|
+
parser.add_argument('--sim', action='store_true')
|
|
415
611
|
args = parser.parse_args()
|
|
416
|
-
|
|
417
|
-
|
|
612
|
+
|
|
613
|
+
if args.workspace:
|
|
614
|
+
workspace_audit(root_path=args.path, mode=args.mode, sim=args.sim)
|
|
615
|
+
else:
|
|
616
|
+
success = run_audit(mode=args.mode, target_path=args.path, apply_fixes=args.apply_fixes, sim=args.sim)
|
|
617
|
+
sys.exit(0 if success else 1)
|