Files
CASAN/packages/casan-harness/tests/generate-agentops-dashboard.py
T
thanhnvandClaude Opus 4.8 4bb184b935 feat(harness): implement Plan-20 transparent agentic client bridge
Wave 0 + Wave 1 core of the transparent agentic-client integration: a
developer types prompts normally in Claude Code / Codex while every
certified turn still carries a full H1->H7 trace and an H6 record.

- agentic_bridge.py: stdlib-only lifecycle state machine (begin/pre-tool/
  post-tool/telemetry/finalize/abort + report/doctor). Single-model
  invariant (never calls a model), fail-closed at the side-effect point,
  admission TTL + canonical-project/session binding, atomic state under
  .specify/state/agentic-sessions/, secret redaction, null-not-zero H6.
- agentic-lifecycle.schema.json: client-agnostic JSON contract.
- adapters/claude-code + adapters/codex: thin hook renderers + config
  templates that call the core bridge.
- phase-agentic-bridge-tests.sh: C1-C12 acceptance + threat suite (30/30).
- devkit templates/{claude,codex} + windows/install-agentic.ps1
  (install/doctor/uninstall with manifest, path-safe).
- docs/casan Windows + security/bypass guides; plan status -> IMPLEMENTED.
- harden generate-agentops-dashboard.py aggregation against null H6 costs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 20:44:07 +07:00

210 lines
12 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
from __future__ import annotations
import json
import os
import pathlib
from datetime import datetime, timezone
def _app_root(start):
# Plan-01: `.resolve()` follows the compat symlink into packages/casan-harness;
# runtime state lives at the app's `.specify`, so walk UP for that marker.
d = pathlib.Path(start).resolve()
for p in (d, *d.parents):
if (p / ".specify").is_dir() or (p / "packages" / "casan-harness").is_dir():
return p
return d.parents[2]
ROOT = _app_root(__file__)
METRICS = pathlib.Path(os.environ.get(
"CASAN_TELEMETRY_METRICS_LOG", os.environ.get(
"CASAN_DASHBOARD_METRICS", ROOT / ".specify" / "logs" / "cost" / "metrics.jsonl")))
FALLBACK = ROOT / ".specify" / "logs" / "level5" / "fallback.jsonl"
TOOL = ROOT / ".specify" / "logs" / "level5" / "tool-registry.jsonl"
PROVIDER = pathlib.Path(os.environ.get(
"CASAN_TELEMETRY_PROVIDER_LOG", os.environ.get(
"CASAN_CP_PROVIDER_USAGE", ROOT / ".specify" / "logs" / "level5" / "provider-usage.jsonl")))
PROJECT_REGISTRY = ROOT / ".specify" / "level5" / "project-registry.json"
DASHBOARD = ROOT / "docs" / "output" / "casan" / "central-agentops-dashboard.html"
LEGACY_DASHBOARD = ROOT / "docs" / "output" / "casan" / "agentops-dashboard.html"
def read_jsonl(path: pathlib.Path) -> list[dict]:
if not path.exists():
return []
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
if line.strip():
rows.append(json.loads(line))
return rows
metrics = read_jsonl(METRICS)
fallback = read_jsonl(FALLBACK)
tools = read_jsonl(TOOL)
provider_usage = read_jsonl(PROVIDER)
project_registry = json.loads(PROJECT_REGISTRY.read_text(encoding="utf-8")) if PROJECT_REGISTRY.exists() else {"projects": []}
# Plan-20 §5: agentic (H6-agentic) records deliberately store token/cost as `null`
# when the client gave no reliable source (never coerced to 0 in the record). Sums
# below must therefore treat a MISSING/null number as 0 for aggregation without
# crashing — the null still surfaces as-is in the per-row table.
def _num(v, cast):
try:
return cast(v)
except (TypeError, ValueError):
return cast(0)
total_cost = sum(_num(row.get("cost_estimate", 0), float) for row in metrics)
avg_latency = round(sum(_num(row.get("latency_ms", 0), int) for row in metrics) / max(len(metrics), 1), 2)
failures = sum(1 for row in metrics if row.get("status") == "failed")
fallback_routes = sum(1 for row in fallback if row.get("route") == "fallback")
tool_denies = sum(1 for row in tools if row.get("decision") == "denied")
provider_tokens = sum(_num(row.get("total_tokens", 0), int) for row in provider_usage)
provider_cost = sum(_num(row.get("cost_usd", 0), float) for row in provider_usage)
registered_projects = len(project_registry.get("projects", []))
hallucination_signals = sum(_num(row.get("hallucination_signals", 0), int) for row in metrics)
# --- Harness maturity: rubric assessment (công tâm), khớp evidence/scoring-run-report.md ---
ASSESS_DATE = "2026-07-05"
HARNESS = [
("H1", "Context", 84), ("H2", "Tool", 80), ("H3", "Evaluation", 82),
("H4", "Security", 80), ("H5", "Governance", 80),
("H6", "AgentOps", 80), ("H7", "Orchestration", 80),
]
avg_score = round(sum(s for _, _, s in HARNESS) / len(HARNESS), 1)
lowest_score = min(s for _, _, s in HARNESS)
def _band(s):
if s >= 81: return ("#16a34a", "Strong")
if s >= 61: return ("#0f766e", "Good")
if s >= 31: return ("#d97706", "Partial")
return ("#dc2626", "GAP")
harness_rows = "".join(
f'<div class="hrow"><span class="hlabel">{hid} · {name}</span>'
f'<span class="meter"><i style="width:{s}%;background:{_band(s)[0]}"></i></span>'
f'<span class="hscore" style="color:{_band(s)[0]}">{s}<small>/100 · {_band(s)[1]}</small></span></div>'
for hid, name, s in HARNESS
)
DASHBOARD.parent.mkdir(parents=True, exist_ok=True)
DASHBOARD.write_text(
f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>CASAN Level 4 · AgentOps Dashboard</title>
<style>
:root {{ color-scheme: light; }}
* {{ box-sizing: border-box; }}
body {{ font-family: -apple-system, "Segoe UI", Arial, sans-serif; margin: 0; padding: 30px 34px 60px; background: #eef1f6; color: #16233a; max-width: 1180px; }}
h1 {{ font-size: 27px; color: #1f3b6e; margin: 0 0 4px; letter-spacing: -.015em; }}
.subtitle {{ color: #5a6b80; font-size: 13px; margin: 0 0 16px; }}
h2 {{ color: #1f3b6e; font-size: 16px; margin: 26px 0 12px; letter-spacing: -.01em; }}
.badges {{ display: flex; flex-wrap: wrap; gap: 9px; margin: 0 0 8px; }}
.badge {{ background: #fff; border: 1px solid #d8dee9; border-radius: 999px; padding: 6px 13px; font-size: 12.5px; font-weight: 700; color: #1f3b6e; }}
.badge.lv {{ background: #eaf6ef; border-color: #bfe0cd; color: #16794f; }}
.panel {{ background: #fff; border: 1px solid #dbe2ec; border-radius: 13px; padding: 18px 20px; box-shadow: 0 1px 2px rgba(16,35,58,.05); }}
.grid {{ display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; }}
.card {{ background: #fff; border: 1px solid #dbe2ec; border-radius: 11px; padding: 15px 16px; box-shadow: 0 1px 2px rgba(16,35,58,.04); }}
.card .k {{ color: #5a6b80; font-size: 11.5px; font-weight: 600; letter-spacing: .01em; }}
.value {{ font-size: 26px; font-weight: 800; color: #0f766e; margin-top: 5px; }}
.hrow {{ display: grid; grid-template-columns: 165px 1fr 128px; align-items: center; gap: 14px; padding: 6px 0; }}
.hlabel {{ font-size: 13.5px; font-weight: 600; color: #28405c; }}
.meter {{ background: #e6ebf2; border-radius: 6px; height: 13px; overflow: hidden; }}
.meter > i {{ display: block; height: 100%; border-radius: 6px; }}
.hscore {{ font-size: 15px; font-weight: 800; text-align: right; white-space: nowrap; }}
.hscore small {{ font-size: 9.5px; color: #8a94a0; font-weight: 600; }}
.hsum {{ margin-top: 13px; padding-top: 12px; border-top: 1px solid #eef1f6; font-size: 13px; color: #41566f; }}
.hsum b {{ color: #1f3b6e; }}
.bandlg {{ display: flex; flex-wrap: wrap; gap: 14px; margin-top: 9px; font-size: 11.5px; color: #6b7888; }}
.bandlg i {{ width: 11px; height: 11px; border-radius: 3px; display: inline-block; margin-right: 5px; vertical-align: -1px; }}
.chips {{ display: flex; flex-wrap: wrap; gap: 9px; }}
.chip {{ display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: #28405c; background: #f5f8fc; border: 1px solid #dbe4ef; border-radius: 999px; padding: 8px 14px; }}
.chip .d {{ width: 8px; height: 8px; border-radius: 99px; background: #16a34a; flex: none; }}
.chip b {{ color: #0f766e; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 4px; background: #fff; }}
td, th {{ border: 1px solid #e2e8f1; padding: 8px 10px; text-align: left; font-size: 12.5px; color: #16233a; }}
th {{ background: #eef2f8; color: #1f3b6e; font-weight: 700; }}
tr:nth-child(even) td {{ background: #f7f9fc; }}
.ok {{ color: #16a34a; font-weight: 700; }}
</style>
</head>
<body>
<h1>CASAN Level 4 Central AgentOps Dashboard</h1>
<p class="subtitle">7-harness security posture · Level-5 controls demonstrated locally · điểm công tâm theo rubric (evidence/scoring-run-report.md) · Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}</p>
<div class="badges">
<span class="badge lv">CASAN Level 4 — chứng minh bằng tấn công</span>
<span class="badge">Average {avg_score}/100</span>
<span class="badge">Harness thấp nhất {lowest_score}</span>
<span class="badge">218 core tests · 0 fail</span>
<span class="badge">Recall model 0.85 &gt; regex 0.00</span>
</div>
<h2>Đánh giá trưởng thành 7 Harness · rubric công tâm ({ASSESS_DATE})</h2>
<div class="panel">
{harness_rows}
<div class="hsum">Average <b>{avg_score}/100</b> · Harness thấp nhất <b>{lowest_score}</b> → <b>CASAN Level 4</b> (chưa lên "Strong/production" — bản production của IdP/WORM-store/HSM/sandbox-isolation còn planned).</div>
<div class="bandlg">
<span><i style="background:#16a34a"></i>Strong 81–100 (production)</span>
<span><i style="background:#0f766e"></i>Good 61–80</span>
<span><i style="background:#d97706"></i>Partial 31–60</span>
<span><i style="background:#dc2626"></i>GAP 0–30</span>
</div>
</div>
<h2>Bảo mật &amp; Governance đã kiểm chứng (test đối kháng thật)</h2>
<div class="panel"><div class="chips">
<span class="chip"><span class="d"></span>Kiểm thử đối kháng <b>218 / 0 fail</b></span>
<span class="chip"><span class="d"></span>H4 recall model <b>0.85</b> &gt; regex 0.00</span>
<span class="chip"><span class="d"></span>Benign FP <b>0.00%</b> · block <b>100.00%</b></span>
<span class="chip"><span class="d"></span>Audit hash-chain + ký KMS (rotate/non-exportable)</span>
<span class="chip"><span class="d"></span>WORM audit ngoài (gap/tamper detected)</span>
<span class="chip"><span class="d"></span>Approval ký-danh-tính (chống giả/replay/tự-duyệt)</span>
<span class="chip"><span class="d"></span>Cost-spike 4 chế độ · drift · hallucination scan</span>
<span class="chip"><span class="d"></span>Alert live: webhook · dead-letter</span>
<span class="chip"><span class="d"></span>Unicode/base64 normalize · tool-output scan</span>
<span class="chip"><span class="d"></span>action / supply-chain / data-exfil gate</span>
</div></div>
<h2>Telemetry trực tiếp (live) · pipeline harness</h2>
<div class="grid">
<div class="card"><div class="k">Total Runs</div><div class="value">{len(metrics)}</div></div>
<div class="card"><div class="k">Average Latency</div><div class="value">{avg_latency}<small style="font-size:14px"> ms</small></div></div>
<div class="card"><div class="k">Estimated Cost</div><div class="value">${total_cost:.6f}</div></div>
<div class="card"><div class="k">Failures</div><div class="value">{failures}</div></div>
<div class="card"><div class="k">Fallback Routes</div><div class="value">{fallback_routes}</div></div>
</div>
<div class="grid" style="margin-top:12px">
<div class="card"><div class="k">Provider Runs</div><div class="value">{len(provider_usage)}</div></div>
<div class="card"><div class="k">Provider Tokens (thật)</div><div class="value">{provider_tokens}</div></div>
<div class="card"><div class="k">Provider Cost</div><div class="value">${provider_cost:.5f}</div></div>
<div class="card"><div class="k">Tool Denials</div><div class="value">{tool_denies}</div></div>
<div class="card"><div class="k">Hallucination Signals</div><div class="value">{hallucination_signals}</div></div>
</div>
<h2>Governance Signals</h2>
<table>
<tr><th>Signal</th><th>Value</th></tr>
<tr><td>Tool registry denials</td><td>{tool_denies}</td></tr>
<tr><td>Fallback records</td><td>{len(fallback)}</td></tr>
<tr><td>Tool registry records</td><td>{len(tools)}</td></tr>
<tr><td>Provider telemetry records</td><td>{len(provider_usage)}</td></tr>
<tr><td>Registered harness projects</td><td>{registered_projects}</td></tr>
</table>
<h2>Recent AgentOps Metrics (live)</h2>
<table>
<tr><th>Trace</th><th>Agent</th><th>Step</th><th>Status</th><th>Latency</th><th>Tokens</th><th>Cost</th></tr>
{''.join(f"<tr><td>{str(m.get('trace_id'))[:8]}…</td><td>{m.get('agent')}</td><td>{m.get('step')}</td><td class='{'ok' if m.get('status')=='success' else ''}'>{m.get('status')}</td><td>{m.get('latency_ms')}ms</td><td>{m.get('total_tokens')}</td><td>${m.get('cost_estimate')}</td></tr>" for m in metrics[-10:])}
</table>
</body>
</html>
""",
encoding="utf-8",
)
LEGACY_DASHBOARD.write_text(DASHBOARD.read_text(encoding="utf-8"), encoding="utf-8")
print(f"DASHBOARD_GENERATED {DASHBOARD}")