use std::path::Path;
use sharpebench_arena::{verify_arena, Arena, RevealedEntry, SigningKey, VerifyingKey};
pub fn run(args: &[String], json: bool) -> i32 {
match args.get(2).map(String::as_str) {
Some("init") => cmd_init(args, json),
Some("open") => cmd_open(args, json),
Some("commit") => cmd_commit(args, json),
Some("advance") => cmd_advance(args, json),
Some("score") => cmd_score(args, json),
Some("publish") => cmd_publish(args, json),
Some("verify") => cmd_verify(args, json),
_ => {
usage();
2
}
}
}
fn usage() {
eprintln!("usage: sharpebench arena <subcommand> [--json]");
eprintln!(" arena init <dir> create an arena directory");
eprintln!(" arena open <dir> <window> <commit_deadline> <reveal_epoch> [--config <score_config.json>]");
eprintln!(" open a window; the scoring rules are fixed now");
eprintln!(" arena commit <dir> <window> <commitment.json> register a pre-deadline commitment (from `sharpebench commit`)");
eprintln!(" arena advance <dir> <epoch> advance the epoch (operator/cron/CI supplies \"now\")");
eprintln!(" arena score <dir> <window> <dataset> <entries.json> verify reveals, refuse mismatches, rank the rest");
eprintln!(" arena publish <dir> <window> <key> sign + write the window's Ed25519 board");
eprintln!(" arena verify <dir> [--pubkey <hex>] verify every published board + the cross-window chain");
eprintln!("\n<key> and --pubkey accept a literal, or env:NAME / file:PATH to keep secrets out of process listings.");
}
fn cmd_init(args: &[String], json: bool) -> i32 {
let Some(dir) = args.get(3) else {
eprintln!("usage: sharpebench arena init <dir>");
return 2;
};
match Arena::init(Path::new(dir)) {
Ok(_) => {
if json {
emit_json(&serde_json::json!({ "ok": true, "dir": dir }));
} else {
println!("initialized arena at {dir}");
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_open(args: &[String], json: bool) -> i32 {
let (Some(dir), Some(window), Some(deadline), Some(reveal)) =
(args.get(3), args.get(4), args.get(5), args.get(6))
else {
eprintln!(
"usage: sharpebench arena open <dir> <window> <commit_deadline> <reveal_epoch> [--config <score_config.json>]"
);
return 2;
};
let (Ok(deadline), Ok(reveal)) = (deadline.parse::<u64>(), reveal.parse::<u64>()) else {
eprintln!("error: commit_deadline and reveal_epoch must be non-negative integers");
return 2;
};
let config = match flag_value(args, "--config") {
Some(path) => match std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {path}: {e}"))
.and_then(|t| {
serde_json::from_str::<sharpebench_core::ScoreConfig>(&t)
.map_err(|e| format!("invalid ScoreConfig JSON in {path}: {e}"))
}) {
Ok(c) => c,
Err(e) => return fail(&e, json),
},
None => sharpebench_core::ScoreConfig::default(),
};
let mut arena = match Arena::load(Path::new(dir)) {
Ok(a) => a,
Err(e) => return fail(&e, json),
};
match arena.open_window(window, deadline, reveal, config) {
Ok(()) => {
if json {
emit_json(&serde_json::json!({
"ok": true,
"window": window,
"commit_deadline": deadline,
"data_reveal_epoch": reveal,
}));
} else {
println!(
"opened window `{window}` (commit deadline: epoch {deadline}, data reveal: epoch {reveal}); scoring rules recorded"
);
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_commit(args: &[String], json: bool) -> i32 {
let (Some(dir), Some(window), Some(commitment_path)) = (args.get(3), args.get(4), args.get(5))
else {
eprintln!("usage: sharpebench arena commit <dir> <window> <commitment.json>");
return 2;
};
let commitment: sharpebench_attest::Commitment = match std::fs::read_to_string(commitment_path)
.map_err(|e| format!("cannot read {commitment_path}: {e}"))
.and_then(|t| {
serde_json::from_str(&t)
.map_err(|e| format!("invalid commitment JSON in {commitment_path}: {e}"))
}) {
Ok(c) => c,
Err(e) => return fail(&e, json),
};
let mut arena = match Arena::load(Path::new(dir)) {
Ok(a) => a,
Err(e) => return fail(&e, json),
};
let agent_id = commitment.agent_id.clone();
match arena.register_entry(window, commitment) {
Ok(()) => {
if json {
emit_json(
&serde_json::json!({ "ok": true, "window": window, "agent_id": agent_id }),
);
} else {
println!("registered commitment for `{agent_id}` in window `{window}`");
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_advance(args: &[String], json: bool) -> i32 {
let (Some(dir), Some(epoch)) = (args.get(3), args.get(4)) else {
eprintln!("usage: sharpebench arena advance <dir> <epoch>");
return 2;
};
let Ok(epoch) = epoch.parse::<u64>() else {
eprintln!("error: epoch must be a non-negative integer");
return 2;
};
let mut arena = match Arena::load(Path::new(dir)) {
Ok(a) => a,
Err(e) => return fail(&e, json),
};
match arena.advance(epoch) {
Ok(()) => {
let committed: Vec<&String> = arena
.window_ids()
.iter()
.filter(|id| {
arena.window(id).map(|w| w.status)
== Some(sharpebench_arena::WindowStatus::Committed)
})
.collect();
if json {
emit_json(
&serde_json::json!({ "ok": true, "epoch": epoch, "committed_windows": committed }),
);
} else {
println!("advanced to epoch {epoch}");
for id in committed {
println!(" window `{id}` is past its commit deadline (committed)");
}
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_score(args: &[String], json: bool) -> i32 {
let (Some(dir), Some(window), Some(dataset), Some(entries_path)) =
(args.get(3), args.get(4), args.get(5), args.get(6))
else {
eprintln!("usage: sharpebench arena score <dir> <window> <dataset> <entries.json>");
return 2;
};
let entries: Vec<RevealedEntry> = match std::fs::read_to_string(entries_path)
.map_err(|e| format!("cannot read {entries_path}: {e}"))
.and_then(|t| {
serde_json::from_str(&t)
.map_err(|e| format!("invalid entries JSON in {entries_path}: {e}"))
}) {
Ok(v) => v,
Err(e) => return fail(&e, json),
};
let mut arena = match Arena::load(Path::new(dir)) {
Ok(a) => a,
Err(e) => return fail(&e, json),
};
match arena.reveal_and_score(window, Path::new(dataset), &entries) {
Ok(scores) => {
let refusals = arena
.window(window)
.map(|w| w.refusals.clone())
.unwrap_or_default();
if json {
emit_json(&serde_json::json!({
"ok": true,
"window": window,
"scored": scores.len(),
"refused": refusals,
"board": scores,
}));
} else {
println!(
"scored window `{window}`: {} entries ranked, {} refused",
scores.len(),
refusals.len()
);
for r in &refusals {
println!(" refused `{}`: {}", r.agent_id, r.reason);
}
print!("{}", sharpebench_leaderboard::render(&scores));
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_publish(args: &[String], json: bool) -> i32 {
let (Some(dir), Some(window), Some(key_spec)) = (args.get(3), args.get(4), args.get(5)) else {
eprintln!("usage: sharpebench arena publish <dir> <window> <key>");
return 2;
};
let key = match resolve_key(key_spec) {
Ok(secret) => SigningKey::derive(&secret),
Err(e) => return fail(&e.to_string(), json),
};
let mut arena = match Arena::load(Path::new(dir)) {
Ok(a) => a,
Err(e) => return fail(&e, json),
};
match arena.publish(window, &key) {
Ok(path) => {
let vk = key.verifying_key().to_hex();
if json {
emit_json(&serde_json::json!({
"ok": true,
"window": window,
"board": path.display().to_string(),
"verifying_key": vk,
}));
} else {
println!("published window `{window}` -> {}", path.display());
println!("verifying key (publish this): {vk}");
}
0
}
Err(e) => fail(&e, json),
}
}
fn cmd_verify(args: &[String], json: bool) -> i32 {
let Some(dir) = args.get(3) else {
eprintln!("usage: sharpebench arena verify <dir> [--pubkey <hex>]");
return 2;
};
let pinned = match flag_value(args, "--pubkey") {
Some(spec) => match resolve_key(spec) {
Ok(bytes) => {
let hex = String::from_utf8_lossy(&bytes).trim().to_string();
match VerifyingKey::from_hex(&hex) {
Some(vk) => Some(vk),
None => {
eprintln!("error: --pubkey is not a valid 64-hex-char Ed25519 key");
return 1;
}
}
}
Err(e) => return fail(&e.to_string(), json),
},
None => None,
};
match verify_arena(Path::new(dir), pinned.as_ref()) {
Ok(report) => {
if json {
emit_json(&report);
} else if report.ok {
println!(
"OK - {} published window(s), every board and the cross-window chain valid under {} key {}",
report.windows.len(),
if pinned.is_some() { "pinned" } else { "embedded" },
report.verifying_key.as_deref().unwrap_or("(none)")
);
} else {
eprintln!("FAIL - the arena chain does not verify:");
for w in &report.windows {
eprintln!(
" {} chain={} anchor={} key={} {}",
w.window_id, w.chain_ok, w.anchor_ok, w.key_ok, w.detail
);
}
}
i32::from(!report.ok)
}
Err(e) => fail(&e, json),
}
}
fn fail(message: &str, json: bool) -> i32 {
if json {
emit_json(&serde_json::json!({ "ok": false, "error": message }));
} else {
eprintln!("error: {message}");
}
1
}
fn emit_json<T: serde::Serialize>(value: &T) {
match serde_json::to_string_pretty(value) {
Ok(j) => println!("{j}"),
Err(e) => eprintln!("error: serializing output: {e}"),
}
}
fn flag_value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.map(String::as_str)
}
fn resolve_key(spec: &str) -> std::io::Result<Vec<u8>> {
if let Some(var) = spec.strip_prefix("env:") {
std::env::var(var)
.map(String::into_bytes)
.map_err(|_| std::io::Error::other(format!("env var {var} is not set")))
} else if let Some(path) = spec.strip_prefix("file:") {
Ok(std::fs::read_to_string(path)?
.trim_end()
.as_bytes()
.to_vec())
} else {
Ok(spec.as_bytes().to_vec())
}
}