feat: ship production local assurance viewer in core

This commit is contained in:
thanhnv
2026-07-28 23:09:22 +07:00
parent 3cc897c5bb
commit 9c9efb1043
21 changed files with 2544 additions and 136 deletions
+15
View File
@@ -5,6 +5,7 @@ Populated incrementally by Plan-01 (Phase 0→6). During migration, files move h
`.specify/` wave-by-wave; the full harness gate must stay green (`PASS=64 FAIL=0`) after each phase.
Layout:
- `assets/local-viewer/` — production static UI for the Core Local Assurance Viewer
- `scripts/` — bash + powershell gate logic (H1→H7), path resolver `casan-paths.sh`
- `security/` — filter/policy rules (prompt-filter, pii-rules, output-policy, ...)
- `governance/`, `agentops/` — H5/H6 code
@@ -12,6 +13,20 @@ Layout:
- `templates/` — spec/plan templates
- `tests/` — source-hub verification only; excluded from production installs
Core visual reporting is intentionally lazy and zero-dependency:
```bash
casan report latest
casan view
casan report export --format html
casan report export --h6 --format html
```
Prompt hooks only write canonical evidence and a small receipt. `casan view`
starts a loopback-only, token-protected, read-only Python server on demand.
No Node/npm, external font/CDN, Platform service or per-prompt HTML generation
is required.
Runtime state (logs, audit chain, tenant state) is NOT part of this package — it stays with
the app under `CASAN_STATE_ROOT`. Domain data (golden-runs, corpus, input) lives in
`apps/okr/domain/` under `CASAN_DOMAIN_ROOT`.
@@ -0,0 +1,271 @@
:root {
--ink: #101828;
--muted: #667085;
--faint: #98a2b3;
--line: #e4e7ec;
--paper: #f6f7f9;
--white: #fff;
--navy: #101828;
--navy-2: #1d2939;
--cyan: #0891b2;
--cyan-soft: #ecfeff;
--green: #067647;
--green-soft: #ecfdf3;
--amber: #b54708;
--amber-soft: #fffaeb;
--rose: #b42318;
--rose-soft: #fef3f2;
--shadow: 0 12px 36px rgba(16, 24, 40, 0.06);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-webkit-font-smoothing: antialiased;
}
button, input, select { font: inherit; }
button, a { -webkit-tap-highlight-color: transparent; }
button:focus-visible, a:focus-visible, input:focus-visible, select:focus-visible, summary:focus-visible {
outline: 3px solid rgba(6, 182, 212, 0.28);
outline-offset: 2px;
}
.hidden { display: none !important; }
.skip-link {
position: fixed; z-index: 100; left: 16px; top: -60px; padding: 10px 14px;
border-radius: 10px; background: white; color: var(--ink); font-weight: 700;
}
.skip-link:focus { top: 16px; }
.sidebar {
position: fixed; inset: 0 auto 0 0; z-index: 30; width: 244px; display: flex;
flex-direction: column; border-right: 1px solid #253044; background: var(--navy);
color: white; padding: 22px 16px;
}
.brand { display: flex; align-items: center; gap: 12px; color: white; text-decoration: none; padding: 4px 8px 24px; }
.brand-mark {
display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid #344054;
border-radius: 12px; background: #172033; color: #67e8f9; font: 800 16px ui-monospace, monospace;
}
.brand strong, .brand small { display: block; }
.brand strong { font-size: 14px; letter-spacing: .08em; }
.brand small { margin-top: 3px; color: #98a2b3; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; }
.sidebar nav { display: grid; gap: 5px; }
.nav-item {
display: grid; grid-template-columns: 30px 1fr; align-items: center; min-height: 46px; width: 100%;
border: 1px solid transparent; border-radius: 12px; background: transparent; color: #98a2b3;
padding: 8px 12px; text-align: left; font-size: 13px; font-weight: 650; cursor: pointer; transition: .18s ease;
}
.nav-item span { color: #667085; font: 700 11px ui-monospace, monospace; }
.nav-item:hover { background: #172033; color: white; }
.nav-item.active { border-color: #344054; background: #1d2939; color: white; }
.nav-item.active span { color: #67e8f9; }
.local-seal {
display: grid; grid-template-columns: 8px 1fr; gap: 10px; align-items: start; margin-top: auto;
border: 1px solid #344054; border-radius: 14px; background: #172033; padding: 13px;
}
.local-seal strong, .local-seal small { display: block; }
.local-seal strong { font-size: 11px; }
.local-seal small { margin-top: 4px; color: #667085; font: 10px ui-monospace, monospace; }
.live-dot { width: 7px; height: 7px; margin-top: 3px; border-radius: 50%; background: #32d583; box-shadow: 0 0 0 4px rgba(50,213,131,.12); }
.shell { margin-left: 244px; min-height: 100vh; }
.topbar {
position: sticky; top: 0; z-index: 20; display: flex; align-items: center; min-height: 76px;
border-bottom: 1px solid var(--line); background: rgba(255,255,255,.92); padding: 12px clamp(20px,4vw,48px);
backdrop-filter: blur(16px);
}
.topbar h1 { margin: 2px 0 0; font-size: 18px; letter-spacing: -.02em; }
.top-eyebrow { margin: 0; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .16em; text-transform: uppercase; }
.top-actions { display: flex; align-items: center; gap: 9px; margin-left: auto; }
.icon-button {
display: grid; place-items: center; min-width: 44px; min-height: 44px; border: 1px solid var(--line);
border-radius: 12px; background: white; color: #475467; cursor: pointer; transition: .18s ease;
}
.icon-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.menu-button { display: none; margin-right: 12px; }
main { max-width: 1500px; margin: 0 auto; padding: 28px clamp(20px,4vw,48px) 64px; }
.view { display: grid; gap: 18px; }
.hero {
position: relative; overflow: hidden; border-radius: 28px; background: var(--navy); color: white;
padding: clamp(28px,5vw,48px); box-shadow: 0 24px 70px rgba(16,24,40,.16);
}
.hero::after {
content: ""; position: absolute; width: 360px; height: 360px; right: -160px; top: -230px;
border: 1px solid rgba(103,232,249,.15); border-radius: 50%; box-shadow: 0 0 0 60px rgba(103,232,249,.025), 0 0 0 120px rgba(103,232,249,.02);
}
.hero-grid { position: relative; z-index: 1; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 28px; align-items: end; }
.eyebrow { margin: 0; color: var(--cyan); font-size: 10px; font-weight: 850; letter-spacing: .18em; text-transform: uppercase; }
.hero .eyebrow { color: #67e8f9; }
.hero h2 { max-width: 760px; margin: 13px 0 12px; font-size: clamp(30px,4vw,48px); line-height: 1.02; letter-spacing: -.045em; }
.hero-description { max-width: 760px; margin: 0; color: #98a2b3; font-size: 14px; line-height: 1.7; }
.hero-meta { display: flex; flex-wrap: wrap; gap: 8px 18px; margin-top: 24px; color: #667085; font: 10px ui-monospace, monospace; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: flex-end; }
.trace-hero { border-radius: 18px; padding: 20px 22px; box-shadow: none; }
.trace-title { margin: 8px 0 !important; font-size: 18px !important; }
.trace-meta { margin-top: 8px; }
.rail-spaced { margin-top: 20px; }
.button {
display: inline-flex; align-items: center; justify-content: center; min-height: 44px; border: 1px solid transparent;
border-radius: 12px; padding: 9px 15px; text-decoration: none; font-size: 12px; font-weight: 750; cursor: pointer; transition: .18s ease;
}
.button:hover { transform: translateY(-1px); }
.button.primary { background: #67e8f9; color: var(--navy); }
.button.primary:hover { background: #a5f3fc; }
.button.secondary { border-color: #344054; background: #1d2939; color: #e4e7ec; }
.button.light { border-color: #d0d5dd; background: white; color: #344054; }
.pill {
display: inline-flex; align-items: center; min-height: 28px; border-radius: 999px; padding: 5px 10px;
font-size: 9px; font-weight: 850; letter-spacing: .1em; text-transform: uppercase; white-space: nowrap;
}
.pill.pass, .pill.certified, .pill.success, .pill.fresh, .pill.operational { background: var(--green-soft); color: var(--green); }
.pill.warning, .pill.attention, .pill.partial, .pill.stale, .pill.in-progress, .pill.in_progress { background: var(--amber-soft); color: var(--amber); }
.pill.error, .pill.failed, .pill.fail, .pill.blocked, .pill.non-certified, .pill.non_certified { background: var(--rose-soft); color: var(--rose); }
.pill.neutral, .pill.queued, .pill.unknown, .pill.no-data, .pill.no_data { background: #f2f4f7; color: #475467; }
.hero .pill { border: 1px solid #344054; background: #1d2939; color: #d0d5dd; }
.metric-grid { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 14px; }
.metric-card, .panel {
border: 1px solid var(--line); border-radius: 18px; background: white; box-shadow: var(--shadow);
}
.metric-card { min-height: 136px; padding: 19px; }
.metric-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .14em; text-transform: uppercase; }
.metric-value { margin-top: 18px; font-size: 27px; font-weight: 720; letter-spacing: -.04em; }
.metric-detail { margin-top: 7px; color: var(--muted); font-size: 11px; line-height: 1.45; }
.panel { overflow: hidden; }
.panel-head { display: flex; justify-content: space-between; gap: 20px; align-items: start; padding: 23px 24px 18px; }
.panel h2 { margin: 6px 0 0; font-size: 19px; letter-spacing: -.025em; }
.panel-description { max-width: 720px; margin: 7px 0 0; color: var(--muted); font-size: 12px; line-height: 1.6; }
.panel-body { border-top: 1px solid #f2f4f7; padding: 22px 24px 24px; }
.two-col { display: grid; grid-template-columns: minmax(0,1.2fr) minmax(330px,.8fr); gap: 18px; }
.rail-wrap { overflow-x: auto; padding-bottom: 4px; }
.evidence-rail { display: grid; grid-template-columns: repeat(7,minmax(116px,1fr)); min-width: 840px; gap: 7px; }
.gate-button {
position: relative; min-height: 92px; border: 1px solid var(--line); border-radius: 14px; background: #fafafa;
padding: 13px; color: #344054; text-align: left; cursor: pointer; transition: .18s ease;
}
.gate-button::after { content: ""; position: absolute; top: 27px; right: -8px; width: 8px; height: 1px; background: #d0d5dd; }
.gate-button:last-child::after { display: none; }
.gate-button:hover { border-color: #98a2b3; transform: translateY(-1px); }
.gate-button.selected { border-color: var(--cyan); background: var(--cyan-soft); box-shadow: 0 0 0 3px rgba(6,182,212,.09); }
.gate-index { color: var(--faint); font: 800 9px ui-monospace, monospace; letter-spacing: .1em; }
.gate-name { display: block; margin-top: 14px; font-size: 11px; font-weight: 750; }
.gate-state { display: flex; align-items: center; gap: 6px; margin-top: 8px; color: var(--muted); font-size: 9px; text-transform: uppercase; }
.state-dot { width: 7px; height: 7px; border-radius: 50%; background: #98a2b3; }
.state-dot.pass { background: #12b76a; }.state-dot.warning { background: #f79009; }.state-dot.error,.state-dot.blocked { background: #f04438; }.state-dot.running { background: #06b6d4; }
.dossier { display: grid; grid-template-columns: minmax(0,.9fr) minmax(360px,1.1fr); gap: 18px; margin-top: 20px; }
.decision-card { display: grid; gap: 12px; }
.inset { border: 1px solid var(--line); border-radius: 14px; background: #f9fafb; padding: 15px; }
.inset-label { color: var(--faint); font-size: 9px; font-weight: 850; letter-spacing: .13em; text-transform: uppercase; }
.inset p { margin: 9px 0 0; color: #344054; font-size: 12px; line-height: 1.6; }
.manifest { overflow: hidden; border: 1px solid #1d2939; border-radius: 16px; background: var(--navy); color: #e4e7ec; }
.manifest-head { display: flex; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid #1d2939; }
.manifest-head .eyebrow { color: #67e8f9; }
.manifest dl { margin: 0; }
.manifest-row { display: grid; grid-template-columns: 140px minmax(0,1fr); gap: 16px; border-bottom: 1px solid #1d2939; padding: 12px 18px; }
.manifest-row:last-child { border-bottom: 0; }
.manifest dt { color: #667085; font: 750 9px ui-monospace,monospace; letter-spacing: .07em; text-transform: uppercase; }
.manifest dd { margin: 0; overflow-wrap: anywhere; color: #d0d5dd; font: 11px/1.5 ui-monospace,monospace; }
.timeline { display: grid; gap: 10px; margin-top: 12px; }
.timeline-item { display: grid; grid-template-columns: 8px minmax(0,1fr); gap: 10px; }
.timeline-line { display: flex; flex-direction: column; align-items: center; }
.timeline-line .state-dot { margin-top: 5px; }
.timeline-line::after { content:""; width: 1px; flex: 1; margin-top: 4px; background: var(--line); }
.timeline-item:last-child .timeline-line::after { display: none; }
.timeline-copy { padding-bottom: 7px; }
.timeline-copy strong { font-size: 11px; text-transform: capitalize; }
.timeline-copy time { float: right; color: var(--faint); font: 9px ui-monospace,monospace; }
.timeline-copy p { margin: 4px 0 0; color: var(--muted); font-size: 11px; line-height: 1.5; }
.table-scroll { overflow-x: auto; }
table { width: 100%; min-width: 780px; border-collapse: collapse; font-size: 12px; }
th { background: #f9fafb; color: var(--faint); padding: 11px 15px; text-align: left; font-size: 9px; letter-spacing: .12em; text-transform: uppercase; }
td { border-top: 1px solid #f2f4f7; padding: 14px 15px; color: #344054; }
tbody tr { transition: background .15s ease; }
tbody tr:hover { background: #f0fdfa; }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
.right { text-align: right; }
.empty { padding: 52px 20px; color: var(--muted); text-align: center; }
.empty-mark { display: grid; place-items: center; width: 42px; height: 42px; margin: 0 auto 13px; border: 1px solid var(--line); border-radius: 50%; color: var(--faint); }
.notice { border: 1px solid; border-radius: 14px; padding: 15px 18px; font-size: 12px; line-height: 1.55; }
.notice.warning { border-color: #fedf89; background: var(--amber-soft); color: var(--amber); }
.notice.error { border-color: #fecdca; background: var(--rose-soft); color: var(--rose); }
.notice.success { border-color: #abefc6; background: var(--green-soft); color: var(--green); }
.findings { display: grid; gap: 10px; }
.finding { display: grid; grid-template-columns: 30px minmax(0,1fr); gap: 12px; border-left: 4px solid var(--cyan); border-radius: 11px; background: #f9fafb; padding: 14px; }
.finding.critical { border-color: #f04438; background: var(--rose-soft); }
.finding.warning { border-color: #f79009; background: var(--amber-soft); }
.finding-index { color: var(--faint); font: 700 14px ui-monospace,monospace; }
.finding code { font-size: 10px; font-weight: 800; color: #344054; }
.finding p { margin: 6px 0 0; color: var(--muted); font-size: 11px; line-height: 1.55; }
.source-grid { display: grid; gap: 10px; }
.source { border: 1px solid var(--line); border-radius: 13px; padding: 14px; }
.source-top { display: flex; justify-content: space-between; gap: 12px; }
.source h3 { margin: 0; font-size: 12px; text-transform: capitalize; }
.source-meta, .source-path { color: var(--faint); font: 9px/1.5 ui-monospace,monospace; }
.source-meta { margin-top: 5px; }.source-path { overflow-wrap:anywhere; border-top: 1px solid #f2f4f7; margin-top: 12px; padding-top: 10px; }
.coverage { display: grid; gap: 18px; }
.coverage-head { display: flex; justify-content: space-between; gap: 12px; font-size: 11px; }
.coverage-track { overflow: hidden; height: 7px; margin-top: 8px; border-radius: 99px; background: #eaecf0; }
.coverage-fill { height: 100%; border-radius: inherit; background: var(--cyan); }
.coverage-fill.width-0 { width: 0; }.coverage-fill.width-5 { width: 5%; }
.coverage-fill.width-10 { width: 10%; }.coverage-fill.width-15 { width: 15%; }
.coverage-fill.width-20 { width: 20%; }.coverage-fill.width-25 { width: 25%; }
.coverage-fill.width-30 { width: 30%; }.coverage-fill.width-35 { width: 35%; }
.coverage-fill.width-40 { width: 40%; }.coverage-fill.width-45 { width: 45%; }
.coverage-fill.width-50 { width: 50%; }.coverage-fill.width-55 { width: 55%; }
.coverage-fill.width-60 { width: 60%; }.coverage-fill.width-65 { width: 65%; }
.coverage-fill.width-70 { width: 70%; }.coverage-fill.width-75 { width: 75%; }
.coverage-fill.width-80 { width: 80%; }.coverage-fill.width-85 { width: 85%; }
.coverage-fill.width-90 { width: 90%; }.coverage-fill.width-95 { width: 95%; }
.coverage-fill.width-100 { width: 100%; }
.success-copy { color: var(--green); }
.filters { display: grid; grid-template-columns: 1fr 1fr 1fr 1.5fr auto; gap: 12px; align-items: end; }
.field span { display: block; margin-bottom: 7px; color: var(--faint); font-size: 9px; font-weight: 800; letter-spacing: .12em; text-transform: uppercase; }
.field input, .field select { width: 100%; min-height: 44px; border: 1px solid #d0d5dd; border-radius: 11px; background: white; padding: 8px 11px; color: var(--ink); font-size: 12px; }
.field input:hover, .field select:hover { border-color: #98a2b3; }
.filter-actions { display: flex; gap: 7px; }
.breakdown-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.loading-state { display: grid; gap: 18px; }
.skeleton { position: relative; overflow: hidden; border-radius: 20px; background: #eaecf0; }
.skeleton::after { content:""; position:absolute; inset:0; transform:translateX(-100%); background:linear-gradient(90deg,transparent,#ffffff80,transparent); animation: shimmer 1.4s infinite; }
.hero-skeleton { height: 280px; }.metric-skeleton { height: 136px; }
@keyframes shimmer { 100% { transform: translateX(100%); } }
.nav-scrim { position: fixed; z-index: 25; inset: 0; background: rgba(16,24,40,.5); }
@media (max-width: 1120px) {
.metric-grid { grid-template-columns: repeat(2,1fr); }
.two-col, .breakdown-grid, .dossier { grid-template-columns: 1fr; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.panel-head { flex-wrap: wrap; }
.filters { grid-template-columns: 1fr 1fr; }
.filter-actions { grid-column: 1/-1; }
}
@media (max-width: 820px) {
.sidebar { transform: translateX(-100%); transition: transform .2s ease; box-shadow: 18px 0 50px rgba(16,24,40,.28); }
.sidebar.open { transform: translateX(0); }
.shell { margin-left: 0; }
.menu-button { display: grid; }
.hero-grid { grid-template-columns: 1fr; }
.hero-actions { justify-content: flex-start; }
.dossier { grid-template-columns: 1fr; }
.top-actions .pill { display: none; }
}
@media (max-width: 560px) {
main { padding: 18px 14px 52px; }
.topbar { padding: 10px 14px; }
.metric-grid, .filters { grid-template-columns: 1fr; }
.hero { border-radius: 20px; padding: 25px 20px; }
.hero h2 { font-size: 31px; }
.panel-head, .panel-body { padding-left: 17px; padding-right: 17px; }
.manifest-row { grid-template-columns: 1fr; gap: 5px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media print {
.sidebar, .topbar { display: none; }
.shell { margin: 0; }
main { max-width: none; padding: 0; }
.hero, .metric-card, .panel { box-shadow: none; break-inside: avoid; }
}
@@ -0,0 +1,383 @@
(() => {
"use strict";
const params = new URLSearchParams(location.search);
const suppliedToken = params.get("token");
if (suppliedToken) sessionStorage.setItem("casan_viewer_token", suppliedToken);
const token = suppliedToken || sessionStorage.getItem("casan_viewer_token") || "";
params.delete("token");
history.replaceState({}, "", `${location.pathname}${params.size ? `?${params}` : ""}${location.hash}`);
const state = {
view: params.get("view") || (params.get("trace") ? "runs" : "overview"),
trace: params.get("trace") || "",
project: null,
registry: null,
h6: null,
run: null,
selectedGate: "H1-context",
};
const $ = (selector) => document.querySelector(selector);
const esc = (value) => String(value ?? "")
.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")
.replaceAll('"', "&quot;").replaceAll("'", "&#039;");
const slug = (value) => String(value || "unknown").toLowerCase().replaceAll("_", "-").replace(/[^a-z0-9-]/g, "");
const text = (value, fallback = "Unavailable") => value === null || value === undefined || value === "" ? fallback : String(value);
const number = (value) => value === null || value === undefined || !Number.isFinite(Number(value))
? "Unavailable" : new Intl.NumberFormat("en-US").format(Number(value));
const time = (value) => value ? String(value).replace("T", " ").replace("Z", " UTC") : "No timestamp";
const money = (value) => value === null || value === undefined ? "Unavailable" : `$${Number(value).toFixed(6)}`;
const pill = (value) => `<span class="pill ${slug(value)}">${esc(String(value || "unknown").replaceAll("_", " "))}</span>`;
async function api(path) {
const response = await fetch(path, {
headers: { "X-CASAN-Viewer-Token": token },
cache: "no-store",
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || `HTTP ${response.status}`);
return payload;
}
function downloadUrl(path, query = {}) {
const next = new URLSearchParams({ ...query, token });
return `${path}?${next}`;
}
function hero({ eyebrow, title, description, verdict, meta = [], actions = "" }) {
return `<article class="hero">
<div class="hero-grid">
<div>
<p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2>
<p class="hero-description">${esc(description)}</p>
<div class="hero-meta">${meta.map((item) => `<span>${esc(item)}</span>`).join("")}</div>
</div>
<div class="hero-actions">${verdict ? pill(verdict) : ""}${actions}</div>
</div>
</article>`;
}
function metric(label, value, detail) {
return `<article class="metric-card"><div class="metric-label">${esc(label)}</div>
<div class="metric-value">${esc(text(value))}</div><div class="metric-detail">${esc(detail)}</div></article>`;
}
function panel(eyebrow, title, description, body, right = "") {
return `<article class="panel"><header class="panel-head"><div><p class="eyebrow">${esc(eyebrow)}</p>
<h2>${esc(title)}</h2><p class="panel-description">${esc(description)}</p></div>${right}</header>
<div class="panel-body">${body}</div></article>`;
}
function empty(title, detail) {
return `<div class="empty"><div class="empty-mark">∅</div><strong>${esc(title)}</strong><p>${esc(detail)}</p></div>`;
}
function setView(view, push = true) {
state.view = ["overview", "runs", "h6"].includes(view) ? view : "overview";
document.querySelectorAll(".nav-item").forEach((item) => {
const active = item.dataset.view === state.view;
item.classList.toggle("active", active);
if (active) item.setAttribute("aria-current", "page");
else item.removeAttribute("aria-current");
});
document.querySelectorAll(".view").forEach((item) => item.classList.add("hidden"));
$(`#${state.view}-view`).classList.remove("hidden");
$("#page-title").textContent = {
overview: "Assurance overview", runs: "Run assurance registry", h6: "H6 AgentOps dossier",
}[state.view];
if (push) {
const next = new URL(location.href);
next.searchParams.set("view", state.view);
if (state.trace) next.searchParams.set("trace", state.trace);
else next.searchParams.delete("trace");
history.pushState({}, "", `${next.pathname}?${next.searchParams}`);
}
if (state.view === "runs") renderRuns();
if (state.view === "h6") renderH6();
closeNav();
}
function renderOverview() {
const profile = state.project;
const registry = state.registry;
const latest = registry.latest || {};
const recent = registry.runs || [];
const failures = recent.filter((row) => row.status === "failed").length;
const certified = latest.certified === true;
const latestTrace = latest.trace_id || recent[0]?.trace_id;
const maturity = profile.maturity || {};
const actions = latestTrace
? `<button class="button primary" type="button" data-open-trace="${esc(latestTrace)}">Inspect latest run</button>
<button class="button secondary" type="button" data-open-h6="${esc(latestTrace)}">Open H6</button>`
: `<button class="button secondary" type="button" data-view-target="runs">View registry</button>`;
$("#overview-view").innerHTML =
hero({
eyebrow: "CASAN Core · Local assurance",
title: latestTrace ? "One prompt. One evidence trail." : "Ready for the first governed run.",
description: "A read-only, offline-capable assurance workspace reconstructed from canonical project evidence. No Platform service or HTML generation on the prompt hot path.",
verdict: latestTrace ? (certified ? "certified" : "review required") : "ready",
meta: [
`${profile.project_name} · ${profile.edition}`,
latestTrace ? `Trace ${latestTrace}` : "No finalized trace yet",
`Maturity ${maturity.level == null ? "not assessed" : `L${maturity.level}`} · ${maturity.status}`,
],
actions,
}) +
`<section class="metric-grid">
${metric("Governed records", number(registry.count), `${recent.length} recent records loaded`)}
${metric("Latest H6 quality", latest.telemetry_quality || "Unavailable", "Missing values are disclosed, never coerced to zero")}
${metric("Observed failures", number(failures), `${recent.length ? Math.round((failures / recent.length) * 100) : 0}% of loaded records`)}
${metric("Evidence gates", latestTrace ? "H1 → H7" : "Waiting", latestTrace ? "Control-by-control reconstruction available" : "Evidence appears after a prompt finalizes")}
</section>` +
panel(
"Product boundary",
"Core is visual by default",
"The local viewer covers the complete single-project review workflow; centralized operations remain a Platform capability.",
`<div class="two-col">
<div class="notice success"><strong>Included in Core</strong><br>Latest run, history, interactive H1–H7 evidence, H6 AgentOps, and on-demand JSON/HTML export.</div>
<div class="notice warning"><strong>Platform boundary</strong><br>Multi-project fleet views, centralized RBAC, shared approvals, ingestion and organization-wide retention.</div>
</div>`
) +
recentRunsPanel(recent.slice(0, 6), "Latest governed evidence");
bindActions();
}
function recentRunsPanel(rows, title = "Recent governed runs") {
const body = rows.length ? `<div class="table-scroll"><table>
<thead><tr><th>Observed at</th><th>Lifecycle step</th><th>Verdict</th><th class="right">Latency</th><th class="right">Tokens</th><th class="right">Evidence</th></tr></thead>
<tbody>${rows.map((row) => `<tr>
<td class="mono">${esc(time(row.timestamp))}</td><td><strong>${esc(row.step || row.harness || "Agentic turn")}</strong></td>
<td>${pill(row.status)}</td><td class="right mono">${esc(row.latency_ms == null ? "Unavailable" : `${number(row.latency_ms)} ms`)}</td>
<td class="right mono">${esc(number(row.total_tokens))}</td>
<td class="right">${row.trace_id ? `<button class="button light" type="button" data-open-trace="${esc(row.trace_id)}">Inspect H1–H7</button>` : "—"}</td>
</tr>`).join("")}</tbody></table></div>` : empty("No governed run yet", "Complete a prompt in a configured client, then refresh this viewer.");
return panel("Evidence index", title, "Operational values are shown only when present in canonical telemetry.", body);
}
function renderRuns() {
const rows = state.registry.runs || [];
const selected = state.run;
const body = hero({
eyebrow: "Run assurance registry",
title: "Every governed run, one evidence trail.",
description: "Select a lifecycle record to reconstruct H1–H7 decisions, inspect sanitized evidence and export an independent dossier.",
verdict: rows.some((row) => row.status === "failed") ? "attention" : "operational",
meta: [`${state.registry.count} records`, `${rows.filter((row) => row.status === "failed").length} failures`, "Local canonical evidence"],
actions: `<button class="button primary" type="button" data-view-target="h6">Open H6 dossier</button>`,
}) + (selected ? renderTrace(selected) : "") + recentRunsPanel(rows);
$("#runs-view").innerHTML = body;
bindActions();
bindGates();
}
function renderTrace(report) {
const gates = report.gates || [];
let selected = gates.find((gate) => gate.id === state.selectedGate) || gates[0];
if (!selected) return "";
const rail = gates.map((gate, index) => `<button type="button" aria-pressed="${gate.id === selected.id}" class="gate-button ${gate.id === selected.id ? "selected" : ""}" data-gate="${esc(gate.id)}">
<span class="gate-index">0${index + 1}</span><span class="gate-name">${esc(gate.title)}</span>
<span class="gate-state"><span class="state-dot ${slug(gate.status)}"></span>${esc(gate.status)}</span></button>`).join("");
const events = (selected.events || []).slice(-4).reverse();
const timeline = events.length ? `<div class="timeline">${events.map((event) => `<div class="timeline-item">
<div class="timeline-line"><span class="state-dot ${slug(event.status)}"></span></div>
<div class="timeline-copy"><strong>${esc(event.status)}</strong><time>${esc(time(event.timestamp))}</time><p>${esc(event.reason)}</p></div>
</div>`).join("")}</div>` : `<p class="metric-detail">No individual event was emitted for this control.</p>`;
const entries = Object.entries(selected.evidence || {});
const manifest = entries.length ? `<dl>${entries.map(([key, value]) => `<div class="manifest-row"><dt>${esc(key)}</dt><dd>${esc(typeof value === "string" ? value : JSON.stringify(value))}</dd></div>`).join("")}</dl>`
: empty("No safe evidence fields", "This control has not emitted a sanitized evidence manifest.");
const actions = `<a class="button secondary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl(`/api/v1/reports/run/${encodeURIComponent(report.trace_id)}/export`, { format: "html" })}">Export dossier</a>`;
return panel(
"Run assurance",
"Governed execution dossier",
"A control-by-control reconstruction backed by sanitized lifecycle evidence.",
`<div class="hero trace-hero">
<div class="hero-grid"><div><p class="eyebrow">Trace provenance</p><h2 class="trace-title">${esc(report.trace_id)}</h2>
<div class="hero-meta trace-meta"><span>${report.summary.gates_observed}/7 controls observed</span><span>${report.project.edition} edition</span></div></div>
<div>${pill(report.verdict)}</div></div></div>
<div class="rail-wrap rail-spaced"><div class="evidence-rail">${rail}</div></div>
<div class="dossier">
<div class="decision-card"><div><p class="eyebrow">Selected control</p><h2>${esc(selected.title)}</h2><p class="panel-description">${esc(selected.description)}</p></div>
<div class="inset"><span class="inset-label">Latest decision · ${esc(time(selected.updated_at))}</span><p>${esc(selected.reason)}</p></div>
<div class="inset"><span class="inset-label">Event history</span>${timeline}</div>
</div>
<div class="manifest"><div class="manifest-head"><div><p class="eyebrow">Safe evidence manifest</p><div class="metric-detail">Sanitized fields persisted by the harness</div></div>${pill(`${entries.length} fields`)}</div>${manifest}</div>
</div>`,
actions,
);
}
function renderH6() {
const data = state.h6;
if (!data) return;
const query = data.scope || {};
const queryObject = Object.fromEntries(Object.entries(query).filter(([, value]) => value));
const actions = `<a class="button secondary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "json" })}">JSON evidence</a>
<a class="button primary" href="${downloadUrl("/api/v1/reports/h6/export", { ...queryObject, format: "html" })}">Export dossier</a>`;
const findings = data.findings.length ? `<div class="findings">${data.findings.map((item, index) => `<article class="finding ${slug(item.severity)}">
<span class="finding-index">${String(index + 1).padStart(2, "0")}</span><div><code>${esc(item.code)}</code><p>${esc(item.message)}</p></div></article>`).join("")}</div>`
: `<div class="notice success">No threshold breach was detected in the selected evidence boundary.</div>`;
const sources = data.evidence_sources.map((source) => {
const status = !source.present ? "missing" : source.stale ? "stale" : "fresh";
return `<article class="source"><div class="source-top"><div><h3>${esc(source.source)}</h3><div class="source-meta">${source.records} records · ${source.age_s == null ? "no timestamp" : `${number(source.age_s)}s old`}</div></div>${pill(status)}</div>
<div class="source-path">${esc(source.path)}</div></article>`;
}).join("");
const breakdown = (rows, subject) => `<div class="table-scroll"><table><thead><tr><th>${esc(subject)}</th><th class="right">Runs</th><th class="right">Failures</th><th class="right">Avg latency</th><th class="right">Tokens</th><th class="right">Cost</th></tr></thead>
<tbody>${rows.length ? rows.map((row) => `<tr><td><strong>${esc(row.key)}</strong></td><td class="right mono">${row.runs}</td><td class="right mono">${row.failures}</td><td class="right mono">${number(row.latency_avg_ms)} ms</td><td class="right mono">${number(row.tokens)}</td><td class="right mono">${money(row.cost_usd)}</td></tr>`).join("") : `<tr><td colspan="6">${empty("No matching records", `No ${subject.toLowerCase()} evidence is available.`)}</td></tr>`}</tbody></table></div>`;
$("#h6-view").innerHTML =
hero({
eyebrow: "H6 · AgentOps assurance",
title: "Operational evidence, ready for a decision.",
description: "Runtime health, reliability, cost provenance and telemetry integrity—designed to answer what happened, why it matters and where the evidence lives.",
verdict: data.verdict,
meta: [data.report_id, `Generated ${time(data.generated_at)}`, `Freshness ${data.freshness.status}`],
actions,
}) +
panel("Scope", "Evidence boundary", "Filters apply to the screen and both export formats.", h6Filters(data)) +
`<section class="metric-grid">
${metric("Governed runs", number(data.summary.runs), `${data.summary.success} successful · ${data.summary.degraded} degraded`)}
${metric("Failure rate", `${data.summary.failure_rate_pct}%`, `${data.summary.failed} failed · ${data.summary.alerts} alerts`)}
${metric("P95 latency", `${number(data.summary.latency_ms.p95)} ms`, `P50 ${number(data.summary.latency_ms.p50)} ms`)}
${metric("Telemetry integrity", data.data_quality.status.replaceAll("_", " "), `${data.summary.coverage.token_pct}% token · ${data.summary.coverage.cost_pct}% cost`)}
</section>` +
`<div class="two-col">
${panel("Decision brief", "What requires attention", "Threshold findings are review signals, not maturity claims.", findings, pill(data.verdict))}
${panel("Source integrity", "Evidence provenance", "Freshness is evaluated independently for each canonical source.", `<div class="source-grid">${sources}</div>`, pill(data.freshness.status))}
</div>` +
`<div class="breakdown-grid">
${panel("Runtime", "Execution profile by step", "Latency, failure and attribution by governed lifecycle step.", breakdown(data.details.by_step, "Step"))}
${panel("Provider", "Model usage and provenance", "Only provider-reported token and actual cost values are shown.", breakdown(data.details.by_provider, "Provider · model"))}
</div>` +
panel("Evidence quality", "Coverage and disclosure", "Unavailable telemetry remains null. Coverage gaps stay visible.", `<div class="two-col">
<div class="coverage">${coverage("Token attribution", data.summary.coverage.token_pct, `${data.summary.coverage.token_records}/${data.summary.coverage.runtime_records} records`)}
${coverage("Cost attribution", data.summary.coverage.cost_pct, `${data.summary.coverage.cost_records}/${data.summary.coverage.runtime_records} records`)}</div>
<div class="inset"><span class="inset-label">Disclosure notes</span>${data.data_quality.warnings.length ? `<ul>${data.data_quality.warnings.map((warning) => `<li class="metric-detail">${esc(warning)}</li>`).join("")}</ul>` : `<p class="success-copy">All required sources are present with no estimation warning.</p>`}</div>
</div>`, pill(data.data_quality.status));
bindH6Filters();
}
function coverage(label, value, detail) {
const width = Math.round(Math.max(0, Math.min(100, Number(value) || 0)) / 5) * 5;
return `<div><div class="coverage-head"><strong>${esc(label)}</strong><span>${esc(value)}%</span></div>
<div class="coverage-track"><div class="coverage-fill width-${width}"></div></div>
<div class="metric-detail">${esc(detail)}</div></div>`;
}
function h6Filters(data) {
const scope = data.scope || {};
const options = data.available_filters || { projects: [], runs: [] };
const date = (value) => value ? value.slice(0, 10) : "";
return `<form id="h6-filters" class="filters">
<label class="field"><span>Project</span><select name="project"><option value="">All projects</option>${options.projects.map((value) => `<option value="${esc(value)}" ${scope.project === value ? "selected" : ""}>${esc(value)}</option>`).join("")}</select></label>
<label class="field"><span>From</span><input type="date" name="from" value="${esc(date(scope.from))}"></label>
<label class="field"><span>To</span><input type="date" name="to" value="${esc(date(scope.to))}"></label>
<label class="field"><span>Run / trace</span><input name="run" list="run-options" value="${esc(scope.run || "")}" placeholder="All governed runs"><datalist id="run-options">${options.runs.map((value) => `<option value="${esc(value)}"></option>`).join("")}</datalist></label>
<div class="filter-actions"><button class="button primary" type="submit">Apply</button><button class="button light" type="button" id="clear-h6">Clear</button></div>
</form>`;
}
async function loadTrace(traceId) {
if (!traceId) return;
state.trace = traceId;
state.selectedGate = "H1-context";
$("#loading").classList.remove("hidden");
try {
state.run = await api(`/api/v1/reports/run/${encodeURIComponent(traceId)}`);
setView("runs");
} catch (error) {
showError(`Run report could not be reconstructed: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
async function loadH6(query = {}) {
const next = new URLSearchParams(Object.fromEntries(Object.entries(query).filter(([, value]) => value)));
$("#loading").classList.remove("hidden");
try {
state.h6 = await api(`/api/v1/reports/h6${next.size ? `?${next}` : ""}`);
renderH6();
} catch (error) {
showError(`H6 report could not be generated: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
function bindActions() {
document.querySelectorAll("[data-view-target]").forEach((button) => button.addEventListener("click", () => setView(button.dataset.viewTarget)));
document.querySelectorAll("[data-open-trace]").forEach((button) => button.addEventListener("click", () => loadTrace(button.dataset.openTrace)));
document.querySelectorAll("[data-open-h6]").forEach((button) => button.addEventListener("click", async () => {
await loadH6({ run: button.dataset.openH6 });
setView("h6");
}));
}
function bindGates() {
document.querySelectorAll("[data-gate]").forEach((button) => button.addEventListener("click", () => {
state.selectedGate = button.dataset.gate;
renderRuns();
}));
}
function bindH6Filters() {
$("#h6-filters")?.addEventListener("submit", async (event) => {
event.preventDefault();
const values = Object.fromEntries(new FormData(event.currentTarget));
await loadH6(values);
});
$("#clear-h6")?.addEventListener("click", () => loadH6());
}
function showError(message) {
$("#error").textContent = message;
$("#error").classList.remove("hidden");
}
function closeNav() {
$(".sidebar").classList.remove("open");
$("#nav-scrim").classList.add("hidden");
$("#menu-button").setAttribute("aria-expanded", "false");
}
async function loadAll() {
$("#error").classList.add("hidden");
$("#loading").classList.remove("hidden");
try {
[state.project, state.registry, state.h6] = await Promise.all([
api("/api/v1/project"),
api("/api/v1/runs?limit=100"),
api("/api/v1/reports/h6?limit=50"),
]);
$("#freshness-pill").textContent = `${state.h6.freshness.status} evidence`;
$("#freshness-pill").className = `pill ${slug(state.h6.freshness.status)}`;
renderOverview();
renderH6();
if (state.trace) {
await loadTrace(state.trace);
} else {
setView(state.view, false);
}
} catch (error) {
showError(`Local evidence could not be loaded: ${error.message}`);
} finally {
$("#loading").classList.add("hidden");
}
}
document.querySelectorAll(".nav-item").forEach((button) => button.addEventListener("click", () => setView(button.dataset.view)));
$("#refresh-button").addEventListener("click", loadAll);
$("#menu-button").addEventListener("click", () => {
const open = !$(".sidebar").classList.contains("open");
$(".sidebar").classList.toggle("open", open);
$("#nav-scrim").classList.toggle("hidden", !open);
$("#menu-button").setAttribute("aria-expanded", String(open));
});
$("#nav-scrim").addEventListener("click", closeNav);
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeNav();
});
window.addEventListener("popstate", () => setView(new URLSearchParams(location.search).get("view") || "overview", false));
loadAll();
})();
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="16" fill="#101828"/>
<rect x="10" y="10" width="44" height="44" rx="12" fill="#172033" stroke="#344054"/>
<path d="M39.5 22.5a14 14 0 1 0 0 19" fill="none" stroke="#67e8f9" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#101828" />
<title>CASAN · Local Assurance Viewer</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="/app.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to report</a>
<div id="app">
<aside class="sidebar" aria-label="Primary navigation">
<a class="brand" href="#overview" aria-label="CASAN overview">
<span class="brand-mark" aria-hidden="true">C</span>
<span><strong>CASAN</strong><small>Local Assurance</small></span>
</a>
<nav>
<button class="nav-item active" data-view="overview" type="button"><span aria-hidden="true">◫</span>Overview</button>
<button class="nav-item" data-view="runs" type="button"><span aria-hidden="true">⌁</span>Run registry</button>
<button class="nav-item" data-view="h6" type="button"><span aria-hidden="true">H6</span>AgentOps</button>
</nav>
<div class="local-seal">
<span class="live-dot"></span>
<div><strong>Local Core</strong><small>Loopback · read-only</small></div>
</div>
</aside>
<div class="shell">
<header class="topbar">
<button id="menu-button" class="icon-button menu-button" type="button" aria-label="Open navigation" aria-expanded="false">☰</button>
<div>
<p class="top-eyebrow">Independent evidence workspace</p>
<h1 id="page-title">Assurance overview</h1>
</div>
<div class="top-actions">
<span id="freshness-pill" class="pill neutral">Evidence loading</span>
<button id="refresh-button" class="icon-button" type="button" aria-label="Refresh evidence">↻</button>
</div>
</header>
<main id="main" tabindex="-1">
<div id="loading" class="loading-state" aria-live="polite">
<div class="skeleton hero-skeleton"></div>
<div class="metric-grid">
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
<div class="skeleton metric-skeleton"></div><div class="skeleton metric-skeleton"></div>
</div>
</div>
<div id="error" class="notice error hidden" role="alert"></div>
<section id="overview-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="runs-view" class="view hidden" aria-labelledby="page-title"></section>
<section id="h6-view" class="view hidden" aria-labelledby="page-title"></section>
</main>
</div>
<div id="nav-scrim" class="nav-scrim hidden"></div>
</div>
<script src="/app.js" defer></script>
</body>
</html>
@@ -0,0 +1,701 @@
#!/usr/bin/env python3
"""Zero-dependency, read-only reporting engine for CASAN Core.
The evidence files under .specify remain the source of truth. This module only
builds bounded, sanitized projections for the Local Assurance Viewer and
on-demand JSON/HTML exports.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from datetime import datetime, timezone
from html import escape
import json
import math
import os
from pathlib import Path
import re
from typing import Any
SAFE_ID = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$")
STALE_AFTER_S = 900
MAX_SOURCE_BYTES = 20 * 1024 * 1024
MAX_RECORDS = 2000
GATES = (
("H1-context", "H1 · Context", "Prompt contract, mode and risk classification"),
("H2-tool", "H2 · Tool", "Allowlisted source and tool preparation"),
("H3-eval", "H3 · Eval", "Grounded synthesis and quality evaluation"),
("H4-security", "H4 · Security", "Input and output security boundary"),
("H5-governance", "H5 · Governance", "Decision policy and append-only audit"),
("H6-agentops", "H6 · AgentOps", "Runtime, token, cost and failure telemetry"),
("H7-orchestration", "H7 · Orchestration", "Final governed outcome and certification"),
)
GATE_MAP = {f"H{index}": gate[0] for index, gate in enumerate(GATES, 1)}
SENSITIVE_KEYS = {
"authorization", "api_key", "apikey", "access_token", "refresh_token",
"password", "secret", "prompt", "raw_prompt", "tool_input", "tool_output",
"request_body", "response_body",
}
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
def state_root(root: str | Path) -> Path:
override = os.environ.get("CASAN_STATE_ROOT")
return Path(override).expanduser().resolve() if override else Path(root).resolve() / ".specify"
def _read_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file() or path.stat().st_size > maximum:
return ""
return path.read_text(encoding="utf-8", errors="replace")
except OSError:
return ""
def _read_tail_text(path: Path, maximum: int = MAX_SOURCE_BYTES) -> str:
try:
if not path.is_file():
return ""
size = path.stat().st_size
with path.open("rb") as handle:
if size > maximum:
handle.seek(-maximum, os.SEEK_END)
handle.readline() # discard a possibly partial first record
return handle.read(maximum).decode("utf-8", errors="replace")
except OSError:
return ""
def read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(_read_text(path))
return value if isinstance(value, dict) else {}
except (TypeError, ValueError):
return {}
def read_jsonl(path: Path, maximum_records: int = MAX_RECORDS) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for line in _read_tail_text(path).splitlines()[-maximum_records:]:
try:
value = json.loads(line)
except ValueError:
continue
if isinstance(value, dict):
rows.append(value)
return rows
def _safe_value(value: Any, depth: int = 0) -> Any:
if depth > 5:
return "[depth limited]"
if isinstance(value, dict):
result: dict[str, Any] = {}
for key, item in list(value.items())[:80]:
normalized = str(key).lower().replace("-", "_")
if normalized in SENSITIVE_KEYS:
result[str(key)] = "[redacted]"
else:
result[str(key)] = _safe_value(item, depth + 1)
return result
if isinstance(value, list):
return [_safe_value(item, depth + 1) for item in value[:80]]
if isinstance(value, str):
return value if len(value) <= 4096 else value[:4096] + "…"
if value is None or isinstance(value, (bool, int, float)):
return value
return str(value)[:4096]
def sanitize(row: dict[str, Any]) -> dict[str, Any]:
value = _safe_value(row)
return value if isinstance(value, dict) else {}
def _number(value: Any) -> float | None:
if isinstance(value, bool):
return None
if isinstance(value, (int, float)) and math.isfinite(float(value)):
return float(value)
return None
def _threshold(name: str, fallback: float) -> float:
try:
value = float(os.environ.get(name, str(fallback)))
return value if math.isfinite(value) and value >= 0 else fallback
except ValueError:
return fallback
def _text(value: Any, fallback: str = "") -> str:
return value.strip() if isinstance(value, str) and value.strip() else fallback
def _row_run(row: dict[str, Any]) -> str:
return _text(row.get("run_id") or row.get("trace_id"), "unattributed")
def _row_project(row: dict[str, Any]) -> str:
return _text(row.get("project") or row.get("project_id"), "default")
def _parse_time(value: Any) -> float | None:
if not isinstance(value, str) or not value:
return None
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).timestamp()
except ValueError:
return None
def _status(value: Any) -> str:
status = _text(value).lower()
if status in {"success", "pass", "passed", "allow", "allowed", "answered", "opened", "certified"}:
return "pass"
if status in {"warn", "warning", "degraded", "partial", "insufficient"}:
return "warning"
if status in {"block", "blocked", "deny", "denied", "flag", "non_certified"}:
return "blocked"
if status in {"fail", "failed", "error"}:
return "error"
if status in {"running", "skipped"}:
return status
return "queued"
def paths(root: str | Path) -> dict[str, Path]:
state = state_root(root)
return {
"config": Path(root).resolve() / ".casan" / "config.json",
"receipt": state / "state" / "latest-run.json",
"metrics": state / "logs" / "cost" / "metrics.jsonl",
"provider": state / "logs" / "level5" / "provider-usage.jsonl",
"alerts": state / "agentops" / "alerts.log",
"trace_dir": state / "logs" / "trace",
"event_dir": state / "logs" / "trace-events",
}
def project_profile(root: str | Path) -> dict[str, Any]:
config = read_json(paths(root)["config"])
maturity = config.get("maturity") if isinstance(config.get("maturity"), dict) else {}
return {
"project_id": _text(config.get("project_id"), Path(root).resolve().name),
"project_name": _text(config.get("project_name"), Path(root).resolve().name),
"edition": _text(config.get("edition") or config.get("target_level_name"), "core"),
"edition_status": _text(config.get("edition_status"), "configured"),
"enforcement_mode": _text(config.get("enforcement_mode"), "unknown"),
"integration_mode": _text(config.get("integration_mode"), "unknown"),
"clients": [str(value) for value in config.get("clients", []) if isinstance(value, str)],
"maturity": {
"level": maturity.get("level") if _number(maturity.get("level")) is not None else None,
"status": _text(maturity.get("status"), "not_assessed"),
"evidence": _text(maturity.get("evidence")) or None,
},
"viewer": {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
},
}
def _trace(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return {}
return read_json(paths(root)["trace_dir"] / f"agentic-{trace_id}.json")
def _metric_rows(root: str | Path) -> list[dict[str, Any]]:
return read_jsonl(paths(root)["metrics"])
def trace_graph(root: str | Path, trace_id: str) -> dict[str, Any]:
if not SAFE_ID.fullmatch(trace_id):
return _empty_graph(trace_id)
source = paths(root)
events: list[dict[str, Any]] = []
for row in read_jsonl(source["event_dir"] / f"{trace_id}.jsonl", 500):
if row.get("trace_id") != trace_id or row.get("gate_id") not in {gate[0] for gate in GATES}:
continue
events.append({
"timestamp": _text(row.get("timestamp")),
"trace_id": trace_id,
"gate_id": _text(row.get("gate_id")),
"status": _status(row.get("status")),
"reason": _text(row.get("reason"), "Evidence observed"),
"evidence": sanitize(row.get("evidence") if isinstance(row.get("evidence"), dict) else {}),
})
if not events:
trace = _trace(root, trace_id)
harness = _text(trace.get("harness"))
if harness in {gate[0] for gate in GATES}:
events.append({
"timestamp": _text(trace.get("timestamp")),
"trace_id": trace_id,
"gate_id": harness,
"status": _status(trace.get("status") or trace.get("action")),
"reason": f"Legacy {harness} trace",
"evidence": sanitize({
"mode": trace.get("mode"),
"action": trace.get("action"),
"risk_level": trace.get("risk_level"),
}),
})
evidence_rows = trace.get("evidence") if isinstance(trace.get("evidence"), list) else []
for evidence in evidence_rows[:200]:
if not isinstance(evidence, dict):
continue
for marker in _text(evidence.get("h")).split("/"):
gate_id = GATE_MAP.get(marker)
if gate_id:
events.append({
"timestamp": _text(evidence.get("at") or trace.get("finalized_at")),
"trace_id": trace_id,
"gate_id": gate_id,
"status": _status(evidence.get("decision")),
"reason": f"{_text(evidence.get('kind'), 'legacy-evidence')}: {_text(evidence.get('detail'))}",
"evidence": sanitize({
"kind": evidence.get("kind"),
"decision": evidence.get("decision"),
"detail": evidence.get("detail"),
"certification_strength": trace.get("certification_strength"),
}),
})
for metric in _metric_rows(root):
if metric.get("trace_id") != trace_id:
continue
quality = _text(metric.get("telemetry_quality"))
events.append({
"timestamp": _text(metric.get("timestamp")),
"trace_id": trace_id,
"gate_id": "H6-agentops",
"status": "warning" if quality in {"partial", "insufficient"} else _status(metric.get("status")),
"reason": _text(metric.get("step"), "Runtime metric"),
"evidence": sanitize({
"latency_ms": metric.get("latency_ms"),
"total_tokens": metric.get("total_tokens"),
"cost_estimate": metric.get("cost_estimate"),
"telemetry_quality": metric.get("telemetry_quality"),
}),
})
events.sort(key=lambda row: _text(row.get("timestamp")))
nodes = []
for gate_id, title, description in GATES:
gate_events = [row for row in events if row["gate_id"] == gate_id]
latest = gate_events[-1] if gate_events else {}
nodes.append({
"id": gate_id,
"title": title,
"description": description,
"status": latest.get("status", "queued"),
"reason": latest.get("reason", "Waiting for evidence"),
"updated_at": latest.get("timestamp") or None,
"evidence": latest.get("evidence", {}),
"events": gate_events,
})
outcome = nodes[-1]
return {
"found": bool(events),
"trace_id": trace_id,
"updated_at": events[-1]["timestamp"] if events else None,
"terminal": outcome["status"] in {"pass", "blocked", "error"},
"progress": sum(1 for node in nodes if node["status"] != "queued"),
"nodes": nodes,
"events": events,
}
def _empty_graph(trace_id: str) -> dict[str, Any]:
nodes = [{
"id": gate_id, "title": title, "description": description,
"status": "queued", "reason": "Waiting for evidence",
"updated_at": None, "evidence": {}, "events": [],
} for gate_id, title, description in GATES]
return {
"found": False, "trace_id": trace_id, "updated_at": None,
"terminal": False, "progress": 0, "nodes": nodes, "events": [],
}
def run_report(root: str | Path, trace_id: str) -> dict[str, Any]:
graph = trace_graph(root, trace_id)
trace = _trace(root, trace_id)
metric = next(
(row for row in reversed(_metric_rows(root)) if row.get("trace_id") == trace_id),
None,
)
profile = project_profile(root)
metric_row = metric or {}
duration = _number(metric_row.get("duration_ms"))
if duration is None:
duration = _number(metric_row.get("latency_ms"))
found = bool(trace or graph["found"] or metric)
finalized = _text(trace.get("finalized_at") or metric_row.get("finished_at")) or None
if not found:
verdict = "not_found"
elif not graph["terminal"] and not finalized:
verdict = "in_progress"
elif trace.get("certified") is True:
verdict = "certified"
else:
verdict = "non_certified"
cost = metric_row.get("cost") if isinstance(metric_row.get("cost"), dict) else {}
reasons = trace.get("certification_reasons") if isinstance(trace.get("certification_reasons"), list) else []
return {
"schema_version": 1,
"report_id": f"RUN-{trace_id}",
"generated_at": utc_now(),
"trace_id": trace_id,
"project": {
"id": profile["project_id"],
"name": profile["project_name"],
"edition": profile["edition"],
"maturity": profile["maturity"],
},
"verdict": verdict,
"certification": {
"strength": _text(trace.get("certification_strength") or metric_row.get("certification_strength")) or None,
"reasons": [str(value) for value in reasons if isinstance(value, str)],
"finalized_at": finalized,
},
"summary": {
"gates_observed": graph["progress"],
"gates_total": 7,
"tool_calls": int(_number(trace.get("tool_calls") or metric_row.get("tool_calls")) or 0),
"failures": int(_number(trace.get("failures") or metric_row.get("failures")) or 0),
"duration_ms": duration,
"telemetry_quality": _text(metric_row.get("telemetry_quality"), "unknown"),
"token_usage_available": any(_number(metric_row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")),
"cost_available": _number(metric_row.get("cost_estimate")) is not None or _number(cost.get("amount")) is not None,
},
"gates": graph["nodes"],
"h6": sanitize(metric_row) if metric else None,
"source": {
"trace_found": bool(trace),
"graph_found": graph["found"],
"metric_found": metric is not None,
},
}
def runs(root: str | Path, limit: int = 100) -> dict[str, Any]:
limit = max(1, min(int(limit), 200))
rows = _metric_rows(root)
receipts = read_json(paths(root)["receipt"])
safe_rows = [sanitize(row) for row in reversed(rows[-limit:])]
if not safe_rows and receipts.get("trace_id"):
safe_rows = [sanitize({
"trace_id": receipts.get("trace_id"),
"project_id": receipts.get("project_id"),
"status": "success" if receipts.get("certified") else "failed",
"timestamp": receipts.get("finalized_at"),
"latency_ms": receipts.get("duration_ms"),
"tool_calls": receipts.get("tool_calls"),
"failures": receipts.get("failures"),
"telemetry_quality": receipts.get("telemetry_quality"),
})]
return {"count": len(rows) if rows else len(safe_rows), "runs": safe_rows, "latest": sanitize(receipts)}
def _freshness(path: Path, source: str, required: bool) -> dict[str, Any]:
try:
stat = path.stat()
age = max(0, int(datetime.now(timezone.utc).timestamp() - stat.st_mtime))
present = path.is_file()
except OSError:
age, present = None, False
return {
"source": source,
"path": str(path),
"present": present,
"updated_at": datetime.fromtimestamp(stat.st_mtime, timezone.utc).isoformat().replace("+00:00", "Z") if present else None,
"age_s": age,
"stale": bool(present and age is not None and age > STALE_AFTER_S),
"required": required,
}
def _display_path(root: str | Path, path: Path) -> str:
try:
return str(path.resolve().relative_to(Path(root).resolve()))
except (OSError, ValueError):
return f"[external]/{path.name}"
def _percentile(values: list[float], pct: int) -> float:
if not values:
return 0
ordered = sorted(values)
return ordered[max(0, math.ceil((pct / 100) * len(ordered)) - 1)]
def _grouped(rows: list[dict[str, Any]], key) -> list[dict[str, Any]]:
groups: dict[str, list[dict[str, Any]]] = defaultdict(list)
for row in rows:
groups[key(row)].append(row)
result = []
for name, records in groups.items():
latencies = [_number(row.get("latency_ms")) for row in records]
latencies = [value for value in latencies if value is not None and value > 0]
tokens = [_number(row.get("total_tokens")) for row in records]
cost_values = [
_number(row.get("cost_usd")) if _number(row.get("cost_usd")) is not None
else _number(row.get("cost_estimate"))
for row in records
]
result.append({
"key": name,
"runs": len(records),
"failures": sum(1 for row in records if _text(row.get("status"), "unknown") == "failed"),
"latency_avg_ms": round(sum(latencies) / len(latencies)) if latencies else 0,
"tokens": round(sum(value for value in tokens if value is not None)) if any(value is not None for value in tokens) else None,
"cost_usd": round(sum(value for value in cost_values if value is not None), 6) if any(value is not None for value in cost_values) else None,
})
return sorted(result, key=lambda row: (-row["runs"], row["key"]))
def _matches(row: dict[str, Any], project: str | None, run: str | None,
start: float | None, end: float | None) -> bool:
if project and _row_project(row) != project:
return False
if run and _row_run(row) != run:
return False
timestamp = _parse_time(row.get("timestamp"))
if start is not None and (timestamp is None or timestamp < start):
return False
if end is not None and (timestamp is None or timestamp > end):
return False
return True
def _date_boundary(value: str | None, end: bool = False) -> tuple[str | None, float | None]:
if not value:
return None, None
normalized = value
if re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
normalized += "T23:59:59.999Z" if end else "T00:00:00.000Z"
parsed = _parse_time(normalized)
if parsed is None:
raise ValueError("invalid report time boundary")
return datetime.fromtimestamp(parsed, timezone.utc).isoformat().replace("+00:00", "Z"), parsed
def h6_report(root: str | Path, project: str | None = None, run: str | None = None,
from_date: str | None = None, to_date: str | None = None,
limit: int = 50) -> dict[str, Any]:
for value in (project, run):
if value and not SAFE_ID.fullmatch(value):
raise ValueError("invalid project or run filter")
normalized_from, start = _date_boundary(from_date)
normalized_to, end = _date_boundary(to_date, True)
if start is not None and end is not None and start > end:
raise ValueError("invalid report time range")
limit = max(1, min(int(limit), 200))
source = paths(root)
all_metrics = _metric_rows(root)
all_provider = read_jsonl(source["provider"])
all_alerts = read_jsonl(source["alerts"])
metrics = [row for row in all_metrics if _matches(row, project, run, start, end)]
provider = [row for row in all_provider if _matches(row, project, run, start, end)]
alerts = [row for row in all_alerts if _matches(row, project, run, start, end)]
latencies = [value for value in (_number(row.get("latency_ms")) for row in metrics) if value is not None and value > 0]
failed = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "failed")
degraded = sum(1 for row in metrics if _text(row.get("status"), "unknown") == "degraded")
success = sum(1 for row in metrics if _text(row.get("status"), "unknown") in {"success", "pass", "passed"})
provider_token_runs = {_row_run(row) for row in provider if _number(row.get("total_tokens")) is not None}
provider_cost_runs = {_row_run(row) for row in provider if _number(row.get("cost_usd")) is not None}
token_records = sum(1 for row in metrics if any(_number(row.get(key)) is not None for key in ("total_tokens", "input_tokens", "output_tokens")) or _row_run(row) in provider_token_runs)
cost_records = sum(1 for row in metrics if _number(row.get("cost_estimate")) is not None or _row_run(row) in provider_cost_runs)
token_pct = round((token_records / len(metrics)) * 100, 1) if metrics else 0
cost_pct = round((cost_records / len(metrics)) * 100, 1) if metrics else 0
quality = Counter(_text(row.get("telemetry_quality"), "unknown") for row in metrics)
evidence = [
{**_freshness(source["metrics"], "metrics", True), "records": len(all_metrics)},
{**_freshness(source["provider"], "provider", False), "records": len(all_provider)},
{**_freshness(source["alerts"], "alerts", False), "records": len(all_alerts)},
]
for item in evidence:
item["path"] = _display_path(root, Path(item["path"]))
primary = evidence[0]
failure_rate = round((failed / len(metrics)) * 100, 1) if metrics else 0
p95 = _percentile(latencies, 95)
failure_threshold = _threshold("CASAN_H6_FAILURE_RATE_THRESHOLD_PCT", 5)
p95_threshold = _threshold("CASAN_H6_P95_LATENCY_THRESHOLD_MS", 5000)
findings: list[dict[str, Any]] = []
warnings: list[str] = []
if not primary["present"]:
findings.append({"severity": "critical", "code": "METRICS_MISSING", "message": "The required runtime metrics source is missing."})
elif primary["stale"]:
findings.append({"severity": "warning", "code": "METRICS_STALE", "message": "The primary runtime metrics source is stale.", "metric": "age_s", "value": primary["age_s"], "threshold": STALE_AFTER_S})
if failure_rate > failure_threshold:
findings.append({"severity": "critical", "code": "FAILURE_RATE_BREACH", "message": "Failure rate exceeds the configured H6 threshold.", "metric": "failure_rate_pct", "value": failure_rate, "threshold": failure_threshold})
elif failed:
findings.append({"severity": "warning", "code": "FAILURES_PRESENT", "message": f"{failed} failed run(s) are present in the selected scope.", "metric": "failed", "value": failed})
if p95 > p95_threshold:
findings.append({"severity": "warning", "code": "P95_LATENCY_BREACH", "message": "P95 latency exceeds the configured H6 threshold.", "metric": "p95_latency_ms", "value": p95, "threshold": p95_threshold})
alert_counter: Counter[str] = Counter()
fingerprints: set[tuple[str, str]] = set()
for row in metrics + alerts:
alert_types: list[str] = []
if isinstance(row.get("alerts"), list):
alert_types.extend(str(value) for value in row["alerts"] if isinstance(value, str))
body = row.get("body") if isinstance(row.get("body"), dict) else {}
if isinstance(body.get("alert.type"), str):
alert_types.append(body["alert.type"])
for alert in alert_types:
row_run = _row_run(row)
fingerprint = (
_text(row.get("timestamp"), "unknown-time")
if row_run == "unattributed" else row_run,
alert,
)
if fingerprint not in fingerprints:
alert_counter[alert] += 1
fingerprints.add(fingerprint)
alert_count = sum(alert_counter.values())
if alert_count:
findings.append({"severity": "warning", "code": "ALERTS_PRESENT", "message": f"{alert_count} alert signal(s) require review.", "metric": "alerts", "value": alert_count})
if degraded:
findings.append({"severity": "warning", "code": "DEGRADED_RUNS_PRESENT", "message": f"{degraded} degraded run(s) are present in the selected scope.", "metric": "degraded", "value": degraded})
if metrics and (token_pct < 100 or cost_pct < 100):
findings.append({"severity": "warning", "code": "TELEMETRY_COVERAGE_GAP", "message": "Some runs do not have reliable provider token or cost attribution.", "metric": "token_cost_coverage_pct", "value": f"{token_pct}/{cost_pct}", "threshold": "100/100"})
if not provider:
warnings.append("No provider usage records matched the selected scope; token and actual-cost breakdown may be incomplete.")
if metrics and token_pct < 100:
warnings.append(f"{token_pct}% of runtime records have reliable token attribution; unavailable values remain null, never zero.")
if metrics and cost_pct < 100:
warnings.append(f"{cost_pct}% of runtime records have reliable cost attribution; unavailable values remain null, never zero.")
for item in evidence[1:]:
if not item["present"]:
warnings.append(f"Optional {item['source']} telemetry source is missing; its breakdown is unavailable.")
elif item["stale"]:
warnings.append(f"Optional {item['source']} telemetry source is stale; its breakdown may not reflect recent activity.")
critical = any(item["severity"] == "critical" for item in findings)
warning = any(item["severity"] == "warning" for item in findings)
verdict = "no_data" if not metrics and not provider else "fail" if critical else "attention" if warning else "pass"
status_counts = Counter(_text(row.get("status"), "unknown") for row in metrics)
generated_at = utc_now()
total = lambda rows, key: sum(value for value in (_number(row.get(key)) for row in rows) if value is not None)
return {
"schema_version": 1,
"report_id": "H6-" + re.sub(r"[-:.TZ+]", "", generated_at)[:14],
"harness": "H6",
"title": "H6 · AgentOps Report",
"description": "Evidence-backed runtime, token, cost, failure, retry and alert telemetry.",
"generated_at": generated_at,
"scope": {"project": project, "from": normalized_from, "to": normalized_to, "run": run},
"verdict": verdict,
"verdict_reasons": [item["code"] for item in findings],
"freshness": {"status": "missing" if not primary["present"] else "stale" if primary["stale"] else "live", "stale_after_s": STALE_AFTER_S, "primary_age_s": primary["age_s"], "sources": evidence},
"summary": {
"runs": len(metrics), "success": success, "failed": failed, "degraded": degraded,
"failure_rate_pct": failure_rate, "retries": int(total(metrics, "retry_count")),
"latency_ms": {
"average": round(sum(latencies) / len(latencies)) if latencies else 0,
"p50": _percentile(latencies, 50), "p95": p95,
"p99": _percentile(latencies, 99), "max": max(latencies) if latencies else 0,
},
"tokens": {
"input": total(metrics, "input_tokens") if any(_number(row.get("input_tokens")) is not None for row in metrics) else None,
"output": total(metrics, "output_tokens") if any(_number(row.get("output_tokens")) is not None for row in metrics) else None,
"total": total(metrics, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in metrics) else None,
"provider_total": total(provider, "total_tokens") if any(_number(row.get("total_tokens")) is not None for row in provider) else None,
},
"cost_usd": {
"provider_actual": round(total(provider, "cost_usd"), 6) if any(_number(row.get("cost_usd")) is not None for row in provider) else None,
"estimated": round(total(metrics, "cost_estimate"), 6) if any(_number(row.get("cost_estimate")) is not None for row in metrics) else None,
},
"provider_calls": len(provider), "alerts": alert_count,
"coverage": {
"runtime_records": len(metrics), "token_records": token_records, "cost_records": cost_records,
"token_pct": token_pct, "cost_pct": cost_pct,
"quality": {name: quality.get(name, 0) for name in ("complete", "partial", "insufficient", "unknown")},
},
},
"thresholds": {"failure_rate_pct": failure_threshold, "p95_latency_ms": p95_threshold, "freshness_age_s": STALE_AFTER_S},
"findings": findings,
"evidence_sources": evidence,
"data_quality": {"status": "insufficient" if not primary["present"] or (metrics and not token_records and not cost_records) else "partial" if warnings else "complete", "warnings": warnings},
"available_filters": {
"projects": sorted({_row_project(row) for row in all_metrics + all_provider + all_alerts}),
"runs": sorted({_row_run(row) for row in all_metrics + all_provider + all_alerts} - {"unattributed"}),
},
"details": {
"by_status": [{"status": key, "count": value} for key, value in status_counts.most_common()],
"by_step": _grouped(metrics, lambda row: _text(row.get("step"), "unknown-step")),
"by_provider": _grouped(provider, lambda row: f"{_text(row.get('provider'), 'unknown-provider')} · {_text(row.get('model'), 'unknown-model')}"),
"by_cost_source": [{"source": row["key"], "records": row["runs"], "cost_usd": row["cost_usd"] or 0} for row in _grouped(metrics, lambda row: _text(row.get("cost_source"), "unknown"))],
"by_alert": [{"alert": key, "count": value} for key, value in alert_counter.most_common()],
"recent_runs": [sanitize(row) for row in reversed(metrics[-limit:])],
"recent_alerts": [sanitize(row) for row in reversed(alerts[-limit:])],
},
}
def report_html(report: dict[str, Any], kind: str) -> str:
"""Render a self-contained, print-ready evidence dossier."""
title = report.get("title") or (
f"Run assurance · {report.get('trace_id', 'unknown')}"
)
verdict = _text(report.get("verdict"), "unknown").replace("_", " ")
if kind == "h6":
summary = report.get("summary", {})
metrics = [
("Governed runs", summary.get("runs")),
("Failure rate", f"{summary.get('failure_rate_pct', 0)}%"),
("P95 latency", f"{summary.get('latency_ms', {}).get('p95', 0)} ms"),
("Data quality", report.get("data_quality", {}).get("status")),
]
findings = report.get("findings", [])
else:
summary = report.get("summary", {})
metrics = [
("Evidence gates", f"{summary.get('gates_observed', 0)}/7"),
("Tool calls", summary.get("tool_calls")),
("Failures", summary.get("failures")),
("H6 quality", summary.get("telemetry_quality")),
]
findings = [
{"code": gate.get("title"), "severity": gate.get("status"), "message": gate.get("reason")}
for gate in report.get("gates", [])
]
cards = "".join(
f'<div class="metric"><span>{escape(str(label))}</span><strong>{escape(str(value if value is not None else "Unavailable"))}</strong></div>'
for label, value in metrics
)
rows = "".join(
'<article class="finding"><div><code>%s</code><em>%s</em></div><p>%s</p></article>' % (
escape(str(item.get("code", "EVIDENCE"))),
escape(str(item.get("severity", "info"))),
escape(str(item.get("message", "No explanation emitted."))),
) for item in findings
) or '<div class="clear">No threshold breach was detected.</div>'
contract = escape(json.dumps(report, ensure_ascii=False, indent=2))
return f"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>{escape(str(title))}</title>
<style>
:root{{--ink:#101828;--muted:#667085;--line:#e4e7ec;--paper:#f7f8fa;--navy:#101828;--cyan:#06b6d4;--green:#067647;--amber:#b54708}}
*{{box-sizing:border-box}}body{{margin:0;background:var(--paper);color:var(--ink);font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}}
main{{max-width:1060px;margin:auto;padding:44px 24px 72px}}header{{border-radius:28px;background:var(--navy);color:white;padding:38px;box-shadow:0 24px 70px #10182820}}
.eyebrow,dt,.metric span{{font-size:11px;font-weight:800;letter-spacing:.14em;text-transform:uppercase}}.eyebrow{{color:#67e8f9}}h1{{max-width:760px;margin:12px 0 14px;font-size:42px;line-height:1.05;letter-spacing:-.04em}}
.meta{{display:flex;flex-wrap:wrap;gap:12px;color:#98a2b3;font:12px ui-monospace,monospace}}.badge{{display:inline-block;margin-top:20px;border:1px solid #344054;border-radius:999px;padding:8px 12px;font-size:11px;font-weight:800;text-transform:uppercase}}
.grid{{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:18px 0}}.metric,.panel{{border:1px solid var(--line);border-radius:18px;background:white;padding:20px;box-shadow:0 10px 30px #10182808}}
.metric span{{display:block;color:var(--muted)}}.metric strong{{display:block;margin-top:16px;font-size:24px;letter-spacing:-.03em}}.panel{{margin-top:18px;padding:26px}}h2{{margin:0 0 6px;font-size:20px}}.sub{{margin:0 0 20px;color:var(--muted);font-size:14px}}
.finding{{border-left:4px solid var(--cyan);border-radius:10px;background:#f8fafc;padding:14px 16px;margin-top:10px}}.finding div{{display:flex;justify-content:space-between;gap:16px}}code{{font-weight:800}}em{{color:var(--muted);font-size:11px;text-transform:uppercase}}.finding p{{margin:8px 0 0;color:#344054;font-size:14px;line-height:1.55}}.clear{{border-radius:12px;background:#ecfdf3;color:var(--green);padding:16px}}
details{{margin-top:18px}}summary{{cursor:pointer;font-weight:700}}pre{{overflow:auto;max-height:720px;border-radius:14px;background:#101828;color:#d0d5dd;padding:20px;font:11px/1.6 ui-monospace,monospace}}
footer{{margin-top:22px;color:var(--muted);font-size:11px}}@media(max-width:760px){{h1{{font-size:32px}}.grid{{grid-template-columns:1fr 1fr}}header{{padding:28px}}}}@media print{{body{{background:white}}main{{max-width:none;padding:0}}header,.panel,.metric{{box-shadow:none}}details{{break-before:page}}}}
</style></head><body><main><header><div class="eyebrow">CASAN · Independent assurance dossier</div><h1>{escape(str(title))}</h1>
<div class="meta"><span>{escape(str(report.get("report_id", "")))}</span><span>Generated {escape(str(report.get("generated_at", "")))}</span></div><div class="badge">Verdict · {escape(verdict)}</div></header>
<section class="grid">{cards}</section><section class="panel"><h2>Decision evidence</h2><p class="sub">Threshold findings and control outcomes from canonical local evidence.</p>{rows}</section>
<section class="panel"><h2>Machine-auditable contract</h2><p class="sub">The complete sanitized report payload used to render this dossier.</p><details><summary>Open JSON evidence</summary><pre>{contract}</pre></details></section>
<footer>Generated on demand by CASAN Core Local Assurance Viewer · read-only · offline-capable</footer></main></body></html>"""
@@ -0,0 +1,295 @@
#!/usr/bin/env python3
"""Loopback-only HTTP server for the CASAN Core Local Assurance Viewer."""
from __future__ import annotations
import argparse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import mimetypes
import os
from pathlib import Path
import signal
import threading
from urllib.parse import parse_qs, unquote, urlparse
from local_report import (
SAFE_ID,
h6_report,
project_profile,
report_html,
run_report,
runs,
trace_graph,
)
ASSET_ROOT = Path(__file__).resolve().parents[2] / "assets" / "local-viewer"
MAX_RESPONSE_BYTES = 24 * 1024 * 1024
def atomic_json(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
temporary.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
try:
os.chmod(temporary, 0o600)
except OSError:
if os.name != "nt":
raise
os.replace(temporary, path)
class ViewerServer(ThreadingHTTPServer):
daemon_threads = True
allow_reuse_address = False
def __init__(self, address, root: Path, token: str):
self.root = root.resolve()
self.token = token
super().__init__(address, ViewerHandler)
class ViewerHandler(BaseHTTPRequestHandler):
server_version = "CASAN-Core-Viewer/1"
sys_version = ""
@property
def viewer(self) -> ViewerServer:
return self.server # type: ignore[return-value]
def log_message(self, fmt: str, *args) -> None:
# Never persist the session token from the initial/query download URL.
print(
"%s - - [%s] %s %s" % (
self.client_address[0],
self.log_date_time_string(),
self.command,
urlparse(self.path).path,
),
flush=True,
)
def _host_allowed(self) -> bool:
host = self.headers.get("Host", "").split(":", 1)[0].strip("[]").lower()
return host in {"127.0.0.1", "localhost", "::1"}
def _authorized(self, query: dict[str, list[str]]) -> bool:
supplied = self.headers.get("X-CASAN-Viewer-Token", "")
if not supplied:
supplied = query.get("token", [""])[0]
return bool(supplied) and supplied == self.viewer.token
def _headers(self, status: int, content_type: str, length: int) -> None:
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(length))
self.send_header("Cache-Control", "no-store")
self.send_header("Pragma", "no-cache")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Cross-Origin-Resource-Policy", "same-origin")
self.send_header(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'; "
"img-src 'self' data:; connect-src 'self'; object-src 'none'; "
"base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
)
self.end_headers()
def _send(self, status: int, body: bytes, content_type: str) -> None:
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_VIEW_RESPONSE_TOO_LARGE"})
return
self._headers(status, content_type, len(body))
if self.command != "HEAD":
self.wfile.write(body)
def _json(self, status: int, payload) -> None:
self._send(
status,
(json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8"),
"application/json; charset=utf-8",
)
def _asset(self, name: str) -> None:
safe_name = "index.html" if name in {"", "/"} else name.lstrip("/")
if safe_name not in {"index.html", "app.css", "app.js", "favicon.svg"}:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
return
path = ASSET_ROOT / safe_name
try:
body = path.read_bytes()
except OSError:
self._json(503, {"error": "CASAN_VIEW_ASSET_MISSING"})
return
content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
if content_type.startswith("text/") or content_type == "application/javascript":
content_type += "; charset=utf-8"
self._send(200, body, content_type)
def _download(self, report: dict, fmt: str, filename: str) -> None:
if fmt == "json":
body = (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
content_type = "application/json; charset=utf-8"
elif fmt == "html":
body = report_html(report, "h6" if report.get("harness") == "H6" else "run").encode("utf-8")
content_type = "text/html; charset=utf-8"
else:
self._json(400, {"error": "CASAN_REPORT_FORMAT_INVALID"})
return
if len(body) > MAX_RESPONSE_BYTES:
self._json(413, {"error": "CASAN_REPORT_TOO_LARGE"})
return
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Disposition", f'attachment; filename="{filename}.{fmt}"')
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Referrer-Policy", "no-referrer")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(body)
def do_HEAD(self) -> None:
self.do_GET()
def do_GET(self) -> None:
if self.client_address[0] not in {"127.0.0.1", "::1"} or not self._host_allowed():
self._json(403, {"error": "CASAN_VIEW_LOOPBACK_ONLY"})
return
parsed = urlparse(self.path)
path = unquote(parsed.path)
query = parse_qs(parsed.query, keep_blank_values=False)
if path in {"/", "/index.html", "/app.css", "/app.js"}:
self._asset(path)
return
if not self._authorized(query):
self._json(401, {"error": "CASAN_VIEW_TOKEN_REQUIRED"})
return
try:
if path == "/healthz":
self._json(200, {
"status": "ok",
"mode": "local_core",
"project_id": project_profile(self.viewer.root)["project_id"],
"pid": os.getpid(),
})
elif path == "/api/v1/project":
self._json(200, project_profile(self.viewer.root))
elif path == "/api/v1/runs":
self._json(200, runs(self.viewer.root, int(query.get("limit", ["100"])[0])))
elif path == "/api/v1/reports/h6":
report = self._h6(query)
self._json(200, report)
elif path == "/api/v1/reports/h6/export":
report = self._h6(query)
self._download(report, query.get("format", ["html"])[0], report["report_id"])
elif path == "/__shutdown":
self._json(200, {"status": "stopping"})
threading.Thread(target=self.viewer.shutdown, daemon=True).start()
elif path.startswith("/api/v1/runs/") and path.endswith("/graph"):
trace_id = path[len("/api/v1/runs/"):-len("/graph")].strip("/")
self._trace_required(trace_id)
self._json(200, trace_graph(self.viewer.root, trace_id))
elif path.startswith("/api/v1/reports/run/"):
suffix = path[len("/api/v1/reports/run/"):].strip("/")
exporting = suffix.endswith("/export")
trace_id = suffix[:-len("/export")].strip("/") if exporting else suffix
self._trace_required(trace_id)
report = run_report(self.viewer.root, trace_id)
if report["verdict"] == "not_found":
self._json(404, {"error": "CASAN_RUN_NOT_FOUND"})
elif exporting:
self._download(
report,
query.get("format", ["html"])[0],
report["report_id"],
)
else:
self._json(200, report)
else:
self._json(404, {"error": "CASAN_VIEW_NOT_FOUND"})
except (TypeError, ValueError):
self._json(400, {"error": "CASAN_VIEW_INVALID_QUERY"})
except BrokenPipeError:
return
except Exception as error: # fail closed without leaking project paths
self.log_error("request failed: %s", type(error).__name__)
self._json(500, {"error": "CASAN_VIEW_INTERNAL_ERROR"})
def _trace_required(self, trace_id: str) -> None:
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("unsafe trace id")
def _h6(self, query: dict[str, list[str]]) -> dict:
return h6_report(
self.viewer.root,
project=query.get("project", [None])[0],
run=query.get("run", [None])[0],
from_date=query.get("from", [None])[0],
to_date=query.get("to", [None])[0],
limit=int(query.get("limit", ["50"])[0]),
)
def do_POST(self) -> None:
self._json(405, {"error": "CASAN_VIEW_READ_ONLY"})
do_PUT = do_POST
do_PATCH = do_POST
do_DELETE = do_POST
def serve(root: Path, port: int, token: str, state_file: Path) -> int:
if not token or len(token) < 32:
raise ValueError("viewer token is too short")
server = ViewerServer(("127.0.0.1", port), root, token)
actual_port = server.server_address[1]
atomic_json(state_file, {
"schema_version": 1,
"pid": os.getpid(),
"port": actual_port,
"token": token,
"project_root": str(root.resolve()),
"started_at": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc
).isoformat().replace("+00:00", "Z"),
})
def stop(_signum, _frame):
threading.Thread(target=server.shutdown, daemon=True).start()
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
try:
server.serve_forever(poll_interval=0.25)
finally:
server.server_close()
try:
current = json.loads(state_file.read_text(encoding="utf-8"))
if current.get("pid") == os.getpid():
state_file.unlink()
except (OSError, ValueError):
pass
return 0
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", required=True)
parser.add_argument("--port", type=int, default=0)
parser.add_argument("--token", required=True)
parser.add_argument("--state-file", required=True)
args = parser.parse_args(argv)
return serve(
Path(args.root).expanduser().resolve(),
max(0, min(args.port, 65535)),
args.token,
Path(args.state_file).expanduser().resolve(),
)
if __name__ == "__main__":
raise SystemExit(main())
+303 -102
View File
@@ -1,102 +1,183 @@
#!/usr/bin/env python3
"""Read-only CASAN report discovery for Core installations.
Core owns the trace/evidence source of truth but not the Control Plane UI. This
helper exposes the latest materialized receipt and opens an enrolled dashboard
without generating HTML on the prompt hot path.
"""
"""CASAN Core report, export and Local Assurance Viewer commands."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import re
import secrets
import subprocess
import sys
import time
from urllib.error import URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
import webbrowser
from urllib.parse import quote
from local_report import (
SAFE_ID,
h6_report,
read_json,
report_html,
run_report,
)
def find_root(start):
current = os.path.abspath(start)
while current != os.path.dirname(current):
if os.path.isfile(os.path.join(current, ".casan", "config.json")):
return current
current = os.path.dirname(current)
return os.path.abspath(start)
def find_root(start: str) -> Path:
current = Path(start).expanduser().resolve()
for candidate in (current, *current.parents):
if (candidate / ".casan" / "config.json").is_file():
return candidate
return current
def read_json(path):
def latest_receipt(root: Path) -> dict:
return read_json(root / ".specify" / "state" / "latest-run.json")
def require_trace(value, receipt) -> str:
trace_id = str(value or receipt.get("trace_id") or "")
if not SAFE_ID.fullmatch(trace_id):
raise ValueError("no safe trace id was supplied and no latest run exists")
return trace_id
def viewer_state_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.json"
def viewer_log_path(root: Path) -> Path:
return root / ".specify" / "state" / "local-viewer.log"
def load_viewer_state(root: Path) -> dict:
state = read_json(viewer_state_path(root))
if state.get("project_root") != str(root.resolve()):
return {}
if not isinstance(state.get("port"), int) or not isinstance(state.get("token"), str):
return {}
return state
def health(state: dict, timeout: float = 0.7) -> dict:
if not state:
return {}
request = Request(
f"http://127.0.0.1:{state['port']}/healthz",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
with open(path, "r", encoding="utf-8") as handle:
value = json.load(handle)
return value if isinstance(value, dict) else {}
except (OSError, ValueError):
with urlopen(request, timeout=timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
return payload if response.status == 200 and isinstance(payload, dict) else {}
except (OSError, URLError, ValueError):
return {}
def dashboard_base(root):
configured = os.environ.get("CASAN_DASHBOARD_URL")
if not configured:
control_plane = read_json(
os.path.join(root, ".casan", "config.json")
).get("control_plane")
if isinstance(control_plane, dict):
configured = control_plane.get("dashboard_url")
base = str(configured or "").strip().rstrip("/")
return base if re.match(r"^https?://[a-zA-Z0-9]", base) else None
def ensure_viewer(root: Path, port: int = 0) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
state_path = viewer_state_path(root)
state_path.parent.mkdir(parents=True, exist_ok=True)
lock_path = state_path.with_suffix(".lock")
acquired = False
for _attempt in range(70):
try:
lock_path.mkdir()
acquired = True
break
except FileExistsError:
state = load_viewer_state(root)
if health(state):
return state
time.sleep(0.1)
if not acquired:
try:
if time.time() - lock_path.stat().st_mtime > 15:
lock_path.rmdir()
lock_path.mkdir()
acquired = True
except OSError:
pass
if not acquired:
raise RuntimeError("another viewer start is still in progress")
try:
return _start_viewer(root, port, state_path)
finally:
try:
lock_path.rmdir()
except OSError:
pass
def latest_receipt(root):
return read_json(
os.path.join(root, ".specify", "state", "latest-run.json")
)
def _start_viewer(root: Path, port: int, state_path: Path) -> dict:
existing = load_viewer_state(root)
if health(existing):
return existing
try:
state_path.unlink()
except OSError:
pass
log_path = viewer_log_path(root)
try:
if log_path.stat().st_size > 1024 * 1024:
rotated = log_path.with_suffix(".log.1")
try:
rotated.unlink()
except OSError:
pass
os.replace(log_path, rotated)
except OSError:
pass
token = secrets.token_urlsafe(32)
server = Path(__file__).with_name("local_viewer.py")
with log_path.open("ab", buffering=0) as log:
popen_options = {
"cwd": str(root),
"stdin": subprocess.DEVNULL,
"stdout": log,
"stderr": subprocess.STDOUT,
"close_fds": True,
}
if os.name == "nt":
popen_options["creationflags"] = (
getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
| getattr(subprocess, "DETACHED_PROCESS", 0)
)
else:
popen_options["start_new_session"] = True
subprocess.Popen(
[
sys.executable, str(server),
"--root", str(root),
"--port", str(port),
"--token", token,
"--state-file", str(state_path),
],
**popen_options,
)
for _attempt in range(50):
time.sleep(0.1)
state = load_viewer_state(root)
if state.get("token") == token and health(state):
return state
raise RuntimeError(f"viewer did not become ready; inspect {log_path}")
def require_trace(value, receipt):
trace_id = value or receipt.get("trace_id")
if not trace_id or not re.match(r"^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$", str(trace_id)):
raise ValueError("no safe trace id was supplied and no latest run exists")
return str(trace_id)
def viewer_url(state: dict, trace_id: str | None = None, view: str | None = None) -> str:
query = {"token": state["token"]}
if trace_id:
query.update({"view": "runs", "trace": trace_id})
elif view:
query["view"] = view
return f"http://127.0.0.1:{state['port']}/?{urlencode(query)}"
def report_url(root, trace_id):
base = dashboard_base(root)
if not base:
return None
return "%s/runs?trace=%s" % (base, quote(trace_id, safe=""))
def export_url(root, trace_id, export_format):
base = dashboard_base(root)
if not base:
return None
return "%s/api/v1/reports/run/%s/export?format=%s" % (
base,
quote(trace_id, safe=""),
export_format,
)
def print_receipt(receipt):
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
return 1
print("CASAN %s" % ("CERTIFIED" if receipt.get("certified") else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
if receipt.get("report_url"):
print("report_url=%s" % receipt["report_url"])
return 0
def open_or_print(url, no_open):
def open_or_print(url: str, no_open: bool) -> int:
print(url)
if no_open:
return 0
@@ -106,52 +187,172 @@ def open_or_print(url, no_open):
return 0
def main(argv=None):
parser = argparse.ArgumentParser(description="CASAN Core report discovery")
parser.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = parser.add_subparsers(dest="command", required=True)
def print_receipt(receipt: dict, root: Path) -> int:
if not receipt:
print("CASAN_REPORT_NONE — no finalized prompt receipt exists yet")
print("next=complete a prompt, then run `casan view`")
return 1
certified = receipt.get("certified") is True
print("CASAN %s" % ("CERTIFIED" if certified else "NON-CERTIFIED"))
print("trace_id=%s" % (receipt.get("trace_id") or "unknown"))
print("project_id=%s" % (receipt.get("project_id") or "unknown"))
print("h6_quality=%s" % (receipt.get("telemetry_quality") or "unknown"))
print("duration_ms=%s tool_calls=%s failures=%s" % (
receipt.get("duration_ms"),
receipt.get("tool_calls", 0),
receipt.get("failures", 0),
))
print("view_command=casan view %s" % (receipt.get("trace_id") or ""))
active_viewer = load_viewer_state(root)
if health(active_viewer):
print("local_report_url=%s" % viewer_url(
active_viewer, str(receipt.get("trace_id") or "") or None))
if receipt.get("report_url"):
print("central_report_url=%s" % receipt["report_url"])
return 0
def export_report(root: Path, args, receipt: dict) -> int:
if args.h6:
report = h6_report(
root,
project=args.project,
run=args.trace_id,
from_date=args.from_date,
to_date=args.to_date,
limit=args.limit,
)
stem = report["report_id"].lower()
kind = "h6"
else:
trace_id = require_trace(args.trace_id, receipt)
report = run_report(root, trace_id)
if report["verdict"] == "not_found":
print("CASAN_REPORT_NOT_FOUND — no evidence exists for that trace", file=sys.stderr)
return 2
stem = re.sub(r"[^a-zA-Z0-9._-]", "-", report["report_id"]).lower()
kind = "run"
destination = Path(args.output or f"{stem}.{args.format}").expanduser()
if not destination.is_absolute():
destination = Path.cwd() / destination
destination.parent.mkdir(parents=True, exist_ok=True)
if args.format == "json":
body = json.dumps(report, ensure_ascii=False, indent=2) + "\n"
else:
body = report_html(report, kind)
destination.write_text(body, encoding="utf-8")
print(str(destination.resolve()))
return 0
def viewer_command(root: Path, action: str, port: int, no_open: bool) -> int:
if action in {"start", "open"}:
try:
state = ensure_viewer(root, port)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(viewer_url(state), no_open or action == "start")
state = load_viewer_state(root)
status = health(state)
if action == "status":
if not status:
print("CASAN_VIEW_STOPPED")
return 1
print(json.dumps({
"status": "running",
"pid": state.get("pid"),
"url": viewer_url(state),
"project_id": status.get("project_id"),
"mode": "local_core",
}, indent=2))
return 0
if action == "stop":
if not status:
try:
viewer_state_path(root).unlink()
except OSError:
pass
print("CASAN_VIEW_ALREADY_STOPPED")
return 0
request = Request(
f"http://127.0.0.1:{state['port']}/__shutdown",
headers={"X-CASAN-Viewer-Token": state["token"]},
)
try:
urlopen(request, timeout=1).read()
except (OSError, URLError):
print("CASAN_VIEW_STOP_FAILED", file=sys.stderr)
return 3
for _attempt in range(30):
time.sleep(0.1)
if not health(state, timeout=0.1):
print("CASAN_VIEW_STOPPED")
return 0
print("CASAN_VIEW_STOP_PENDING")
return 0
return 64
def parser() -> argparse.ArgumentParser:
value = argparse.ArgumentParser(description=__doc__)
value.add_argument("--root", default=os.environ.get("CASAN_APP_ROOT") or os.getcwd())
commands = value.add_subparsers(dest="command", required=True)
latest = commands.add_parser("latest", help="show the latest finalized prompt receipt")
latest.add_argument("--json", action="store_true")
view = commands.add_parser("view", help="open a trace in the enrolled Control Plane")
view = commands.add_parser("view", help="open a trace in the local Core viewer")
view.add_argument("trace_id", nargs="?")
view.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
view.add_argument("--no-open", action="store_true")
view.add_argument("--port", type=int, default=0)
export = commands.add_parser("export", help="download a trace evidence snapshot on demand")
export = commands.add_parser("export", help="write a self-contained report on demand")
export.add_argument("trace_id", nargs="?")
export.add_argument("--h6", action="store_true", help="export the H6 dossier instead of a run dossier")
export.add_argument("--format", choices=["html", "json"], default="html")
export.add_argument("--no-open", action="store_true", help="print the URL without opening a browser")
export.add_argument("--output", "-o")
export.add_argument("--project")
export.add_argument("--from", dest="from_date")
export.add_argument("--to", dest="to_date")
export.add_argument("--limit", type=int, default=50)
args = parser.parse_args(argv)
viewer = commands.add_parser("viewer", help="manage the local Core viewer")
viewer.add_argument("action", choices=["start", "status", "stop", "open"])
viewer.add_argument("--port", type=int, default=0)
viewer.add_argument("--no-open", action="store_true")
return value
def main(argv=None) -> int:
args = parser().parse_args(argv)
root = find_root(args.root)
receipt = latest_receipt(root)
if args.command == "latest":
if args.json:
print(json.dumps(receipt, ensure_ascii=False, indent=2))
return 0 if receipt else 1
return print_receipt(receipt)
return print_receipt(receipt, root)
if args.command == "viewer":
return viewer_command(root, args.action, args.port, args.no_open)
if args.command == "export":
try:
return export_report(root, args, receipt)
except ValueError as error:
print(f"CASAN_REPORT_INVALID — {error}", file=sys.stderr)
return 2
try:
trace_id = require_trace(args.trace_id, receipt)
if args.trace_id and not SAFE_ID.fullmatch(str(args.trace_id)):
raise ValueError("unsafe trace id")
trace_id = str(args.trace_id or receipt.get("trace_id") or "") or None
state = ensure_viewer(root, args.port)
except ValueError as error:
print("CASAN_REPORT_NOT_FOUND — %s" % error, file=sys.stderr)
print(f"CASAN_REPORT_NOT_FOUND — {error}", file=sys.stderr)
return 2
url = (
report_url(root, trace_id)
if args.command == "view"
else export_url(root, trace_id, args.format)
)
if not url:
print(
"CASAN_CONTROL_PLANE_NOT_ENROLLED — run `casan init "
"--dashboard-url https://your-casan.example` or set CASAN_DASHBOARD_URL",
file=sys.stderr,
)
except RuntimeError as error:
print(f"CASAN_VIEW_START_FAILED — {error}", file=sys.stderr)
return 3
return open_or_print(url, args.no_open)
return open_or_print(viewer_url(state, trace_id), args.no_open)
if __name__ == "__main__":
@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
# Proves that the release artifact—not the source checkout—contains a complete,
# offline, read-only Local Assurance Viewer.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
WORK="$(mktemp -d)"
DIST="$WORK/dist"
PROJECT="$WORK/clean-project"
VIEWER_STARTED=0
cleanup() {
if [[ "$VIEWER_STARTED" == "1" && -n "${CASAN_BIN:-}" ]]; then
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop >/dev/null 2>&1 || true
fi
rm -rf "$WORK"
}
trap cleanup EXIT
CASAN_DIST_DIR="$DIST" bash "$ROOT/scripts/package-release.sh" core >/dev/null
ARTIFACT="$DIST/casan-core-v$(cat "$ROOT/VERSION").tar.gz"
tar -xzf "$ARTIFACT" -C "$WORK"
BUNDLE="$WORK/casan-core-v$(cat "$ROOT/VERSION")"
CASAN_BIN="$BUNDLE/bin/casan"
test -f "$BUNDLE/packages/casan-harness/assets/local-viewer/index.html"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_report.py"
test -f "$BUNDLE/packages/casan-harness/scripts/python/local_viewer.py"
test ! -e "$BUNDLE/packages/casan-control-panel"
test ! -e "$BUNDLE/package.json"
mkdir -p \
"$PROJECT/.casan" \
"$PROJECT/.specify/state" \
"$PROJECT/.specify/logs/trace" \
"$PROJECT/.specify/logs/trace-events" \
"$PROJECT/.specify/logs/cost"
python3 - "$PROJECT" <<'PY'
import json
from pathlib import Path
import sys
root = Path(sys.argv[1])
(root / ".casan/config.json").write_text(json.dumps({
"schema_version": "21.3",
"project_id": "artifact-project",
"project_name": "Artifact Project",
"edition": "core",
"enforcement_mode": "enforce",
}), encoding="utf-8")
trace_id = "artifact-run-1"
trace = {
"schema_version": "20.1",
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 1,
"failures": 0,
"evidence": [
{"h": f"H{i}", "at": f"2026-07-28T10:00:0{i}Z", "decision": "pass", "kind": "gate", "detail": f"H{i} passed"}
for i in range(1, 8)
],
}
(root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "artifact-project",
"harness": "H6-agentic",
"step": "agentic-turn",
"status": "success",
"latency_ms": 812,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": "insufficient",
"alerts": [],
}
(root / ".specify/logs/cost/metrics.jsonl").write_text(
json.dumps(metric) + "\n", encoding="utf-8")
(root / ".specify/state/latest-run.json").write_text(json.dumps({
"schema_version": 1,
"trace_id": trace_id,
"project_id": "artifact-project",
"certified": True,
"telemetry_quality": "insufficient",
"duration_ms": 812,
"tool_calls": 1,
"failures": 0,
"finalized_at": "2026-07-28T10:00:07Z",
}), encoding="utf-8")
PY
(
cd "$PROJECT"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report latest | grep -q "view_command=casan view artifact-run-1"
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export artifact-run-1 \
--format html --output "$WORK/run.html" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format json --output "$WORK/h6.json" >/dev/null
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" report export --h6 \
--format html --output "$WORK/h6.html" >/dev/null
)
grep -q "Independent assurance dossier" "$WORK/run.html"
grep -q "H6 · AgentOps Report" "$WORK/h6.html"
python3 - "$WORK/h6.json" <<'PY'
import json, sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
assert d["harness"] == "H6"
assert d["summary"]["runs"] == 1
assert d["summary"]["tokens"]["total"] is None
assert d["summary"]["cost_usd"]["estimated"] is None
PY
URL="$(CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" view artifact-run-1 --no-open)"
VIEWER_STARTED=1
python3 - "$URL" <<'PY'
import json
import sys
from urllib.error import HTTPError
from urllib.parse import parse_qs, urlparse
from urllib.request import Request, urlopen
url = urlparse(sys.argv[1])
token = parse_qs(url.query)["token"][0]
base = f"{url.scheme}://{url.netloc}"
def get(path):
request = Request(base + path, headers={"X-CASAN-Viewer-Token": token})
return json.loads(urlopen(request, timeout=3).read())
project = get("/api/v1/project")
assert project["project_id"] == "artifact-project"
assert project["viewer"] == {
"mode": "local_core",
"scope": "single_project",
"read_only": True,
"network": "loopback_only",
}
run = get("/api/v1/reports/run/artifact-run-1")
assert run["verdict"] == "certified"
assert run["summary"]["gates_observed"] == 7
h6 = get("/api/v1/reports/h6?run=artifact-run-1")
assert h6["summary"]["runs"] == 1
try:
urlopen(Request(base + "/api/v1/project"), timeout=3)
raise AssertionError("API accepted a request without the session token")
except HTTPError as error:
assert error.code == 401
try:
urlopen(Request(
base + "/api/v1/project",
method="POST",
headers={"X-CASAN-Viewer-Token": token},
), timeout=3)
raise AssertionError("read-only viewer accepted POST")
except HTTPError as error:
assert error.code == 405
try:
urlopen(Request(
base + "/api/v1/project",
headers={"X-CASAN-Viewer-Token": token, "Host": "attacker.example"},
), timeout=3)
raise AssertionError("viewer accepted a non-loopback Host header")
except HTTPError as error:
assert error.code == 403
PY
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard status | grep -q '"mode": "local_core"'
CASAN_APP_ROOT="$PROJECT" "$CASAN_BIN" dashboard stop | grep -q "CASAN_VIEW_STOPPED"
VIEWER_STARTED=0
echo "PASS: Core release artifact provides offline visual reports without Platform or Node"
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""Acceptance tests for the stdlib-only CASAN Core reporting boundary."""
from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
MODULE = Path(__file__).resolve().parents[1] / "scripts" / "python" / "local_report.py"
SPEC = importlib.util.spec_from_file_location("casan_local_report", MODULE)
REPORT = importlib.util.module_from_spec(SPEC)
assert SPEC.loader
SPEC.loader.exec_module(REPORT)
class LocalReportTests(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.root = Path(self.temporary.name)
(self.root / ".casan").mkdir()
(self.root / ".casan" / "config.json").write_text(json.dumps({
"project_id": "project-one",
"project_name": "Project One",
"edition": "core",
"enforcement_mode": "enforce",
"maturity": {"level": 4, "status": "assessed"},
}), encoding="utf-8")
for path in (
".specify/state",
".specify/logs/trace",
".specify/logs/trace-events",
".specify/logs/cost",
".specify/logs/level5",
".specify/agentops",
):
(self.root / path).mkdir(parents=True, exist_ok=True)
def tearDown(self):
self.temporary.cleanup()
def write_jsonl(self, relative, rows):
path = self.root / relative
path.write_text(
"".join(json.dumps(row, separators=(",", ":")) + "\n" for row in rows),
encoding="utf-8",
)
def write_run(self, trace_id="trace-1", quality="partial"):
trace = {
"trace_id": trace_id,
"project_id": "project-one",
"certified": True,
"certification_strength": "project_hook",
"finalized_at": "2026-07-28T10:00:07Z",
"tool_calls": 2,
"failures": 0,
"evidence": [
{"h": f"H{index}", "at": f"2026-07-28T10:00:0{index}Z", "decision": "pass", "kind": "gate", "detail": f"H{index} passed"}
for index in range(1, 8)
],
}
(self.root / f".specify/logs/trace/agentic-{trace_id}.json").write_text(
json.dumps(trace), encoding="utf-8")
metric = {
"timestamp": "2026-07-28T10:00:06Z",
"trace_id": trace_id,
"project_id": "project-one",
"step": "agentic-turn",
"status": "success",
"latency_ms": 1200,
"retry_count": 0,
"input_tokens": None,
"output_tokens": None,
"total_tokens": None,
"cost_estimate": None,
"telemetry_quality": quality,
"alerts": [],
}
self.write_jsonl(".specify/logs/cost/metrics.jsonl", [metric])
return metric
def test_core_empty_state_is_truthful(self):
registry = REPORT.runs(self.root)
h6 = REPORT.h6_report(self.root)
self.assertEqual(registry["count"], 0)
self.assertEqual(h6["verdict"], "no_data")
self.assertEqual(h6["summary"]["runs"], 0)
self.assertIsNone(h6["summary"]["tokens"]["total"])
self.assertTrue(any(item["code"] == "METRICS_MISSING" for item in h6["findings"]))
def test_run_reconstructs_all_gates_and_redacts_sensitive_fields(self):
self.write_run()
event = {
"timestamp": "2026-07-28T10:00:01Z",
"trace_id": "trace-1",
"gate_id": "H1-context",
"status": "pass",
"reason": "admitted",
"evidence": {"kind": "context", "prompt": "private customer prompt", "access_token": "secret"},
}
self.write_jsonl(".specify/logs/trace-events/trace-1.jsonl", [event])
report = REPORT.run_report(self.root, "trace-1")
self.assertEqual(report["verdict"], "certified")
self.assertEqual(report["summary"]["gates_observed"], 1)
self.assertEqual(report["gates"][0]["evidence"]["prompt"], "[redacted]")
self.assertEqual(report["gates"][0]["evidence"]["access_token"], "[redacted]")
self.assertIsNone(report["h6"]["total_tokens"])
def test_legacy_trace_reconstructs_h1_to_h7(self):
self.write_run()
graph = REPORT.trace_graph(self.root, "trace-1")
self.assertTrue(graph["terminal"])
self.assertEqual(graph["progress"], 7)
self.assertEqual([node["status"] for node in graph["nodes"]], [
"pass", "pass", "pass", "pass", "pass", "warning", "pass",
])
def test_h6_preserves_unknown_token_and_cost_as_null(self):
self.write_run()
report = REPORT.h6_report(self.root, run="trace-1")
self.assertEqual(report["summary"]["coverage"]["token_pct"], 0)
self.assertEqual(report["summary"]["coverage"]["cost_pct"], 0)
self.assertIsNone(report["summary"]["tokens"]["total"])
self.assertIsNone(report["summary"]["cost_usd"]["estimated"])
self.assertEqual(report["data_quality"]["status"], "insufficient")
self.assertIn("TELEMETRY_COVERAGE_GAP", report["verdict_reasons"])
def test_self_contained_html_escapes_evidence(self):
self.write_run()
report = REPORT.run_report(self.root, "trace-1")
report["gates"][0]["reason"] = "<script>alert(1)</script>"
html = REPORT.report_html(report, "run")
self.assertIn("<!doctype html>", html.lower())
self.assertNotIn("<script>alert(1)</script>", html)
self.assertIn("&lt;script&gt;alert(1)&lt;/script&gt;", html)
self.assertIn("Machine-auditable contract", html)
def test_unsafe_trace_id_never_reads_outside_project(self):
report = REPORT.run_report(self.root, "../../etc/passwd")
self.assertEqual(report["verdict"], "not_found")
self.assertFalse(report["source"]["trace_found"])
if __name__ == "__main__":
unittest.main()