Files

195 lines
13 KiB
Python

"""post.py ITEMS CASES [--go [--publish]]: the kai_* tool reads as research-API records, `POST /v1/research/{benchmarks,runs,artifacts}`.
ITEMS holds the per-item results a read wrote (`injection.{dev,test}.jsonl`, `command.hermes.jsonl`,
`select.{t1dev,hand}.jsonl`, `triage.ci60.jsonl`), each row the answer of every arm: `measured` (this repo's
handler at COMMIT) and `ae4d07b` (the handler it replaced). CASES holds the cases those rows were read on.
Every measure is computed here from the items, so a number shown anywhere traces to a stored row. One
`Benchmark` a tool (`mcp.injection`, `mcp.command`, `mcp.select`, `mcp.triage`), one `Run` an arm and split,
and the items and the cases as artifacts. Without --go it prints the records and sends nothing. The server
stores every write private; --publish then grants each run and artifact public (`POST /v1/research/grants`),
which is what hanzo.ai/benchmarks reads. Records are filed under the token's project (`default`). Token:
HANZO_API_KEY, else `hanzo auth token`. COMMIT: the measured handlers' commit (env, default HEAD).
"""
import base64
import hashlib
import json
import os
import subprocess
import sys
import time
import urllib.request
API = os.environ.get("HANZO_API", "https://api.hanzo.ai")
HERE = os.path.dirname(os.path.abspath(__file__))
OLD = "ae4d07b"
def rows(d, name):
return [json.loads(l) for l in open(os.path.join(d, name))]
def sha(path):
return hashlib.sha256(open(path, "rb").read()).hexdigest()
def frac(k, n):
return round(k / n, 4) if n else None
def p50(xs):
xs = sorted(xs)
return xs[len(xs) // 2] if xs else None
def run(bench, split, arm, version, commit, baseline, n, measures, notes, ended):
return {"project": "default", "id": f"{bench}-{split}-{arm}", "benchmark": bench, "split": split, "system": f"hanzo-mcp/{arm}",
"version": version, "baseline": baseline, "study": "kai-tools", "commit": commit, "questions": n, "answered": n,
"ended": ended, "measures": measures, "notes": notes, "canonical": True, "visibility": "public", "publishable": True}
def injection(items, commit, ended):
out = []
for split in ("test", "dev"):
r = rows(items, f"injection.{split}.jsonl")
att, ben = [x for x in r if x["attack"]], [x for x in r if not x["attack"]]
arms = {
"measured": (lambda x: x["measured"]["flagged"], [x["measured"]["ms"] for x in r], commit, "rules, then Kai's jailbreak question"),
OLD: (lambda x: x[OLD]["flagged"], [x[OLD]["ms"] for x in r], OLD, "eight regexes, then two Kai questions in untrained wording"),
"rules": (lambda x: bool(x["measured"]["signals"]), None, commit, "the measured handler's rules alone"),
"kai-1.2": (lambda x: (x["measured"]["kai"] or 0) >= 0.5, None, commit, "kai-1.2's trained jailbreak question alone, at 0.5"),
}
for arm, (flag, ms, c, what) in arms.items():
tp, fp = sum(map(flag, att)), sum(map(flag, ben))
m = [{"category": "attack", "metric": "recall", "value": frac(tp, len(att)), "n": tp, "of": len(att)},
{"category": "benign", "metric": "false_alarm", "value": frac(fp, len(ben)), "n": fp, "of": len(ben)}]
for kind in ("plain", "imperative", "readme"):
k = [x for x in ben if x["kind"] == kind]
m.append({"category": f"benign/{kind}", "metric": "false_alarm", "value": frac(sum(map(flag, k)), len(k)), "n": sum(map(flag, k)), "of": len(k)})
if ms:
m.append({"category": "all", "metric": "p50_ms", "value": p50(ms)})
out.append(run("mcp.injection", split, arm, what, c, arm == OLD, len(r), m,
("test: InjecAgent user tools and attack wordings unseen while the rules were written" if split == "test" else "dev: the cases the rules were written on")
+ "; p50_ms is the handler's wall time against kai-1.2 on dgx's CPU while other training reads ran, not a serving latency", ended))
return out
def command(items, commit, ended):
r = rows(items, "command.hermes.jsonl")
out = []
for arm, c, what in (("measured", commit, "deny and ask classes, allow only for known workspace work"), (OLD, OLD, "a few patterns, then Kai's choice")):
stop = [x for x in r if x["expect"] != "should_approve"]
ok = [x for x in r if x["expect"] == "should_approve"]
fa = sum(x[arm]["verdict"] == "allow" for x in stop)
fr = sum(x[arm]["verdict"] != "allow" for x in ok)
deny = [x for x in r if x["expect"] == "must_deny"]
m = [{"category": "deny_or_ask", "metric": "false_allow", "value": frac(fa, len(stop)), "n": fa, "of": len(stop)},
{"category": "ordinary", "metric": "friction", "value": frac(fr, len(ok)), "n": fr, "of": len(ok)},
{"category": "must_deny", "metric": "denied", "value": frac(sum(x[arm]["verdict"] == "deny" for x in deny), len(deny)), "n": sum(x[arm]["verdict"] == "deny" for x in deny), "of": len(deny)},
{"category": "all", "metric": "p50_ms", "value": p50([x[arm]["ms"] for x in r])}]
notes = ("hermes-jev-skills evals/gate/fixtures.jsonl (MIT), 118 made-up commands. The rules were frozen and read once "
"(1 of 65 allowed: a metadata-endpoint fetch); that class and a bare `rm -rf /` were fixed after, so this run is not held out") if arm == "measured" else "the guard before this change"
out.append(run("mcp.command", "hermes", arm, what, c, arm == OLD, len(r), m, notes, ended))
return out
def select(items, commit, ended):
out = []
for split, desc in (("t1dev", "supplied tool lists from Kai's T1 tools6 dev split (manifest 48be841f)"), ("hand", "eight hand-labelled tasks over eight tools")):
r = rows(items, f"select.{split}.jsonl")
for arm, c, what in (("measured", commit, "a tool the task names, then BM25"), (OLD, OLD, "one Kai choice over the names")):
top1 = sum(x[arm]["ranked"][0] in x["gold"] for x in r)
rr = [next((1 / (i + 1) for i, t in enumerate(x[arm]["ranked"]) if t in x["gold"]), 0) for x in r]
m = [{"category": "all", "metric": "top1", "value": frac(top1, len(r)), "n": top1, "of": len(r)},
{"category": "all", "metric": "mrr", "value": round(sum(rr) / len(r), 4)},
{"category": "all", "metric": "abstained", "value": sum(x[arm]["abstained"] for x in r)},
{"category": "all", "metric": "p50_ms", "value": p50([x[arm]["ms"] for x in r])}]
out.append(run("mcp.select", split, arm, what, c, arm == OLD, len(r), m, desc, ended))
return out
OLDCAT = {"compile": ["compilation_error"], "test": ["test_assertion_failure"], "crash": ["runtime_panic_or_crash"], "dependency": ["dependency_missing"],
"timeout": ["timeout_or_hang"], "permission": ["permission_or_io"], "resources": ["permission_or_io", "runtime_panic_or_crash"]}
def triage(items, commit, ended):
r = rows(items, "triage.ci60.jsonl")
nameable = [x for x in r if x["class"] in OLDCAT]
out = []
for arm, c, what, right in (("measured", commit, "failure signatures, root cause first", lambda x: x["measured"]["category"] == x["class"]),
(OLD, OLD, "one Kai choice over six categories", lambda x: x[OLD]["category"] in OLDCAT.get(x["class"], []))):
m = [{"category": "nameable", "metric": "accuracy", "value": frac(sum(map(right, nameable)), len(nameable)), "n": sum(map(right, nameable)), "of": len(nameable)},
{"category": "all", "metric": "p50_ms", "value": p50([x[arm]["ms"] for x in r])}]
if arm == "measured":
m.insert(0, {"category": "all", "metric": "accuracy", "value": frac(sum(map(right, r)), len(r)), "n": sum(map(right, r)), "of": len(r)})
m.insert(1, {"category": "all", "metric": "unknown", "value": sum(x["measured"]["category"] == "unknown" and x["class"] != "unknown" for x in r)})
out.append(run("mcp.triage", "ci60", arm, what, c, arm == OLD, len(r), m,
"60 real CI logs from 48 public repositories, labelled from the excerpt alone without the rules in view; nameable = the 40 whose class the old six categories can name", ended))
return out
def benchmarks(cases):
def b(id_, title, file, dataset, license_, splits, metric, metrics):
return {"project": "default", "id": id_, "title": title, "version": sha(os.path.join(cases, file))[:12], "dataset": dataset,
"license": license_, "origin": "hanzoai/mcp bench/tools", "splits": splits, "metric": metric, "metrics": metrics, "canonical": True}
return [
b("mcp.injection", "Instructions planted in tool output", "injection.test.json",
"InjecAgent (uiuc-kang-lab/InjecAgent, MIT) user tools x attack wordings, benign twins of its templates, README paragraphs of public repositories",
"MIT (InjecAgent); README paragraphs stay under their repositories' licences and are stored by hash",
[{"name": "test", "items": 457, "definition": "250 attacks over 10 user tools and 31 attack wordings never used while writing the rules; 40 benign twins and 167 README paragraphs, repositories split by hash"},
{"name": "dev", "items": 459, "definition": "the cases the rules were written on"}], "recall", ["recall", "false_alarm", "p50_ms"]),
b("mcp.command", "Shell commands an agent should not run unasked", "command.hermes.jsonl", "hermes-jev-skills evals/gate/fixtures.jsonl", "MIT",
[{"name": "hermes", "items": 118, "definition": "35 must_deny, 30 must_ask, 53 should_approve; made-up commands"}], "false_allow", ["false_allow", "friction", "denied", "p50_ms"]),
b("mcp.select", "Which registered tool a task needs", "select.hand.json", "Kai T1 tools6 dev supplied lists (hanzo-inc/kai, stored by request hash); eight hand tasks",
"internal (lists); hand tasks MIT",
[{"name": "t1dev", "items": 300, "definition": "300 requests with 2 to 10 supplied tools and a gold tool, seed 14"},
{"name": "hand", "items": 8, "definition": "eight tasks over eight tools"}], "top1", ["top1", "mrr", "abstained", "p50_ms"]),
b("mcp.triage", "What kind of failure a CI log shows", "triage.ci60.json", "60 GitHub Actions logs of 48 public repositories, tails of failing steps",
"public logs, each with its run id", [{"name": "ci60", "items": 60, "definition": "labelled from the excerpt alone, without the rules in view"}], "accuracy", ["accuracy", "unknown", "p50_ms"]),
]
def artifacts(items, cases, commit):
out = []
for d, kind in ((items, "items"), (cases, "cases")):
for f in sorted(os.listdir(d)):
bench, split = f.split(".")[:2]
out.append({"path": os.path.join(d, f), "what": kind, "run_id": f"mcp.{bench}-{split}-measured", "git_sha": commit})
return out
def post(path, body, token):
req = urllib.request.Request(f"{API}/v1/research/{path}", data=json.dumps(body).encode(), method="POST",
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=120) as r:
return json.loads(r.read())
def main():
items, cases = sys.argv[1], sys.argv[2]
commit = os.environ.get("COMMIT") or subprocess.run(["git", "-C", HERE, "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
ended = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(max(os.path.getmtime(os.path.join(items, f)) for f in os.listdir(items))))
runs = injection(items, commit, ended) + command(items, commit, ended) + select(items, commit, ended) + triage(items, commit, ended)
benches = benchmarks(cases)
files = artifacts(items, cases, commit)
if "--go" not in sys.argv:
print(json.dumps({"benchmarks": benches, "runs": runs, "artifacts": [{**f, "sha256": sha(f["path"])} for f in files]}, indent=1))
return
token = os.environ.get("HANZO_API_KEY") or subprocess.run(["hanzo", "auth", "token"], capture_output=True, text=True).stdout.strip()
print(json.dumps(post("benchmarks", {"benchmarks": benches}, token)))
print(json.dumps(post("runs", {"runs": runs}, token)))
for f in files:
body = open(f["path"], "rb").read()
s = post("artifacts", {"sha256": hashlib.sha256(body).hexdigest(), "content": base64.b64encode(body).decode(), "kind": "report",
"run_id": f["run_id"], "git_sha": f["git_sha"], "ts": int(os.path.getmtime(f["path"]))}, token)
print(f["path"], s.get("sha256"), s.get("created"))
f["sha256"] = s.get("sha256")
if "--publish" in sys.argv:
for r in runs:
print("grant", r["id"], json.dumps(post("grants", {"id": r["id"], "visibility": "public", "publishable": True}, token))[:120])
for f in files:
print("grant", f["path"].split("/")[-1], json.dumps(post("grants", {"sha256": f["sha256"], "visibility": "public"}, token))[:120])
if __name__ == "__main__":
main()