feat: ship production local assurance viewer in core
This commit is contained in:
Executable → Regular
+303
-102
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user