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.
Files changed (60) hide show
  1. agent_ops_cockpit/agent.py +43 -81
  2. agent_ops_cockpit/cache/semantic_cache.py +10 -21
  3. agent_ops_cockpit/cli/main.py +105 -153
  4. agent_ops_cockpit/eval/load_test.py +33 -50
  5. agent_ops_cockpit/eval/quality_climber.py +88 -93
  6. agent_ops_cockpit/eval/red_team.py +54 -21
  7. agent_ops_cockpit/mcp_server.py +26 -93
  8. agent_ops_cockpit/ops/arch_review.py +221 -148
  9. agent_ops_cockpit/ops/auditors/base.py +50 -0
  10. agent_ops_cockpit/ops/auditors/behavioral.py +31 -0
  11. agent_ops_cockpit/ops/auditors/compliance.py +35 -0
  12. agent_ops_cockpit/ops/auditors/dependency.py +48 -0
  13. agent_ops_cockpit/ops/auditors/finops.py +48 -0
  14. agent_ops_cockpit/ops/auditors/graph.py +49 -0
  15. agent_ops_cockpit/ops/auditors/pivot.py +51 -0
  16. agent_ops_cockpit/ops/auditors/reasoning.py +67 -0
  17. agent_ops_cockpit/ops/auditors/reliability.py +53 -0
  18. agent_ops_cockpit/ops/auditors/security.py +87 -0
  19. agent_ops_cockpit/ops/auditors/sme_v12.py +76 -0
  20. agent_ops_cockpit/ops/auditors/sovereignty.py +74 -0
  21. agent_ops_cockpit/ops/auditors/sre_a2a.py +179 -0
  22. agent_ops_cockpit/ops/benchmarker.py +97 -0
  23. agent_ops_cockpit/ops/cost_optimizer.py +15 -24
  24. agent_ops_cockpit/ops/discovery.py +214 -0
  25. agent_ops_cockpit/ops/evidence_bridge.py +30 -63
  26. agent_ops_cockpit/ops/frameworks.py +124 -1
  27. agent_ops_cockpit/ops/git_portal.py +74 -0
  28. agent_ops_cockpit/ops/mcp_hub.py +19 -42
  29. agent_ops_cockpit/ops/orchestrator.py +477 -277
  30. agent_ops_cockpit/ops/policy_engine.py +38 -38
  31. agent_ops_cockpit/ops/reliability.py +120 -65
  32. agent_ops_cockpit/ops/remediator.py +54 -0
  33. agent_ops_cockpit/ops/secret_scanner.py +34 -22
  34. agent_ops_cockpit/ops/swarm.py +17 -27
  35. agent_ops_cockpit/ops/ui_auditor.py +67 -6
  36. agent_ops_cockpit/ops/watcher.py +41 -70
  37. agent_ops_cockpit/ops/watchlist.json +30 -0
  38. agent_ops_cockpit/optimizer.py +157 -407
  39. agent_ops_cockpit/tests/test_arch_review.py +6 -6
  40. agent_ops_cockpit/tests/test_discovery.py +96 -0
  41. agent_ops_cockpit/tests/test_ops_core.py +56 -0
  42. agent_ops_cockpit/tests/test_orchestrator_fleet.py +73 -0
  43. agent_ops_cockpit/tests/test_persona_architect.py +75 -0
  44. agent_ops_cockpit/tests/test_persona_finops.py +31 -0
  45. agent_ops_cockpit/tests/test_persona_security.py +55 -0
  46. agent_ops_cockpit/tests/test_persona_sre.py +43 -0
  47. agent_ops_cockpit/tests/test_persona_ux.py +42 -0
  48. agent_ops_cockpit/tests/test_quality_climber.py +2 -2
  49. agent_ops_cockpit/tests/test_remediator.py +75 -0
  50. agent_ops_cockpit/tests/test_ui_auditor.py +52 -0
  51. agentops_cockpit-0.9.8.dist-info/METADATA +172 -0
  52. agentops_cockpit-0.9.8.dist-info/RECORD +71 -0
  53. agent_ops_cockpit/tests/test_optimizer.py +0 -68
  54. agent_ops_cockpit/tests/test_red_team.py +0 -35
  55. agent_ops_cockpit/tests/test_secret_scanner.py +0 -24
  56. agentops_cockpit-0.9.7.dist-info/METADATA +0 -246
  57. agentops_cockpit-0.9.7.dist-info/RECORD +0 -47
  58. {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/WHEEL +0 -0
  59. {agentops_cockpit-0.9.7.dist-info → agentops_cockpit-0.9.8.dist-info}/entry_points.txt +0 -0
  60. {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("%Y-%m-%d %H:%M:%S")
21
- self.report_path = "cockpit_final_report.md"
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"[cyan]Running {name}...")
29
-
30
- # Use absolute path for the cockpit source code
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["PYTHONPATH"] = f"{cockpit_root}{os.pathsep}{env.get('PYTHONPATH', '')}"
34
-
71
+ env['PYTHONPATH'] = f"{cockpit_src}{os.pathsep}{cockpit_root}{os.pathsep}{env.get('PYTHONPATH', '')}"
35
72
  try:
36
- # We use Popen to potentially stream output if we wanted,
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
- output = stdout if success else f"{stdout}\n{stderr}"
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"[green]✅ {name}", completed=100)
79
+ progress.update(task_id, description=f'[green]✅ {name}', completed=100)
57
80
  else:
58
- progress.update(task_id, description=f"[red]❌ {name} Failed", completed=100)
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] = {"success": False, "output": str(e)}
63
- progress.update(task_id, description=f"[red]💥 {name} Error", completed=100)
64
- return name, False
65
-
66
- PERSONA_MAP = {
67
- "Architecture Review": "🏛️ Principal Platform Engineer",
68
- "Policy Enforcement": "⚖️ Governance & Compliance SME",
69
- "Secret Scanner": "🔐 SecOps Principal",
70
- "Token Optimization": "💰 FinOps Principal Architect",
71
- "Reliability (Quick)": "🛡️ QA & Reliability Principal",
72
- "Quality Hill Climbing": "🧗 AI Quality SME",
73
- "Red Team Security (Full)": "🚩 Red Team Principal (White-Hat)",
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, "title", "Audit Report")
82
- report = [
83
- f"# 🕹️ AgentOps Cockpit: {title}",
84
- f"**Timestamp**: {self.timestamp}",
85
- f"**Status**: {' PASS' if all(r['success'] for r in self.results.values()) else '❌ FAIL'}",
86
- "\n---",
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 = "✅ APPROVED" if data["success"] else "❌ REJECTED"
101
- persona = self.PERSONA_MAP.get(name, "👤 Automated Auditor")
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"- **{persona}** ([{name}]): {status}")
104
-
105
- # Parse outputs
106
- if data["output"]:
107
- for line in data["output"].split("\n"):
108
- if "ACTION:" in line:
109
- developer_actions.append(line.replace("ACTION:", "").strip())
110
- if "SOURCE:" in line:
111
- developer_sources.append(line.replace("SOURCE:", "").strip())
112
-
113
- console.print("\n", persona_table)
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("\n## 🛠️ Developer Action Plan")
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("| File:Line | Issue | Recommended Fix |")
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"| `{parts[0]}` | {parts[1]} | {parts[2]} |")
124
-
142
+ report.append(f'| `{parts[0]}` | {parts[1]} | {parts[2]} |')
125
143
  if developer_sources:
126
- report.append("\n## 📜 Evidence Bridge: Research & Citations")
127
- report.append("Cross-verified architectural patterns and SDK best-practices mapped to official cloud standards.")
128
- report.append("| Knowledge Pillar | SDK/Pattern Citation | Evidence & Best Practice |")
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"| {parts[0]} | [Source Citation]({parts[1]}) | {parts[2]} |")
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
- with open(self.report_path, "w") as f:
146
- f.write("\n".join(report))
153
+ # --- [NEW] Executive Risk Scorecard & Thresholds ---
154
+ report.append('\n## 👔 Executive Risk Scorecard')
147
155
 
148
- # Also generate a professional HTML report
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
- console.print(f"\n✨ [bold green]Final Report generated at {self.report_path}[/bold green]")
152
- console.print(f"📄 [bold blue]Printable HTML Report available at cockpit_report.html[/bold blue]")
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 premium HTML version of the report with Kokpi mascot."""
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>AgentOps Audit: {getattr(self, 'title', 'Report')}</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: 1000px; margin: 0 auto; padding: 60px 40px; background: #f1f5f9; }}
165
- .report-card {{ background: white; padding: 40px; border-radius: 24px; box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04); border: 1px solid #e2e8f0; }}
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: 10px; border-radius: 16px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }}
168
- .mascot {{ width: 80px; height: 80px; border-radius: 12px; object-fit: contain; }}
169
- .mascot-name {{ font-size: 0.6rem; font-weight: 800; color: #3b82f6; text-transform: uppercase; margin-top: 5px; letter-spacing: 0.1em; }}
170
- h1 {{ color: #0f172a; margin: 0; font-size: 2.25rem; letter-spacing: -0.025em; font-weight: 900; }}
171
- h2 {{ color: #0f172a; margin-top: 48px; font-size: 1.25rem; display: flex; align-items: center; gap: 12px; font-weight: 800; border-left: 4px solid #3b82f6; padding-left: 15px; text-transform: uppercase; letter-spacing: 0.05em; }}
172
- .status-badge {{ display: inline-block; padding: 6px 16px; border-radius: 999px; font-weight: 700; text-transform: uppercase; font-size: 0.75rem; margin-top: 10px; }}
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
- table {{ width: 100%; border-collapse: separate; border-spacing: 0; margin-top: 24px; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; }}
176
- th, td {{ text-align: left; padding: 16px; border-bottom: 1px solid #e2e8f0; }}
177
- th {{ background: #f8fafc; font-weight: 700; color: #64748b; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }}
178
- .action-table th {{ background: #eff6ff; color: #1e40af; }}
179
- .source-table th {{ background: #f0f9ff; color: #0369a1; }}
180
- code {{ font-family: 'JetBrains Mono', monospace; background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-size: 0.9em; }}
181
- pre {{ background: #0f172a; color: #e2e8f0; padding: 24px; border-radius: 16px; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; margin-top: 16px; border: 1px solid #1e293b; }}
182
- .source-link {{ color: #3b82f6; text-decoration: none; font-weight: 600; border-bottom: 1px solid transparent; }}
183
- .source-link:hover {{ border-bottom-color: #3b82f6; }}
184
- .footer {{ margin-top: 40px; text-align: center; color: #94a3b8; font-size: 0.8rem; border-top: 1px solid #e2e8f0; padding-top: 20px; }}
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>🕹️ AgentOps Cockpit</h1>
192
- <p style="color: #64748b; margin: 8px 0 0 0; font-weight: 500;">Report: {getattr(self, 'title', 'Build Audit')}</p>
193
- <span class="status-badge {'pass' if all(r['success'] for r in self.results.values()) else 'fail'}">
194
- {'PASSED' if all(r['success'] for r in self.results.values()) else 'FAILED'}
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="public/kokpi_branded.jpg" class="mascot" alt="Kokpi">
199
- <div class="mascot-name">KOKPI APPROVED</div>
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
- <p style="font-size: 0.875rem; color: #64748b;"><strong>Timestamp</strong>: {self.timestamp}</p>
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 Approvals</h2>
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, "Automated Auditor")
219
- status = "APPROVED" if data["success"] else "REJECTED"
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
- <tr>
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
- <tr>
276
- <td style="font-weight: 700;">{parts[0]}</td>
277
- <td><a href="{parts[1]}" class="source-link" target="_blank">View Citation &rarr;</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 &rarr;</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
- html_content += """
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 = "smtp.gmail.com", port: int = 587):
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
- sender_email = os.environ.get("AGENT_OPS_SENDER_EMAIL")
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
- console.print("[red]❌ Email failed: AGENT_OPS_SENDER_EMAIL or AGENT_OPS_SME_TOKEN not set.[/red]")
315
- return False
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"AgentOps Cockpit Audit <{sender_email}>"
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"[red]❌ Email failed: {e}[/red]")
373
+ console.print(f'[red]❌ Email failed: {e}[/red]')
339
374
  return False
340
375
 
341
- def run_audit(mode: str = "quick", target_path: str = "."):
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
- title = "QUICK SAFE-BUILD" if mode == "quick" else "DEEP SYSTEM AUDIT"
346
- subtitle = "Essential checks for dev-velocity" if mode == "quick" else "Full benchmarks & stress-testing"
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
- # Use the consolidated package
363
- base_mod = "agent_ops_cockpit"
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
- # 1. Essential "Safe-Build" Steps (Fast)
366
- token_opt_cmd = [sys.executable, "-m", f"{base_mod}.optimizer", target_path, "--no-interactive"]
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
- ("Architecture Review", [sys.executable, "-m", f"{base_mod}.ops.arch_review", "--path", target_path]),
372
- ("Policy Enforcement", [sys.executable, "-m", f"{base_mod}.ops.policy_engine"]), # Policy is global to cockpit
373
- ("Secret Scanner", [sys.executable, "-m", f"{base_mod}.ops.secret_scanner", target_path]),
374
- ("Token Optimization", token_opt_cmd),
375
- ("Reliability (Quick)", [sys.executable, "-m", f"{base_mod}.ops.reliability", "--quick", "--path", target_path]),
376
- ("Face Auditor", [sys.executable, "-m", f"{base_mod}.ops.ui_auditor", "--path", target_path])
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
- ("Quality Hill Climbing", [sys.executable, "-m", f"{base_mod}.eval.quality_climber", "--steps", "10"]),
383
- ("Red Team Security (Full)", [sys.executable, "-m", f"{base_mod}.eval.red_team", target_path]),
384
- ("Load Test (Baseline)", [sys.executable, "-m", f"{base_mod}.eval.load_test", "--requests", "50", "--concurrency", "5"]),
385
- ("Evidence Packing Audit", [sys.executable, "-m", f"{base_mod}.ops.arch_review", "--path", target_path])
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
- # Quick mode still needs a fast security check
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
- # Execute in parallel
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
- # Return True if all steps passed
408
- return all(r["success"] for r in orchestrator.results.values())
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
- if __name__ == "__main__":
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("--mode", choices=["quick", "deep"], default="quick")
414
- parser.add_argument("--path", default=".")
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
- success = run_audit(mode=args.mode, target_path=args.path)
417
- sys.exit(0 if success else 1)
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)