# aggregate.py: results.jsonl -> aggregate.json + agent-permissions-2026.csv (aggregates only).
import json, sys, csv, collections, re
S, SNAP = sys.argv[1], sys.argv[2]
rows = [json.loads(l) for l in open(f"{S}/results.jsonl")]
ok = [r for r in rows if not r.get("parse_error") and not r.get("scan_error")]
KINDS = ["root-mcp", "cursor", "claude-desktop", "plain-mcp", "claude-settings", "codex", "gemini"]
LABEL = {"root-mcp": "Project .mcp.json", "cursor": "Cursor .cursor/mcp.json", "claude-desktop": "Claude Desktop config", "plain-mcp": "Other mcp.json",
         "claude-settings": "Claude Code .claude/settings.json", "codex": "Codex .codex/config.toml", "gemini": "Gemini CLI .gemini/settings.json"}
def pct(a, b): return round(100 * a / b, 1) if b else 0.0
out = {"snapshot": SNAP, "fetched": len(rows), "graded": len(ok), "parse_errors": sum(1 for r in rows if r.get("parse_error")),
       "scan_errors": sum(1 for r in rows if r.get("scan_error")), "deedglass_version": "0.11.0", "metrics": [], "kinds": {}, "grades": {}, "servers": [], "cred_types": {}}
M = out["metrics"]
def metric(key, label, seg, pred, pop):
    n = sum(1 for r in pop if pred(r)); d = len(pop)
    M.append({"metric": key, "label": label, "segment": seg, "numerator": n, "denominator": d, "percent": pct(n, d)})
    return n, d
mcp = [r for r in ok if r.get("servers_n", 0) > 0]
cs = [r for r in ok if r["kind"] == "claude-settings"]
segs = [("all", ok), ("with_mcp_servers", mcp)] + [(k, [r for r in ok if r["kind"] == k]) for k in KINDS]
for seg, pop in segs:
    if not pop: continue
    metric("db_write", "Database MCP server with no read-only setting", seg, lambda r: r.get("db_write"), pop)
    metric("shell_unguarded", "Shell access with no guard", seg, lambda r: r.get("shell_unguarded"), pop)
    metric("plaintext_credential", "Plaintext credential present", seg, lambda r: bool(r.get("cred_types")), pop)
    metric("any_of_three", "Any of the three above", seg, lambda r: r.get("db_write") or r.get("shell_unguarded") or bool(r.get("cred_types")), pop)
    metric("grade_d_or_f", "Deedglass scan grade D or F", seg, lambda r: r["grade"] in ("D", "F"), pop)
    metric("grade_below_a", "Deedglass scan grade below A", seg, lambda r: r["grade"] != "A", pop)
    metric("write_path_unchecked", "At least one write path with no check", seg, lambda r: r.get("unchecked", 0) > 0, pop)
metric("no_pretool_hook", "No PreToolUse hook", "claude-settings", lambda r: not r.get("pretool_hook"), cs)
metric("no_hooks", "No hooks of any kind", "claude-settings", lambda r: not r.get("hooks_any"), cs)
metric("no_ask_rules", "No ask rules", "claude-settings", lambda r: r.get("ask_n", 0) == 0, cs)
metric("no_deny_rules", "No deny rules", "claude-settings", lambda r: r.get("deny_n", 0) == 0, cs)
metric("broad_bash_allow", "Blanket or interpreter-wide Bash allow rule", "claude-settings", lambda r: r.get("broad_bash"), cs)
metric("bypass_mode", "Default mode bypassPermissions", "claude-settings", lambda r: r.get("bypass_mode"), cs)
metric("all_project_mcp", "enableAllProjectMcpServers on", "claude-settings", lambda r: r.get("mcp_all_enabled"), cs)
cs_perm = [r for r in cs if r.get("has_permissions")]
metric("no_pretool_hook_with_rules", "No PreToolUse hook, among files with permission rules", "claude-settings-with-rules", lambda r: not r.get("pretool_hook"), cs_perm)
cx = [r for r in ok if r["kind"] == "codex"]
metric("codex_danger_full_access", "sandbox_mode danger-full-access", "codex", lambda r: r.get("codex_danger_full_access"), cx)
metric("codex_approval_never", "approval_policy never", "codex", lambda r: r.get("codex_approval_never"), cx)
gm = [r for r in ok if r["kind"] == "gemini"]
metric("gemini_yolo", "YOLO approval mode", "gemini", lambda r: r.get("gemini_yolo"), gm)
for k in KINDS:
    pop = [r for r in ok if r["kind"] == k]
    out["kinds"][k] = {"label": LABEL[k], "n": len(pop), "with_servers": sum(1 for r in pop if r.get("servers_n", 0) > 0)}
for seg, pop in (("all", ok), ("with_mcp_servers", mcp)):
    c = collections.Counter(r["grade"] for r in pop)
    out["grades"][seg] = {g: c.get(g, 0) for g in "ABCDF"}
# servers: distinct files per server id, risky categories only; ids that could name a person or a local path are left out
RISKY = ("database", "shell", "filesystem", "browser", "cloud", "payments")
cnt, catof, ro = collections.Counter(), {}, collections.Counter()
allcnt = collections.Counter()
for r in ok:
    seen = set()
    for s in r.get("servers") or []:
        if s["id"].startswith(("local:", "name:")) or s["id"] in ("docker", "remote"): continue
        if s["id"] in seen: continue
        seen.add(s["id"]); allcnt[s["id"]] += 1
        rc = [c for c in s["cats"] if c in RISKY]
        if rc:
            cnt[s["id"]] += 1; catof[s["id"]] = rc[0]
            if s["ro"]: ro[s["id"]] += 1
out["servers_total_files_with_servers"] = len(mcp)
out["servers"] = [{"id": i, "category": catof[i], "files": n, "read_only_files": ro[i]} for i, n in cnt.most_common(60)]
out["servers_any_top"] = [{"id": i, "files": n} for i, n in allcnt.most_common(40)]
cat_files = collections.Counter()
for r in mcp:
    cs_ = {c for s in r.get("servers") or [] for c in s["cats"] if c in RISKY}
    for c in cs_: cat_files[c] += 1
out["category_files"] = dict(cat_files)
# curated, named groups for the page: public products only, each a count of distinct files
GROUPS = [
 ("Playwright MCP", "browser", lambda i: i in ("@playwright/mcp", "@executeautomation/playwright-mcp-server")),
 ("Supabase MCP (hosted or npm)", "database", lambda i: i in ("remote:mcp.supabase.com", "@supabase/mcp-server-supabase", "@supabase/mcp-server")),
 ("Filesystem reference server", "filesystem", lambda i: i in ("@modelcontextprotocol/server-filesystem", "mcp-server-filesystem")),
 ("Chrome DevTools MCP", "browser", lambda i: i == "chrome-devtools-mcp"),
 ("Postgres reference server (archived)", "database", lambda i: i == "@modelcontextprotocol/server-postgres"),
 ("Cloudflare MCP servers (not docs)", "cloud", lambda i: i.startswith("remote:") and i.endswith("mcp.cloudflare.com") and "docs." not in i),
 ("Vercel MCP", "cloud", lambda i: i == "remote:mcp.vercel.com"),
 ("Puppeteer reference server", "browser", lambda i: i == "@modelcontextprotocol/server-puppeteer"),
 ("Neon MCP (hosted or npm)", "database", lambda i: i in ("remote:mcp.neon.tech", "@neondatabase/mcp-server-neon")),
 ("Firebase CLI MCP", "database", lambda i: i == "firebase-tools"),
 ("Terraform MCP", "cloud", lambda i: i == "docker:hashicorp/terraform-mcp-server"),
 ("Desktop Commander (shell and files)", "shell", lambda i: i.endswith("desktop-commander")),
 ("Stripe MCP", "payments", lambda i: i in ("remote:mcp.stripe.com", "@stripe/mcp")),
 ("SQLite reference servers", "database", lambda i: i in ("@modelcontextprotocol/server-sqlite", "mcp-server-sqlite", "mcp-server-sqlite-npx")),
]
grp = []
for label, cat, f in GROUPS:
    files = rofiles = 0
    for r in ok:
        hit = [s for s in r.get("servers") or [] if f(s["id"])]
        if hit:
            files += 1
            if all(s["ro"] for s in hit): rofiles += 1
    grp.append({"label": label, "category": cat, "files": files, "read_only_files": rofiles})
out["server_groups"] = sorted(grp, key=lambda g: -g["files"])
a_mcp = [r for r in mcp if r["grade"] == "A"]
metric("a_grade_with_risk", "Graded A but with one of the three risks", "with_mcp_servers_graded_a", lambda r: r.get("db_write") or r.get("shell_unguarded") or bool(r.get("cred_types")), a_mcp)
metric("db_write_excl_pg_ref", "Database server with no read-only setting, not counting the archived Postgres reference server", "with_mcp_servers",
       lambda r: any("database" in s["cats"] and not s["ro"] and s["id"] != "@modelcontextprotocol/server-postgres" for s in r.get("servers") or []), mcp)
ct = collections.Counter(t for r in ok for t in r.get("cred_types") or [])
out["cred_types"] = dict(ct.most_common())
json.dump(out, open(f"{S}/aggregate.json", "w"), indent=1)
with open(f"{S}/agent-permissions-2026.csv", "w", newline="") as f:
    w = csv.writer(f); w.writerow(["snapshot", "metric", "label", "segment", "numerator", "denominator", "percent"])
    for m in M: w.writerow([SNAP, m["metric"], m["label"], m["segment"], m["numerator"], m["denominator"], m["percent"]])
    for seg, g in out["grades"].items():
        tot = sum(g.values())
        for k, v in g.items(): w.writerow([SNAP, "grade_" + k.lower(), f"Deedglass scan grade {k}", seg, v, tot, pct(v, tot)])
    for k, v in out["kinds"].items(): w.writerow([SNAP, "files", "Files in the sample", k, v["n"], out["graded"], pct(v["n"], out["graded"])])
    for k, v in out["category_files"].items(): w.writerow([SNAP, "category_" + k, f"Configures a {k} MCP server", "with_mcp_servers", v, len(mcp), pct(v, len(mcp))])
    for g in out["server_groups"]: w.writerow([SNAP, "server_group", g["label"] + " (" + g["category"] + ")", "with_mcp_servers", g["files"], len(mcp), pct(g["files"], len(mcp))])
    for k, v in out["cred_types"].items(): w.writerow([SNAP, "credential_type_" + k, f"Plaintext credential type: {k}", "all", v, len(ok), pct(v, len(ok))])
print(json.dumps({k: out[k] for k in ("fetched", "graded", "parse_errors", "scan_errors", "kinds", "grades", "category_files", "cred_types")}, indent=0))
for m in M:
    if m["segment"] in ("all", "with_mcp_servers", "claude-settings", "codex", "gemini", "claude-settings-with-rules"): print(m["metric"], m["segment"], m["numerator"], m["denominator"], m["percent"])
