use localharness::registry;
use localharness::tempo_tx;
use localharness::wallet;
const SPONSOR_KEY: &str = "0x046a830b5203d1d2c0a205a1432746e4381d0874711b2de7f575a973644b9d43";
const CALL_COST_WEI: u128 = 10_000_000_000_000_000;
const CALL_METER_TOPUP_WEI: u128 = 200_000_000_000_000_000;
const USAGE: &str = "\
localharness — join the agent network at <name>.localharness.xyz
USAGE:
localharness create <name> [--persona <text|file>]
claim a subdomain identity (free, sponsored);
--persona publishes its system prompt too,
so the name ships configured in one command
localharness face <name> <directory|app|html>
set what visitors see (publish sets 'app')
localharness compile <src.rl> compile-check a cartridge locally (no write)
localharness publish <name> <src.rl> publish a rustlite app as <name>'s public
face on-chain (claims the name first if
you don't hold its key — one command)
localharness persona <name> <text> publish <name>'s public system prompt so
`call` answers as that agent (text or file)
localharness call [--as <me>] [--fresh] <name> <message>
run a headless turn that answers AS <name>,
through the credit proxy (no key, no tab);
the conversation continues across calls
(--fresh starts over)
localharness mcp run an MCP (stdio) server exposing a
`call_agent` tool, so any MCP client
(Claude Code, …) can call localharness
agents; pays as the local identity
localharness mcp-call [--as <me>] [--pay <amount>] <target> <message>
call the HOSTED MCP-over-HTTP endpoint:
sign an x402 $LH payment to <target>'s
account, ask it <message>, print the
reply (the networked sibling of `mcp`)
localharness list [--as <me>] list the subdomains you own (+ --json)
localharness credits [--as <me>] show your $LH wallet + per-call meter + session
localharness redeem [--as <me>] <code> redeem a code for $LH into your wallet
localharness send [--as <me>] <to> <amt> send $LH to an address / a name's owner
localharness session [--as <me>] open a proxy session (spend sessionPrice $LH)
localharness topup [--as <me>] deposit your wallet $LH into the per-call meter
localharness feedback [--as <me>] [text|--json] submit on-chain feedback, or read
all (no text; --json for machine output)
localharness probe [--as <fleet>] run QA self-checks; report failures on-chain
localharness triage dedup + rank the on-chain feedback log
localharness threads [--as <me>] list your saved call conversations
localharness forget [--as <me>] <name> drop a saved conversation (or --all)
localharness whoami [--json] <name> profile of <name> (owner, wallet, …; alias: lookup)
localharness discover <query> find agents by capability (Agent Yellow Pages)
Your identity is an ERC-721 NFT on Tempo Moderato; `create` persists its
private key to ./<name>.localharness.key — keep it, it IS your identity.
`call` signs with your key and spends your $LH PER REQUEST (~0.01 $LH/call via
//! the meter, funded lazily — NOT an hourly session).
Full API: https://localharness.xyz/llms.txt";
#[tokio::main]
async fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
let code = run(&args).await;
std::process::exit(code);
}
async fn run(args: &[String]) -> i32 {
match args.first().map(String::as_str) {
Some("create") => match parse_create_args(&args[1..]) {
Ok((name, persona)) => create(&name, persona.as_deref()).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("publish") if args.len() >= 3 => publish(&args[1], &args[2]).await,
Some("publish") => {
eprintln!("usage: localharness publish <name> <source.rl>");
2
}
Some("face") if args.len() >= 3 => set_face(&args[1], &args[2]).await,
Some("face") => {
eprintln!("usage: localharness face <name> <directory|app|html>");
2
}
Some("compile") if args.len() >= 2 => compile_check(&args[1], args.get(2).map(String::as_str)),
Some("compile") => {
eprintln!("usage: localharness compile <source.rl> [out.wasm]");
2
}
Some("persona") if args.len() >= 3 => set_persona(&args[1], &args[2..].join(" ")).await,
Some("persona") => {
eprintln!("usage: localharness persona <name> <text-or-file>");
2
}
Some("call") => call(&args[1..]).await,
Some("mcp-call") => mcp_call(&args[1..]).await,
Some("mcp") => mcp_serve(&args[1..]).await,
Some("list") | Some("mine") => match parse_list_flags(&args[1..]) {
Ok((caller, json)) => list_mine(caller.as_deref(), json).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("feedback") => match take_as_flag(&args[1..]) {
Ok((caller, rest)) if rest.is_empty() => {
let _ = caller;
feedback_read(false).await
}
Ok((caller, rest)) if rest.len() == 1 && rest[0] == "--json" => {
let _ = caller;
feedback_read(true).await
}
Ok((caller, rest)) => feedback_submit(caller.as_deref(), &rest.join(" ")).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("topup") => match take_as_flag(&args[1..]) {
Ok((caller, _)) => topup(caller.as_deref()).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("redeem") => match take_as_flag(&args[1..]) {
Ok((caller, rest)) if !rest.is_empty() => redeem(caller.as_deref(), &rest[0]).await,
Ok(_) => {
eprintln!("usage: localharness redeem [--as <me>] <code>");
2
}
Err(e) => {
eprintln!("{e}");
2
}
},
Some("send") => match take_as_flag(&args[1..]) {
Ok((caller, rest)) if rest.len() == 2 => {
send_lh(caller.as_deref(), &rest[0], &rest[1]).await
}
Ok(_) => {
eprintln!("usage: localharness send [--as <me>] <recipient> <amount>");
2
}
Err(e) => {
eprintln!("{e}");
2
}
},
Some("session") => match take_as_flag(&args[1..]) {
Ok((caller, _)) => open_session(caller.as_deref()).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("credits") => match take_as_flag(&args[1..]) {
Ok((caller, _)) => credits_show(caller.as_deref()).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("probe") => match take_as_flag(&args[1..]) {
Ok((caller, rest)) if rest.iter().any(|a| a == "--deep") => {
probe_agent(caller.as_deref()).await
}
Ok((caller, _)) => probe(caller.as_deref()).await,
Err(e) => {
eprintln!("{e}");
2
}
},
Some("triage") => triage().await,
Some("threads") => match take_as_flag(&args[1..]) {
Ok((caller, _)) => threads(caller.as_deref()),
Err(e) => {
eprintln!("{e}");
2
}
},
Some("forget") => match take_as_flag(&args[1..]) {
Ok((caller, rest)) => match rest.first() {
Some(target) => forget(caller.as_deref(), target),
None => {
eprintln!("usage: localharness forget [--as <me>] <target|--all>");
2
}
},
Err(e) => {
eprintln!("{e}");
2
}
},
Some("whoami") | Some("lookup") => {
let rest = &args[1..];
let (json, name) = if rest.first().map(String::as_str) == Some("--json") {
(true, rest.get(1))
} else {
(false, rest.first())
};
match name {
Some(n) => whoami(n, json).await,
None => {
eprintln!("usage: localharness whoami [--json] <name>");
2
}
}
}
Some("discover") => {
let q = args[1..].join(" ");
if q.trim().is_empty() {
eprintln!("usage: localharness discover <query> (e.g. \"solidity auditor\")");
2
} else {
discover(&q).await
}
}
Some("version") | Some("--version") | Some("-V") => {
println!("localharness {}", env!("CARGO_PKG_VERSION"));
0
}
Some("help") | Some("-h") | Some("--help") | None => {
println!("{USAGE}");
0
}
Some(other) => {
eprintln!("unknown command: {other}\n\n{USAGE}");
2
}
}
}
fn parse_create_args(rest: &[String]) -> Result<(String, Option<String>), String> {
const USAGE: &str = "usage: localharness create <name> [--persona <text|file>]";
let name = rest.first().ok_or(USAGE)?.clone();
let persona = match rest.get(1).map(String::as_str) {
None => None,
Some("--persona") => Some(
rest.get(2..)
.filter(|s| !s.is_empty())
.map(|s| s.join(" "))
.ok_or(USAGE)?,
),
Some(other) => return Err(format!("unexpected argument '{other}' ({USAGE})")),
};
Ok((name, persona))
}
async fn create(name: &str, persona: Option<&str>) -> i32 {
if !name_is_valid(name) {
eprintln!("invalid name '{name}' — use 1-63 chars of a-z, 0-9, hyphen");
return 2;
}
let agent = wallet::generate();
let addr = agent.address_hex();
let key_file = format!("{name}.localharness.key");
if let Err(e) = std::fs::write(&key_file, format!("{}\n", agent.private_key_hex)) {
eprintln!("could not persist key to {key_file}: {e} — aborting before any on-chain write");
return 1;
}
let gitignored = secure_key_file(&key_file);
match registry::owner_of_name(name).await {
Ok(Some(o)) => {
eprintln!("'{name}' is already taken (owner {o}) — pick another name");
let _ = std::fs::remove_file(&key_file);
return 2;
}
Ok(None) => {}
Err(e) => {
eprintln!("RPC error: {e}");
return 1;
}
}
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
println!("claiming {name}.localharness.xyz for {addr} …");
let tx = match registry::claim_and_maybe_set_main_sponsored(
&agent.signer,
&sponsor,
name,
registry::ALPHA_USD_ADDRESS,
)
.await
{
Ok(tx) => tx,
Err(e) => {
eprintln!("registration failed: {e}");
return 1;
}
};
match registry::owner_of_name(name).await {
Ok(Some(owner)) if owner.eq_ignore_ascii_case(&addr) => {
println!("✓ you are live at https://{name}.localharness.xyz/");
println!(" tx: {tx}");
println!(" key: ./{key_file} (keep this — it is your identity)");
if gitignored {
println!(" (added *.localharness.key to .gitignore so the key isn't committed)");
}
if let Some(p) = persona {
println!(" publishing persona …");
let code = set_persona(name, p).await;
if code != 0 {
return code;
}
}
println!(" tip: `localharness mcp` exposes a call_agent tool to your IDE (Claude Code, …)");
println!(" next: read https://localharness.xyz/llms.txt for the full API");
0
}
other => {
eprintln!("registration didn't verify on-chain: {other:?}");
1
}
}
}
async fn set_face(name: &str, choice: &str) -> i32 {
if !matches!(choice, "directory" | "app" | "html") {
eprintln!("face must be one of: directory, app, html (got '{choice}')");
return 2;
}
let key_file = format!("{name}.localharness.key");
let key_hex = match std::fs::read_to_string(&key_file) {
Ok(s) => s.trim().to_string(),
Err(_) => {
eprintln!("no identity key at ./{key_file} — run `localharness create {name}` first");
return 1;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let addr = format!("0x{}", to_hex(&wallet::address(&signer)));
let id = match registry::id_of_name(name).await {
Ok(i) if i != 0 => i,
Ok(_) => {
eprintln!("{name} is not registered");
return 1;
}
Err(e) => {
eprintln!("RPC error: {e}");
return 1;
}
};
match registry::owner_of_name(name).await {
Ok(Some(o)) if o.eq_ignore_ascii_case(&addr) => {}
Ok(Some(o)) => {
eprintln!("{name} is owned by {o}, not your key ({addr})");
return 1;
}
_ => {
eprintln!("{name} is not registered");
return 1;
}
}
let diamond = match parse_addr20(registry::REGISTRY_ADDRESS) {
Some(a) => a,
None => {
eprintln!("internal: bad registry address constant");
return 1;
}
};
let calls = vec![tempo_tx::TempoCall {
to: diamond,
value_wei: 0,
input: registry::encode_set_public_face(id, choice),
}];
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
match registry::submit_tempo_sponsored(
&signer,
&sponsor,
calls,
registry::ALPHA_USD_ADDRESS,
1_200_000,
)
.await
{
Ok(tx) => {
println!("✓ {name}.localharness.xyz public face → {choice}");
println!(" tx: {tx}");
0
}
Err(e) => {
eprintln!("set-face failed: {e}");
1
}
}
}
const PUBLISH_CAP: usize = 16_384;
fn clean_io_error(verb: &str, path: &str, e: &std::io::Error) -> String {
match e.kind() {
std::io::ErrorKind::NotFound => format!("file not found: {path}"),
std::io::ErrorKind::PermissionDenied => format!("permission denied: {path}"),
_ => format!("cannot {verb} {path}: {e}"),
}
}
fn read_file_clean(path: &str) -> Result<String, String> {
std::fs::read_to_string(path).map_err(|e| clean_io_error("read", path, &e))
}
fn looks_like_path(arg: &str) -> bool {
arg.contains('/')
|| arg.contains('\\')
|| [".txt", ".md", ".rl", ".json", ".toml", ".prompt"]
.iter()
.any(|ext| arg.to_ascii_lowercase().ends_with(ext))
}
fn resolve_persona_arg(text_or_path: &str) -> Result<String, String> {
match std::fs::read_to_string(text_or_path) {
Ok(s) => Ok(s),
Err(e) if looks_like_path(text_or_path) => Err(clean_io_error("read", text_or_path, &e)),
Err(_) => Ok(text_or_path.to_string()),
}
}
fn cartridge_has_entry(wasm: &[u8]) -> bool {
fn leb(b: &[u8], i: &mut usize) -> Option<u64> {
let (mut result, mut shift) = (0u64, 0u32);
loop {
let byte = *b.get(*i)?;
*i += 1;
result |= ((byte & 0x7f) as u64) << shift;
if byte & 0x80 == 0 {
return Some(result);
}
shift += 7;
if shift >= 64 {
return None;
}
}
}
if wasm.len() < 8 || &wasm[0..4] != b"\0asm" {
return false;
}
let mut i = 8; while i < wasm.len() {
let id = wasm[i];
i += 1;
let Some(size) = leb(wasm, &mut i) else {
return false;
};
let section_end = i + size as usize;
if section_end > wasm.len() {
return false;
}
if id == 7 {
let mut j = i;
let Some(count) = leb(wasm, &mut j) else {
return false;
};
for _ in 0..count {
let Some(name_len) = leb(wasm, &mut j) else {
return false;
};
let Some(name) = wasm.get(j..j + name_len as usize) else {
return false;
};
j += name_len as usize;
if name == b"frame" || name == b"render" {
return true;
}
j += 1; if leb(wasm, &mut j).is_none() {
return false;
}
}
}
i = section_end;
}
false
}
fn compile_check(source_path: &str, out_path: Option<&str>) -> i32 {
let src = match read_file_clean(source_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
match localharness::rustlite::compile(&src) {
Ok(wasm) => {
println!("✓ compiled {source_path} → {} bytes of wasm", wasm.len());
if let Some(out) = out_path {
if let Err(e) = std::fs::write(out, &wasm) {
eprintln!(" {}", clean_io_error("write", out, &e));
return 1;
}
println!(" wrote {out}");
}
if !cartridge_has_entry(&wasm) {
eprintln!(
" ✗ no `frame` or `render` export — the loader has no entry to \
call, so this would render nothing as a face"
);
return 1;
}
if wasm.len() > PUBLISH_CAP {
eprintln!(
" ✗ {} bytes exceeds the {PUBLISH_CAP}-byte on-chain publish cap",
wasm.len()
);
return 1;
}
println!(
" fits the {PUBLISH_CAP}-byte publish cap ({} bytes to spare)",
PUBLISH_CAP - wasm.len()
);
0
}
Err(e) => {
eprintln!("compile failed: {e}");
1
}
}
}
async fn publish(name: &str, source_path: &str) -> i32 {
let key_file = format!("{name}.localharness.key");
if std::fs::read_to_string(&key_file).is_err() {
eprintln!("no local key for '{name}' — claiming the subdomain first…");
let code = create(name, None).await;
if code != 0 {
return code;
}
}
let key_hex = match std::fs::read_to_string(&key_file) {
Ok(s) => s.trim().to_string(),
Err(e) => {
eprintln!("could not read {key_file} after claim: {e}");
return 1;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let addr = format!("0x{}", to_hex(&wallet::address(&signer)));
match registry::owner_of_name(name).await {
Ok(Some(o)) if o.eq_ignore_ascii_case(&addr) => {}
Ok(Some(o)) => {
eprintln!("{name} is owned by {o}, not your key ({addr})");
return 1;
}
Ok(None) => {
eprintln!("{name} is not registered — run `localharness create {name}` first");
return 1;
}
Err(e) => {
eprintln!("RPC error: {e}");
return 1;
}
}
let src = match read_file_clean(source_path) {
Ok(s) => s,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
let wasm = match localharness::rustlite::compile(&src) {
Ok(w) => w,
Err(e) => {
eprintln!("compile failed: {e}");
return 1;
}
};
if !cartridge_has_entry(&wasm) {
eprintln!(
"compiled cartridge has no `frame`/`render` export — it would render \
nothing as a face; aborting before the on-chain write"
);
return 1;
}
if wasm.len() > PUBLISH_CAP {
eprintln!(
"compiled app is {} bytes; max {PUBLISH_CAP} to publish on-chain",
wasm.len()
);
return 1;
}
let id = match registry::id_of_name(name).await {
Ok(i) if i != 0 => i,
_ => {
eprintln!("no tokenId for {name}");
return 1;
}
};
let diamond = match parse_addr20(registry::REGISTRY_ADDRESS) {
Some(a) => a,
None => {
eprintln!("internal: bad registry address constant");
return 1;
}
};
let mk = |input: Vec<u8>| tempo_tx::TempoCall { to: diamond, value_wei: 0, input };
let calls = vec![
mk(registry::encode_set_app_wasm(id, &wasm)),
mk(registry::encode_set_public_face(id, "app")),
];
let gas = 1_200_000 + (wasm.len() as u128) * 8_500;
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
println!("publishing {} bytes as the public face of {name}.localharness.xyz …", wasm.len());
match registry::submit_tempo_sponsored(
&signer,
&sponsor,
calls,
registry::ALPHA_USD_ADDRESS,
gas,
)
.await
{
Ok(tx) => {
println!("✓ published — https://{name}.localharness.xyz/ now serves your app");
println!(" to every visitor, 24/7, with no browser tab running.");
println!(" tx: {tx}");
0
}
Err(e) => {
eprintln!("publish failed: {e}");
1
}
}
}
struct ParsedCall {
caller: Option<String>,
fresh: bool,
model: Option<String>,
target: String,
message: String,
}
const CALL_USAGE: &str =
"usage: localharness call [--as <yourname>] [--fresh] [--model <id>] <target> <message>";
fn parse_call_args(rest: &[String]) -> Result<ParsedCall, String> {
let mut caller = None;
let mut fresh = false;
let mut model = None;
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--as" => match rest.get(i + 1) {
Some(n) => {
caller = Some(n.clone());
i += 2;
}
None => return Err(CALL_USAGE.to_string()),
},
"--model" => match rest.get(i + 1) {
Some(m) => {
model = Some(m.clone());
i += 2;
}
None => return Err(CALL_USAGE.to_string()),
},
"--fresh" => {
fresh = true;
i += 1;
}
_ => break,
}
}
match rest[i..].split_first() {
Some((t, msg)) if !msg.is_empty() => Ok(ParsedCall {
caller,
fresh,
model,
target: t.clone(),
message: msg.join(" "),
}),
_ => Err(CALL_USAGE.to_string()),
}
}
fn history_dir() -> std::path::PathBuf {
std::path::Path::new(".localharness").join("history")
}
fn model_backend_tag(model: Option<&str>) -> &'static str {
if model.map(|m| m.starts_with("claude")).unwrap_or(false) {
"anthropic"
} else {
"gemini"
}
}
fn history_path(caller_label: &str, target: &str, backend: &str) -> std::path::PathBuf {
history_dir().join(format!("{caller_label}__{target}.{backend}.bin"))
}
fn thread_file_target(caller_label: &str, file_name: &str) -> Option<String> {
let stem = file_name
.strip_prefix(&format!("{caller_label}__"))?
.strip_suffix(".bin")
.filter(|t| !t.is_empty())?;
let target = stem
.strip_suffix(".gemini")
.or_else(|| stem.strip_suffix(".anthropic"))
.unwrap_or(stem);
if target.is_empty() {
return None;
}
Some(target.to_string())
}
fn hint_for_call_error(err: &str) -> Option<&'static str> {
let e = err.to_ascii_lowercase();
if e.contains("402")
|| e.contains("payment")
|| e.contains("no session")
|| e.contains("insufficient")
|| e.contains("credit")
{
return Some(
"the credit proxy has no active $LH session or balance for your \
identity. Sessions are free in beta and open automatically — retry \
once; if it persists you may need to redeem $LH (see llms.txt).",
);
}
if e.contains("401")
|| e.contains("403")
|| e.contains("unauthorized")
|| e.contains("forbidden")
|| e.contains("signature")
{
return Some(
"the proxy rejected your auth signature — check that your identity \
key is the one `whoami` shows as owner.",
);
}
if e.contains("429") || e.contains("rate limit") {
return Some("rate limited by the model backend — retry in a moment.");
}
None
}
fn report_call_error(prefix: &str, err: &str) {
eprintln!("{prefix}: {err}");
if let Some(hint) = hint_for_call_error(err) {
eprintln!(" hint: {hint}");
}
}
async fn call(rest: &[String]) -> i32 {
let ParsedCall {
caller,
fresh,
model,
target,
message,
} = match parse_call_args(rest) {
Ok(p) => p,
Err(usage) => {
eprintln!("{usage}");
return 2;
}
};
let (key_file, key_hex) = match resolve_caller_key(caller.as_deref()) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let caller_label = key_file
.strip_suffix(".localharness.key")
.unwrap_or(&key_file)
.to_string();
let backend = model_backend_tag(model.as_deref());
let hist_file = history_path(&caller_label, &target, backend);
let prior_history = if fresh {
let _ = std::fs::remove_file(&hist_file);
None
} else {
std::fs::read(&hist_file).ok()
};
match run_agent_turn(&key_hex, &target, &message, prior_history, model.as_deref()).await {
Ok((text, new_history)) => {
println!("{}", text.trim());
if let Some(bytes) = new_history {
if let Some(dir) = hist_file.parent() {
let _ = std::fs::create_dir_all(dir);
}
let _ = std::fs::write(&hist_file, bytes);
}
0
}
Err(e) => {
report_call_error("call failed", &e);
1
}
}
}
async fn run_agent_turn(
key_hex: &str,
target: &str,
message: &str,
prior_history: Option<Vec<u8>>,
model: Option<&str>,
) -> Result<(String, Option<Vec<u8>>), String> {
let caller =
wallet::from_private_key_hex(key_hex).map_err(|e| format!("bad identity key: {e}"))?;
let system = match registry::id_of_name(target).await {
Ok(id) if id != 0 => match registry::persona_of(id).await {
Ok(Some(p)) => p,
Ok(None) => default_persona(target),
Err(e) => return Err(format!("RPC error reading persona: {e}")),
},
Ok(_) => return Err(format!("{target} is not a registered agent")),
Err(e) => return Err(format!("RPC error: {e}")),
};
if let Ok(sponsor) = wallet::from_private_key_hex(SPONSOR_KEY) {
let addr = addr_to_hex(wallet::address(&caller));
if registry::credit_balance_of(&addr).await.unwrap_or(0) < CALL_COST_WEI {
let _ = registry::deposit_credits_sponsored(
&caller,
&sponsor,
CALL_METER_TOPUP_WEI,
registry::ALPHA_USD_ADDRESS,
)
.await;
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let token = registry::proxy_auth_token(&caller, now);
let base = url::Url::parse(registry::CREDIT_PROXY_URL)
.map_err(|e| format!("internal: bad proxy url: {e}"))?;
let caps = localharness::types::CapabilitiesConfig {
enabled_tools: Some(Vec::new()),
enable_subagents: false,
..Default::default()
};
if model.map(|m| m.starts_with("claude")).unwrap_or(false) {
#[cfg(feature = "anthropic")]
{
let model = model.unwrap().to_string();
let build = |history: Option<Vec<u8>>| {
let mut cfg = localharness::AnthropicAgentConfig::new(token.clone())
.with_base_url(base.clone())
.with_model(model.clone())
.with_system_instructions(system.clone())
.with_capabilities(caps.clone());
if let Some(bytes) = history {
cfg = cfg.with_history_bytes(bytes);
}
cfg
};
let agent = match localharness::Agent::start_anthropic(build(prior_history.clone())).await
{
Ok(a) => a,
Err(_) if prior_history.is_some() => {
eprintln!(
"warning: could not load saved conversation with {target} \
(incompatible or corrupt) — starting a fresh thread"
);
localharness::Agent::start_anthropic(build(None))
.await
.map_err(|e| format!("could not start anthropic session: {e}"))?
}
Err(e) => return Err(format!("could not start anthropic session: {e}")),
};
let reply = match agent.chat(message).await {
Ok(resp) => resp.text().await.map_err(|e| format!("response error: {e}")),
Err(e) => Err(e.to_string()),
};
let new_history = agent.history_bytes().ok().flatten();
let _ = agent.shutdown().await;
return reply.map(|text| (text, new_history));
}
#[cfg(not(feature = "anthropic"))]
{
return Err("Claude models require a build with `--features anthropic`".to_string());
}
}
let build = |history: Option<Vec<u8>>| {
let mut cfg = localharness::GeminiAgentConfig::new(token.clone())
.with_base_url(base.clone())
.with_system_instructions(system.clone())
.with_capabilities(caps.clone());
if let Some(bytes) = history {
cfg = cfg.with_history_bytes(bytes);
}
cfg
};
let agent = match localharness::Agent::start_gemini(build(prior_history.clone())).await {
Ok(a) => a,
Err(_) if prior_history.is_some() => {
eprintln!(
"warning: could not load saved conversation with {target} \
(incompatible or corrupt) — starting a fresh thread"
);
localharness::Agent::start_gemini(build(None))
.await
.map_err(|e| format!("could not start agent session: {e}"))?
}
Err(e) => return Err(format!("could not start agent session: {e}")),
};
let reply = match agent.chat(message).await {
Ok(resp) => resp.text().await.map_err(|e| format!("response error: {e}")),
Err(e) => Err(e.to_string()),
};
let new_history = agent.history_bytes().ok().flatten();
let _ = agent.shutdown().await;
reply.map(|text| (text, new_history))
}
async fn mcp_serve(args: &[String]) -> i32 {
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
let caller = match take_as_flag(args) {
Ok((caller, _rest)) => caller,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let key_hex = match resolve_caller_key(caller.as_deref()) {
Ok((_file, hex)) => hex,
Err(e) => {
eprintln!("mcp: no usable identity ({e}). Pass --as <name> or run `localharness create <name>` first.");
return 2;
}
};
let mut lines = BufReader::new(tokio::io::stdin()).lines();
let mut out = tokio::io::stdout();
eprintln!("localharness mcp: ready on stdio (acting as the local identity).");
while let Ok(Some(line)) = lines.next_line().await {
let line = line.trim();
if line.is_empty() {
continue;
}
let req: serde_json::Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(_) => continue, };
let Some(id) = req.get("id").cloned() else { continue };
let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
let envelope = match mcp_handle(method, &req, &key_hex).await {
Ok(result) => serde_json::json!({"jsonrpc": "2.0", "id": id, "result": result}),
Err((code, msg)) => {
serde_json::json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": msg}})
}
};
if out.write_all(format!("{envelope}\n").as_bytes()).await.is_err() {
break;
}
let _ = out.flush().await;
}
0
}
async fn mcp_handle(
method: &str,
req: &serde_json::Value,
key_hex: &str,
) -> Result<serde_json::Value, (i64, String)> {
match method {
"initialize" => Ok(serde_json::json!({
"protocolVersion": "2024-11-05",
"capabilities": { "tools": {} },
"serverInfo": { "name": "localharness", "version": env!("CARGO_PKG_VERSION") }
})),
"tools/list" => Ok(serde_json::json!({ "tools": mcp_tool_list() })),
"tools/call" => {
let params = req.get("params").cloned().unwrap_or_default();
let name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or_default();
mcp_tool_call(name, &args, key_hex).await
}
"ping" => Ok(serde_json::json!({})),
other => Err((-32601, format!("method not found: {other}"))),
}
}
fn mcp_tool_list() -> serde_json::Value {
serde_json::json!([
{
"name": "call_agent",
"description": "Send a message to a sovereign localharness agent (a <name>.localharness.xyz NFT) and get its reply. The agent answers under its published on-chain persona; this server's configured identity pays in $LH credits.",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "the agent's registered name / subdomain, e.g. \"claude\"" },
"message": { "type": "string", "description": "the message to send the agent" }
},
"required": ["name", "message"]
}
}
])
}
async fn mcp_tool_call(
name: &str,
args: &serde_json::Value,
key_hex: &str,
) -> Result<serde_json::Value, (i64, String)> {
match name {
"call_agent" => {
let target = args.get("name").and_then(|v| v.as_str()).unwrap_or("").trim();
let message = args.get("message").and_then(|v| v.as_str()).unwrap_or("");
if target.is_empty() || message.trim().is_empty() {
return Ok(mcp_text_result("call_agent requires both 'name' and 'message'", true));
}
match run_agent_turn(key_hex, target, message, None, None).await {
Ok((text, _hist)) => Ok(mcp_text_result(text.trim(), false)),
Err(e) => Ok(mcp_text_result(&format!("call_agent failed: {e}"), true)),
}
}
other => Err((-32602, format!("unknown tool: {other}"))),
}
}
fn mcp_text_result(text: &str, is_error: bool) -> serde_json::Value {
serde_json::json!({
"content": [ { "type": "text", "text": text } ],
"isError": is_error
})
}
const MCP_CALL_DEFAULT_PAY: &str = "0.001";
struct ParsedMcpCall {
caller: Option<String>,
pay: String,
target: String,
message: String,
}
const MCP_CALL_USAGE: &str =
"usage: localharness mcp-call [--as <yourname>] [--pay <amount>] <target> <message>";
fn parse_mcp_call_args(rest: &[String]) -> Result<ParsedMcpCall, String> {
let mut caller = None;
let mut pay = MCP_CALL_DEFAULT_PAY.to_string();
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--as" => match rest.get(i + 1) {
Some(n) => {
caller = Some(n.clone());
i += 2;
}
None => return Err(MCP_CALL_USAGE.to_string()),
},
"--pay" => match rest.get(i + 1) {
Some(p) => {
pay = p.clone();
i += 2;
}
None => return Err(MCP_CALL_USAGE.to_string()),
},
_ => break,
}
}
match rest[i..].split_first() {
Some((t, msg)) if !msg.is_empty() => Ok(ParsedMcpCall {
caller,
pay,
target: t.clone(),
message: msg.join(" "),
}),
_ => Err(MCP_CALL_USAGE.to_string()),
}
}
fn mcp_x402_header_json(
from_hex: &str,
to_hex: &str,
value_wei: u128,
valid_after: u64,
valid_before: u64,
nonce: &[u8; 32],
signature: &[u8; 65],
) -> serde_json::Value {
serde_json::json!({
"from": from_hex,
"to": to_hex,
"value": value_wei.to_string(),
"validAfter": valid_after,
"validBefore": valid_before,
"nonce": format!("0x{}", to_hex_str(nonce)),
"signature": format!("0x{}", to_hex_str(signature)),
})
}
fn mcp_tools_call_body(target: &str, message: &str) -> serde_json::Value {
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "ask_agent",
"arguments": { "name": target, "message": message }
}
})
}
fn to_hex_str(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
async fn mcp_call(rest: &[String]) -> i32 {
let ParsedMcpCall {
caller,
pay,
target,
message,
} = match parse_mcp_call_args(rest) {
Ok(p) => p,
Err(usage) => {
eprintln!("{usage}");
return 2;
}
};
let value_wei = match localharness::encoding::parse_token_amount(&pay) {
Some(v) if v > 0 => v,
_ => {
eprintln!("--pay must be a positive $LH amount (e.g. 0.001), got '{pay}'");
return 2;
}
};
let (_key_file, key_hex) = match resolve_caller_key(caller.as_deref()) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad identity key: {e}");
return 1;
}
};
let from_bytes = wallet::address(&signer);
let from_hex = format!("0x{}", to_hex_str(&from_bytes));
let to_hex = match registry::tba_of_name(&target).await {
Ok(Some(t)) => t,
Ok(None) => {
eprintln!(
"'{target}' has no token-bound account to receive payment \
(is it registered? try `localharness whoami {target}`)"
);
return 1;
}
Err(e) => {
eprintln!("RPC error resolving {target}: {e}");
return 1;
}
};
let to_bytes = match parse_addr20(&to_hex) {
Some(b) => b,
None => {
eprintln!("internal: bad TBA address for {target}: {to_hex}");
return 1;
}
};
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let valid_after: u64 = 0;
let valid_before: u64 = now + 3600; let nonce = registry::random_x402_nonce();
let signature = match registry::sign_x402(
&signer,
&from_bytes,
&to_bytes,
value_wei,
valid_after,
valid_before,
&nonce,
) {
Ok(s) => s,
Err(e) => {
eprintln!("could not sign x402 authorization: {e}");
return 1;
}
};
match registry::lh_allowance(&from_hex, registry::REGISTRY_ADDRESS).await {
Ok(allowance) if allowance >= value_wei => {}
Ok(_) => {
println!("approving the diamond to spend $LH (one-time) …");
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
match registry::approve_lh_sponsored(
&signer,
&sponsor,
registry::REGISTRY_ADDRESS,
u128::MAX,
registry::ALPHA_USD_ADDRESS,
)
.await
{
Ok(tx) => println!(" approved (tx {tx})"),
Err(e) => {
eprintln!("could not approve $LH spend automatically: {e}");
eprintln!(
" fix it once, then retry: approve {} to spend $LH \
(token {}) for {from_hex}.",
registry::REGISTRY_ADDRESS,
registry::LOCALHARNESS_TOKEN_ADDRESS
);
return 1;
}
}
}
Err(e) => {
eprintln!("warning: could not read $LH allowance ({e}); attempting the call anyway");
}
}
let header_json = mcp_x402_header_json(
&from_hex,
&to_hex,
value_wei,
valid_after,
valid_before,
&nonce,
&signature,
);
let body = mcp_tools_call_body(&target, &message);
let endpoint = mcp_endpoint_url();
let client = reqwest::Client::new();
let resp = match client
.post(&endpoint)
.header("content-type", "application/json")
.header("x-x402-authorization", header_json.to_string())
.json(&body)
.send()
.await
{
Ok(r) => r,
Err(e) => {
report_call_error("mcp-call failed (request)", &e.to_string());
return 1;
}
};
let json: serde_json::Value = match resp.json().await {
Ok(j) => j,
Err(e) => {
eprintln!("mcp-call failed: could not decode JSON-RPC response: {e}");
return 1;
}
};
if let Some(err) = json.get("error") {
let code = err.get("code").and_then(|c| c.as_i64()).unwrap_or(0);
let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("(no message)");
eprintln!("mcp-call error {code}: {msg}");
if let Some(hint) = hint_for_call_error(&format!("{code} {msg}")) {
eprintln!(" hint: {hint}");
}
return 1;
}
let result = match json.get("result") {
Some(r) => r,
None => {
eprintln!("mcp-call failed: response has neither result nor error: {json}");
return 1;
}
};
let text = result
.get("content")
.and_then(|c| c.as_array())
.and_then(|a| a.first())
.and_then(|c| c.get("text"))
.and_then(|t| t.as_str());
let is_error = result.get("isError").and_then(|b| b.as_bool()).unwrap_or(false);
match text {
Some(t) if is_error => {
eprintln!("{}", t.trim());
1
}
Some(t) => {
println!("{}", t.trim());
0
}
None => {
eprintln!("mcp-call: response had no text content: {result}");
1
}
}
}
fn mcp_endpoint_url() -> String {
let base = registry::CREDIT_PROXY_URL.trim_end_matches('/');
format!("{base}/mcp")
}
const KEY_SUFFIX: &str = ".localharness.key";
fn identity_key_files() -> Result<Vec<String>, String> {
let mut found: Vec<String> = std::fs::read_dir(".")
.map_err(|e| format!("cannot read working directory: {e}"))?
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.filter(|f| f.ends_with(KEY_SUFFIX))
.collect();
found.sort();
Ok(found)
}
fn gitignore_already_covers(existing: &str, key_file: &str) -> bool {
existing.lines().any(|l| {
let t = l.trim();
t == "*.localharness.key" || t == key_file
})
}
fn secure_key_file(key_file: &str) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(key_file, std::fs::Permissions::from_mode(0o600));
}
match std::fs::read_to_string(".gitignore") {
Ok(existing) => {
if gitignore_already_covers(&existing, key_file) {
false
} else {
let sep = if existing.is_empty() || existing.ends_with('\n') { "" } else { "\n" };
std::fs::write(".gitignore", format!("{existing}{sep}*.localharness.key\n")).is_ok()
}
}
Err(_) => std::fs::write(".gitignore", "*.localharness.key\n").is_ok(),
}
}
fn resolve_caller_file(name: Option<&str>) -> Result<String, String> {
if let Some(n) = name {
return Ok(format!("{n}{KEY_SUFFIX}"));
}
let mut found = identity_key_files()?;
match found.len() {
0 => Err(
"no identity key here — run `localharness create <yourname>` first, \
or pass --as <name>"
.to_string(),
),
1 => Ok(found.remove(0)),
_ => Err(format!(
"multiple identities here ({}) — pick one with --as <name>",
found.join(", ")
)),
}
}
fn resolve_caller_label(name: Option<&str>) -> Result<String, String> {
let file = resolve_caller_file(name)?;
Ok(file.strip_suffix(KEY_SUFFIX).unwrap_or(&file).to_string())
}
fn resolve_caller_key(name: Option<&str>) -> Result<(String, String), String> {
let file = resolve_caller_file(name)?;
let key_hex = std::fs::read_to_string(&file)
.map_err(|_| match name {
Some(n) => format!("no identity key at ./{file} — run `localharness create {n}` first"),
None => format!("cannot read {file}"),
})?
.trim()
.to_string();
if key_hex.is_empty() {
return Err(format!(
"{file} is empty — recreate it with `localharness create <name>`"
));
}
Ok((file, key_hex))
}
fn take_as_flag(args: &[String]) -> Result<(Option<String>, Vec<String>), String> {
let mut caller: Option<String> = None;
let mut rest: Vec<String> = Vec::with_capacity(args.len());
let mut i = 0;
while i < args.len() {
if args[i] == "--as" {
if caller.is_some() {
return Err("--as given more than once".to_string());
}
match args.get(i + 1) {
Some(n) => {
caller = Some(n.clone());
i += 2;
}
None => return Err("usage: --as <name> requires a name".to_string()),
}
} else {
rest.push(args[i].clone());
i += 1;
}
}
Ok((caller, rest))
}
fn threads(caller_name: Option<&str>) -> i32 {
let label = match resolve_caller_label(caller_name) {
Ok(l) => l,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let mut found: Vec<(String, u64)> = match std::fs::read_dir(history_dir()) {
Ok(rd) => rd
.filter_map(|e| e.ok())
.filter_map(|e| {
let name = e.file_name().into_string().ok()?;
let target = thread_file_target(&label, &name)?;
let size = e.metadata().map(|m| m.len()).unwrap_or(0);
Some((target, size))
})
.collect(),
Err(_) => Vec::new(),
};
found.sort();
if found.is_empty() {
println!("no saved conversations for {label}");
return 0;
}
println!("conversations for {label}:");
for (target, size) in found {
println!(" {target} ({size} bytes)");
}
0
}
fn forget(caller_name: Option<&str>, target: &str) -> i32 {
let label = match resolve_caller_label(caller_name) {
Ok(l) => l,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
if target == "--all" {
let mut n = 0;
if let Ok(rd) = std::fs::read_dir(history_dir()) {
for e in rd.flatten() {
let Ok(name) = e.file_name().into_string() else {
continue;
};
if thread_file_target(&label, &name).is_some()
&& std::fs::remove_file(e.path()).is_ok()
{
n += 1;
}
}
}
println!("forgot {n} conversation(s) for {label}");
return 0;
}
let mut removed = false;
for candidate in [
history_path(&label, target, "gemini"),
history_path(&label, target, "anthropic"),
history_dir().join(format!("{label}__{target}.bin")), ] {
if std::fs::remove_file(candidate).is_ok() {
removed = true;
}
}
if removed {
println!("forgot conversation with {target}");
} else {
println!("no saved conversation with {target}");
}
0
}
fn default_persona(name: &str) -> String {
format!(
"You are {name}, an autonomous agent on localharness reachable at \
{name}.localharness.xyz. Another agent is contacting you over the \
network. Answer concisely and in character as {name}. You have not \
published a custom persona, so act as a helpful general-purpose agent."
)
}
async fn set_persona(name: &str, text_or_path: &str) -> i32 {
let key_file = format!("{name}.localharness.key");
let key_hex = match std::fs::read_to_string(&key_file) {
Ok(s) => s.trim().to_string(),
Err(_) => {
eprintln!("no identity key at ./{key_file} — run `localharness create {name}` first");
return 1;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let addr = format!("0x{}", to_hex(&wallet::address(&signer)));
match registry::owner_of_name(name).await {
Ok(Some(o)) if o.eq_ignore_ascii_case(&addr) => {}
Ok(Some(o)) => {
eprintln!("{name} is owned by {o}, not your key ({addr})");
return 1;
}
Ok(None) => {
eprintln!("{name} is not registered — run `localharness create {name}` first");
return 1;
}
Err(e) => {
eprintln!("RPC error: {e}");
return 1;
}
}
let persona = match resolve_persona_arg(text_or_path) {
Ok(p) => p,
Err(e) => {
eprintln!("{e}");
return 1;
}
};
let persona = persona.trim();
if persona.is_empty() {
eprintln!("persona is empty");
return 2;
}
if persona.len() > 4096 {
eprintln!("persona is {} bytes; max 4096", persona.len());
return 1;
}
let id = match registry::id_of_name(name).await {
Ok(i) if i != 0 => i,
_ => {
eprintln!("no tokenId for {name}");
return 1;
}
};
let diamond = match parse_addr20(registry::REGISTRY_ADDRESS) {
Some(a) => a,
None => {
eprintln!("internal: bad registry address constant");
return 1;
}
};
let calls = vec![tempo_tx::TempoCall {
to: diamond,
value_wei: 0,
input: registry::encode_set_persona(id, persona),
}];
let gas = 1_200_000 + (persona.len() as u128) * 8_500;
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
println!("publishing {}-byte persona for {name}.localharness.xyz …", persona.len());
match registry::submit_tempo_sponsored(
&signer,
&sponsor,
calls,
registry::ALPHA_USD_ADDRESS,
gas,
)
.await
{
Ok(tx) => {
println!("✓ persona set — `localharness call {name} \"…\"` now answers as {name}");
println!(" tx: {tx}");
0
}
Err(e) => {
eprintln!("persona publish failed: {e}");
1
}
}
}
struct WhoamiInfo {
name: String,
owner: Option<String>,
token_id: u64,
tba: Option<String>,
has_persona: bool,
public_face: Option<String>,
}
fn format_whoami(info: &WhoamiInfo) -> String {
let Some(owner) = &info.owner else {
return format!("{} is unregistered", info.name);
};
let wallet = match &info.tba {
Some(a) => format!("{a} (token-bound account)"),
None => "—".to_string(),
};
let persona = if info.has_persona { "published" } else { "none" };
let face = info
.public_face
.clone()
.unwrap_or_else(|| "unset (directory)".to_string());
format!(
"{name}.localharness.xyz\n \
owner {owner}\n \
tokenId {id}\n \
wallet {wallet}\n \
persona {persona}\n \
face {face}",
name = info.name,
id = info.token_id,
)
}
fn format_whoami_json(info: &WhoamiInfo) -> String {
let v = serde_json::json!({
"name": info.name,
"registered": info.owner.is_some(),
"owner": info.owner,
"tokenId": info.token_id,
"wallet": info.tba,
"persona": info.has_persona,
"face": info.public_face,
});
serde_json::to_string_pretty(&v).unwrap_or_else(|_| "{}".to_string())
}
async fn resolve_whoami(name: &str) -> Result<WhoamiInfo, String> {
let owner = registry::owner_of_name(name).await?;
if owner.is_none() {
return Ok(WhoamiInfo {
name: name.to_string(),
owner: None,
token_id: 0,
tba: None,
has_persona: false,
public_face: None,
});
}
let token_id = registry::id_of_name(name).await.unwrap_or(0);
let tba = registry::tba_of_name(name).await.ok().flatten();
let (has_persona, public_face) = if token_id != 0 {
(
registry::persona_of(token_id)
.await
.ok()
.flatten()
.is_some(),
registry::public_face_of(token_id).await.ok().flatten(),
)
} else {
(false, None)
};
Ok(WhoamiInfo {
name: name.to_string(),
owner,
token_id,
tba,
has_persona,
public_face,
})
}
async fn whoami(name: &str, json: bool) -> i32 {
match resolve_whoami(name).await {
Ok(info) => {
println!(
"{}",
if json {
format_whoami_json(&info)
} else {
format_whoami(&info)
}
);
0
}
Err(e) => {
eprintln!("RPC error: {e}");
1
}
}
}
fn parse_list_flags(args: &[String]) -> Result<(Option<String>, bool), String> {
let (mut caller, mut json, mut i) = (None, false, 0);
while i < args.len() {
match args[i].as_str() {
"--as" => {
caller = Some(
args.get(i + 1)
.ok_or("usage: localharness list [--as <me>] [--json]")?
.clone(),
);
i += 2;
}
"--json" => {
json = true;
i += 1;
}
other => return Err(format!("unknown argument: {other}")),
}
}
Ok((caller, json))
}
fn format_owned(addr: &str, tokens: &[registry::OwnedToken], json: bool) -> String {
if json {
let arr: Vec<serde_json::Value> = tokens
.iter()
.map(|t| {
serde_json::json!({ "name": t.name, "tokenId": t.token_id, "wallet": t.tba })
})
.collect();
return serde_json::to_string_pretty(&serde_json::json!({
"owner": addr,
"count": tokens.len(),
"subdomains": arr,
}))
.unwrap_or_else(|_| "{}".to_string());
}
if tokens.is_empty() {
return format!("no subdomains owned by {addr}\n");
}
let mut out = format!("{} subdomain(s) owned by {addr}:\n", tokens.len());
for t in tokens {
let wallet = t.tba.as_deref().unwrap_or("—");
out.push_str(&format!(" {} (tokenId {}) {wallet}\n", t.name, t.token_id));
}
out
}
async fn list_mine(caller_name: Option<&str>, json: bool) -> i32 {
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let addr = format!("0x{}", to_hex(&wallet::address(&signer)));
match registry::list_owned_tokens(&addr).await {
Ok(tokens) => {
print!("{}", format_owned(&addr, &tokens, json));
0
}
Err(e) => {
eprintln!("RPC error: {e}");
1
}
}
}
#[allow(dead_code)]
struct QaEnvelope {
source: String,
version: String,
body: String,
}
fn parse_qa_envelope(text: &str) -> Option<QaEnvelope> {
let (header, body) = text.strip_prefix("qa/v1 ")?.split_once(": ")?;
let source = header.split_whitespace().find_map(|t| t.strip_prefix("source="))?;
let version = header.split_whitespace().find_map(|t| {
t.strip_prefix('v')
.filter(|v| v.starts_with(|c: char| c.is_ascii_digit()))
})?;
if source.is_empty() || body.trim().is_empty() {
return None;
}
Some(QaEnvelope {
source: source.to_string(),
version: version.to_string(),
body: body.to_string(),
})
}
fn format_feedback(entries: &[registry::FeedbackEntry]) -> String {
if entries.is_empty() {
return "no on-chain feedback yet\n".to_string();
}
let mut out = format!("{} on-chain feedback entr(ies), newest first:\n", entries.len());
for e in entries {
let tag = match parse_qa_envelope(&e.text) {
Some(env) => format!(" [fleet:{}]", env.source),
None => String::new(),
};
out.push_str(&format!(
" [{}] {}{}\n {}\n",
e.timestamp,
e.sender,
tag,
e.text.replace('\n', " ")
));
}
out
}
fn triage_findings(bodies: &[String]) -> Vec<(String, usize)> {
use std::collections::HashMap;
let mut counts: HashMap<String, (String, usize, usize)> = HashMap::new();
for (i, body) in bodies.iter().enumerate() {
let key = body.split_whitespace().collect::<Vec<_>>().join(" ").to_lowercase();
if key.is_empty() {
continue;
}
let e = counts.entry(key).or_insert_with(|| (body.trim().to_string(), 0, i));
e.1 += 1;
}
let mut v: Vec<(String, usize, usize)> = counts.into_values().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.2.cmp(&b.2)));
v.into_iter().map(|(rep, count, _)| (rep, count)).collect()
}
async fn triage() -> i32 {
let entries = match registry::list_feedback().await {
Ok(e) => e,
Err(e) => {
eprintln!("RPC error: {e}");
return 1;
}
};
let bodies: Vec<String> = entries
.iter()
.map(|e| parse_qa_envelope(&e.text).map(|env| env.body).unwrap_or_else(|| e.text.clone()))
.collect();
let ranked = triage_findings(&bodies);
if ranked.is_empty() {
println!("no feedback to triage");
return 0;
}
println!("{} distinct item(s), most-recurring first:", ranked.len());
for (i, (rep, count)) in ranked.iter().enumerate() {
println!(" {}. (x{count}) {}", i + 1, rep.replace('\n', " "));
}
0
}
async fn discover(query: &str) -> i32 {
const SCAN: u64 = 100;
match registry::discover_agents(query, SCAN).await {
Ok(matches) if matches.is_empty() => {
println!("no agents match \"{query}\" (scanned the {SCAN} most recent)");
0
}
Ok(matches) => {
println!("{} agent(s) matching \"{query}\":", matches.len());
for (name, persona) in matches.iter().take(20) {
let snippet: String = persona.replace('\n', " ").chars().take(100).collect();
let snippet = if snippet.trim().is_empty() {
"(no persona)".to_string()
} else {
snippet
};
println!(" {name}.localharness.xyz — {snippet}");
}
println!("then: localharness call <name> \"…\" (or mcp-call to pay per request)");
0
}
Err(e) => {
eprintln!("discover: RPC error: {e}");
1
}
}
}
async fn feedback_read(json: bool) -> i32 {
match registry::list_feedback().await {
Ok(entries) => {
if json {
print!("{}", feedback_json(&entries));
} else {
print!("{}", format_feedback(&entries));
}
0
}
Err(e) => {
eprintln!("RPC error: {e}");
1
}
}
}
fn feedback_json(entries: &[registry::FeedbackEntry]) -> String {
let items: Vec<serde_json::Value> = entries
.iter()
.map(|e| {
let mut o = serde_json::json!({
"timestamp": e.timestamp,
"sender": e.sender,
"text": e.text,
});
if let Some(env) = parse_qa_envelope(&e.text) {
o["fleet_source"] = serde_json::json!(env.source);
o["body"] = serde_json::json!(env.body);
}
o
})
.collect();
serde_json::to_string_pretty(&serde_json::Value::Array(items))
.unwrap_or_else(|_| "[]".to_string())
+ "\n"
}
async fn feedback_submit(caller_name: Option<&str>, text: &str) -> i32 {
let text = text.trim();
if text.is_empty() {
eprintln!("feedback text is empty");
return 2;
}
if text.len() > 2048 {
eprintln!("feedback too long: {} bytes (max 2048)", text.len());
return 1;
}
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
println!("submitting {}-byte feedback on-chain …", text.len());
match registry::submit_feedback_sponsored(&signer, &sponsor, text, registry::ALPHA_USD_ADDRESS)
.await
{
Ok(tx) => {
println!("✓ feedback submitted\n tx: {tx}");
0
}
Err(e) => {
eprintln!("feedback failed: {e}");
1
}
}
}
fn addr_to_hex(a: [u8; 20]) -> String {
let mut s = String::from("0x");
for b in a {
s.push_str(&format!("{b:02x}"));
}
s
}
fn fmt_lh(wei: u128) -> String {
let whole = wei / 1_000_000_000_000_000_000u128;
let cents = (wei % 1_000_000_000_000_000_000u128) / 10_000_000_000_000_000u128;
format!("{whole}.{cents:02} LH")
}
async fn credits_show(caller_name: Option<&str>) -> i32 {
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let addr = addr_to_hex(wallet::address(&signer));
let token = registry::token_balance_of(&addr).await.unwrap_or(0);
let meter = registry::credit_balance_of(&addr).await.unwrap_or(0);
let expiry = registry::session_expiry_of(&addr).await.unwrap_or(0);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
println!("{addr}");
println!(" wallet {}", fmt_lh(token));
println!(" meter {} <- per-call billing debits this", fmt_lh(meter));
if expiry > now {
println!(
" session active ~{}min left (free; a funded meter now overrides it)",
(expiry - now) / 60
);
} else {
println!(" session none");
}
0
}
async fn redeem(caller_name: Option<&str>, code: &str) -> i32 {
let code = code.trim();
if code.is_empty() {
eprintln!("redeem: empty code");
return 2;
}
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
match registry::redeem_sponsored(&signer, &sponsor, code, registry::ALPHA_USD_ADDRESS).await {
Ok(tx) => {
println!("redeemed — $LH minted to your wallet tx: {tx}");
0
}
Err(e) => {
eprintln!("redeem failed: {e}");
1
}
}
}
async fn send_lh(caller_name: Option<&str>, recipient: &str, amount: &str) -> i32 {
use localharness::encoding::{classify_recipient, Recipient};
let to_hex = match classify_recipient(recipient) {
Ok(Recipient::Address(a)) => a,
Ok(Recipient::Name(n)) => match registry::owner_of_name(&n).await {
Ok(Some(o)) => o,
Ok(None) => {
eprintln!("send: '{n}' is not registered");
return 1;
}
Err(e) => {
eprintln!("send: RPC error resolving '{n}': {e}");
return 1;
}
},
Err(e) => {
eprintln!("send: {e}");
return 2;
}
};
let amount_wei = match localharness::encoding::parse_token_amount(amount) {
Some(w) if w > 0 => w,
_ => {
eprintln!("send: invalid amount '{amount}' (expected a positive number of $LH)");
return 2;
}
};
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
match registry::transfer_lh_sponsored(
&signer,
&sponsor,
&to_hex,
amount_wei,
registry::ALPHA_USD_ADDRESS,
)
.await
{
Ok(tx) => {
println!("sent {amount} $LH to {to_hex} tx: {tx}");
0
}
Err(e) => {
eprintln!("send failed: {e}");
1
}
}
}
async fn open_session(caller_name: Option<&str>) -> i32 {
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
match registry::open_session_sponsored(&signer, &sponsor, registry::ALPHA_USD_ADDRESS).await {
Ok(tx) => {
println!("session opened tx: {tx}");
0
}
Err(e) => {
eprintln!("open session failed: {e}");
1
}
}
}
async fn topup(caller_name: Option<&str>) -> i32 {
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let signer = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
let sponsor = match wallet::from_private_key_hex(SPONSOR_KEY) {
Ok(s) => s,
Err(e) => {
eprintln!("sponsor key error: {e}");
return 1;
}
};
let addr = addr_to_hex(wallet::address(&signer));
if registry::can_claim_credits(&addr).await.unwrap_or(false) {
match registry::claim_daily_sponsored(&signer, &sponsor, registry::ALPHA_USD_ADDRESS).await {
Ok(tx) => println!("claimed daily $LH tx: {tx}"),
Err(e) => eprintln!("claim failed (continuing to deposit): {e}"),
}
} else {
println!("daily allowance already claimed today (or none) - skipping claim");
}
let bal = registry::token_balance_of(&addr).await.unwrap_or(0);
if bal == 0 {
println!("wallet has 0 $LH - nothing to deposit");
return 0;
}
match registry::deposit_credits_sponsored(&signer, &sponsor, bal, registry::ALPHA_USD_ADDRESS)
.await
{
Ok(tx) => {
println!("deposited {} into the meter tx: {tx}", fmt_lh(bal));
0
}
Err(e) => {
eprintln!("deposit failed: {e}");
1
}
}
}
fn run_qa_checks() -> Vec<String> {
let mut fails = Vec::new();
let good = "fn frame(t: i32) { host::display::clear(0); host::display::present(); }";
match localharness::rustlite::compile(good) {
Ok(wasm) if !cartridge_has_entry(&wasm) => {
fails.push("a valid frame() cartridge compiled but has no frame/render export".into())
}
Ok(_) => {}
Err(e) => fails.push(format!("a known-good cartridge failed to compile: {e}")),
}
if localharness::rustlite::compile("this is not rustlite").is_ok() {
fails.push("the compiler ACCEPTED non-rustlite garbage (should error)".into());
}
if let Ok(wasm) = localharness::rustlite::compile("fn helper(n: i32) -> i32 { n + 1 }") {
if cartridge_has_entry(&wasm) {
fails.push("an entry-less cartridge wrongly reports a frame/render export".into());
}
}
fails
}
async fn probe_agent(caller_name: Option<&str>) -> i32 {
let (key_file, key_hex) = match resolve_caller_key(caller_name) {
Ok(c) => c,
Err(e) => {
eprintln!("{e}");
return 2;
}
};
let caller = match wallet::from_private_key_hex(&key_hex) {
Ok(s) => s,
Err(e) => {
eprintln!("bad key in {key_file}: {e}");
return 1;
}
};
if let Ok(sponsor) = wallet::from_private_key_hex(SPONSOR_KEY) {
let addr = addr_to_hex(wallet::address(&caller));
if registry::credit_balance_of(&addr).await.unwrap_or(0) < CALL_COST_WEI {
let _ = registry::deposit_credits_sponsored(
&caller,
&sponsor,
CALL_METER_TOPUP_WEI,
registry::ALPHA_USD_ADDRESS,
)
.await;
}
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let token = registry::proxy_auth_token(&caller, now);
let base = match url::Url::parse(registry::CREDIT_PROXY_URL) {
Ok(u) => u,
Err(e) => {
eprintln!("internal: bad proxy url: {e}");
return 1;
}
};
let qa_compile = localharness::ClosureTool::new(
"qa_compile",
"Compile rustlite source; report ok + wasm byte size + whether it exposes a \
frame/render entry, OR the compile error. Probe with valid and invalid sources.",
serde_json::json!({
"type": "object",
"properties": { "source": { "type": "string", "description": "rustlite source to compile" } },
"required": ["source"]
}),
|args: serde_json::Value, _ctx| async move {
let src = args.get("source").and_then(|v| v.as_str()).unwrap_or("");
eprintln!(" probing: compiling {} bytes …", src.len());
Ok(match localharness::rustlite::compile(src) {
Ok(wasm) => serde_json::json!({
"ok": true, "wasm_bytes": wasm.len(), "has_entry": cartridge_has_entry(&wasm)
}),
Err(e) => serde_json::json!({ "ok": false, "error": e.to_string() }),
})
},
);
let caps = localharness::types::CapabilitiesConfig {
enabled_tools: Some(vec![]),
enable_subagents: false,
..Default::default()
};
let policies = vec![localharness::deny_all(), localharness::Policy::allow("qa_compile")];
let cfg = localharness::GeminiAgentConfig::new(token)
.with_base_url(base)
.with_system_instructions(
"You are qa-observe, a READ-ONLY QA agent for localharness. Use qa_compile to \
probe the rustlite compiler, then ANSWER IN TEXT with your findings: a short \
numbered list of concrete issues you actually observed, or exactly 'no issues \
found'. Be terse.",
)
.with_capabilities(caps)
.with_policies(policies)
.with_tool(qa_compile);
println!("running observe-agent probe (live, via proxy) …");
let agent = match localharness::Agent::start_gemini(cfg).await {
Ok(a) => a,
Err(e) => {
eprintln!("could not start agent: {e}");
return 1;
}
};
let mut findings = String::new();
let mut nudge = "Probe the rustlite compiler: try a valid `fn frame(t: i32)` cartridge that \
draws, an obviously invalid source, and one edge case via qa_compile."
.to_string();
for _ in 0..5 {
match agent.chat(nudge.as_str()).await {
Ok(r) => {
let t = r.text().await.unwrap_or_default();
if !t.trim().is_empty() {
findings = t;
break;
}
}
Err(e) => {
let _ = agent.shutdown().await;
eprintln!("agent run failed: {e}");
return 1;
}
}
nudge = "Based on the qa_compile results so far, state your concrete findings now as a \
short numbered list in text, or exactly 'no issues found'."
.to_string();
}
let _ = agent.shutdown().await;
println!("--- agent findings ---\n{}", findings.trim());
if findings.to_lowercase().contains("no issues") || findings.trim().is_empty() {
println!("(agent reported no issues — nothing filed)");
return 0;
}
let mut env = format!(
"qa/v1 source=qa-observe v{}: {}",
env!("CARGO_PKG_VERSION"),
findings.replace('\n', " ")
);
if env.len() > 2048 {
let mut cut = 2048;
while cut > 0 && !env.is_char_boundary(cut) {
cut -= 1;
}
env.truncate(cut);
}
let _ = feedback_submit(caller_name, &env).await;
0
}
async fn probe(caller_name: Option<&str>) -> i32 {
let mut fails = run_qa_checks();
match registry::owner_of_name("claude").await {
Ok(Some(_)) => {}
Ok(None) => fails.push("registry reports claude.localharness.xyz unregistered".into()),
Err(e) => fails.push(format!("chain read failed: {e}")),
}
if fails.is_empty() {
println!("✓ probe: all platform checks passed");
return 0;
}
eprintln!("probe found {} issue(s):", fails.len());
for f in &fails {
eprintln!(" - {f}");
}
let mut envelope = format!(
"qa/v1 source=qa-probe v{}: {}",
env!("CARGO_PKG_VERSION"),
fails.join(" | ")
);
if envelope.len() > 2048 {
let mut cut = 2048;
while cut > 0 && !envelope.is_char_boundary(cut) {
cut -= 1;
}
envelope.truncate(cut);
}
if feedback_submit(caller_name, &envelope).await == 0 {
eprintln!(" → reported on-chain");
}
1
}
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn parse_addr20(s: &str) -> Option<[u8; 20]> {
let t = s.trim().trim_start_matches("0x").trim_start_matches("0X");
if t.len() != 40 {
return None;
}
let mut out = [0u8; 20];
for (i, slot) in out.iter_mut().enumerate() {
*slot = u8::from_str_radix(t.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}
fn name_is_valid(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 63
&& !name.starts_with('-')
&& !name.ends_with('-')
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
#[cfg(test)]
mod tests {
use super::*;
fn args(parts: &[&str]) -> Vec<String> {
parts.iter().map(|s| s.to_string()).collect()
}
#[test]
fn gitignore_already_covers_detects_wildcard_and_exact() {
assert!(gitignore_already_covers("target/\n*.localharness.key\n", "alice.localharness.key"));
assert!(gitignore_already_covers("alice.localharness.key\n", "alice.localharness.key"));
assert!(gitignore_already_covers(" *.localharness.key \n", "x.localharness.key"));
assert!(!gitignore_already_covers("target/\nnode_modules/\n", "alice.localharness.key"));
assert!(!gitignore_already_covers("bob.localharness.key\n", "alice.localharness.key"));
assert!(!gitignore_already_covers("", "alice.localharness.key"));
}
#[test]
fn parse_create_args_name_only_and_with_persona() {
let (n, p) = parse_create_args(&args(&["alice"])).unwrap();
assert_eq!(n, "alice");
assert_eq!(p, None);
let (n, p) = parse_create_args(&args(&["alice", "--persona", "you", "are", "alice"]))
.unwrap();
assert_eq!(n, "alice");
assert_eq!(p.as_deref(), Some("you are alice"));
}
#[test]
fn parse_create_args_rejects_bad_forms() {
assert!(parse_create_args(&args(&[])).is_err()); assert!(parse_create_args(&args(&["alice", "--persona"])).is_err()); assert!(parse_create_args(&args(&["alice", "bob"])).is_err()); }
#[test]
fn parse_call_plain_target_and_message() {
let p = parse_call_args(&args(&["alice", "how", "are", "you"])).unwrap();
assert_eq!(p.caller, None);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "how are you");
}
#[test]
fn parse_call_single_word_message() {
let p = parse_call_args(&args(&["alice", "hello"])).unwrap();
assert_eq!(p.caller, None);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "hello");
}
#[test]
fn parse_call_with_as_flag() {
let p = parse_call_args(&args(&["--as", "bob", "alice", "what's", "up"])).unwrap();
assert_eq!(p.caller.as_deref(), Some("bob"));
assert!(!p.fresh);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "what's up");
}
#[test]
fn parse_call_fresh_flag() {
let p = parse_call_args(&args(&["--fresh", "alice", "hi"])).unwrap();
assert!(p.fresh);
assert_eq!(p.caller, None);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "hi");
}
#[test]
fn parse_call_flags_order_independent() {
let a = parse_call_args(&args(&["--as", "bob", "--fresh", "alice", "hi"])).unwrap();
let b = parse_call_args(&args(&["--fresh", "--as", "bob", "alice", "hi"])).unwrap();
for p in [a, b] {
assert_eq!(p.caller.as_deref(), Some("bob"));
assert!(p.fresh);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "hi");
}
}
#[test]
fn parse_call_accepts_model_flag_in_any_order() {
let perms = [
vec!["--model", "claude-opus", "--as", "bob", "--fresh", "alice", "hi"],
vec!["--fresh", "--model", "claude-opus", "--as", "bob", "alice", "hi"],
vec!["--as", "bob", "--model", "claude-opus", "--fresh", "alice", "hi"],
];
for parts in perms {
let p = parse_call_args(&args(&parts)).unwrap();
assert_eq!(p.caller.as_deref(), Some("bob"));
assert_eq!(p.model.as_deref(), Some("claude-opus"));
assert!(p.fresh);
assert_eq!(p.target, "alice");
assert_eq!(p.message, "hi");
}
assert!(parse_call_args(&args(&["--model"])).is_err());
}
#[test]
fn parse_call_defaults_to_not_fresh() {
let p = parse_call_args(&args(&["alice", "hi"])).unwrap();
assert!(!p.fresh);
}
#[test]
fn parse_call_message_preserves_internal_spacing_as_single_spaces() {
let p = parse_call_args(&args(&["alice", "a", "b", "c"])).unwrap();
assert_eq!(p.message, "a b c");
}
#[test]
fn parse_call_rejects_missing_message() {
assert!(parse_call_args(&args(&["alice"])).is_err());
}
#[test]
fn parse_mcp_call_defaults_and_flags() {
let p = parse_mcp_call_args(&args(&["claude", "hi", "there"])).unwrap();
assert_eq!(p.caller, None);
assert_eq!(p.pay, MCP_CALL_DEFAULT_PAY);
assert_eq!(p.target, "claude");
assert_eq!(p.message, "hi there");
for parts in [
vec!["--as", "bob", "--pay", "0.5", "claude", "yo"],
vec!["--pay", "0.5", "--as", "bob", "claude", "yo"],
] {
let p = parse_mcp_call_args(&args(&parts)).unwrap();
assert_eq!(p.caller.as_deref(), Some("bob"));
assert_eq!(p.pay, "0.5");
assert_eq!(p.target, "claude");
assert_eq!(p.message, "yo");
}
}
#[test]
fn parse_mcp_call_rejects_bad_forms() {
assert!(parse_mcp_call_args(&args(&[])).is_err()); assert!(parse_mcp_call_args(&args(&["claude"])).is_err()); assert!(parse_mcp_call_args(&args(&["--as"])).is_err()); assert!(parse_mcp_call_args(&args(&["--pay"])).is_err()); assert!(parse_mcp_call_args(&args(&["--pay", "1", "claude"])).is_err()); }
#[test]
fn mcp_call_pay_parses_to_18_decimal_wei() {
assert_eq!(
localharness::encoding::parse_token_amount(MCP_CALL_DEFAULT_PAY),
Some(1_000_000_000_000_000) );
assert_eq!(
localharness::encoding::parse_token_amount("1"),
Some(1_000_000_000_000_000_000)
);
}
#[test]
fn mcp_x402_header_json_matches_proxy_shape() {
let from = "0x00000000000000000000000000000000000000aa";
let to = "0x00000000000000000000000000000000000000bb";
let nonce = [0x11u8; 32];
let sig = [0x22u8; 65];
let j = mcp_x402_header_json(from, to, 1_000_000_000_000_000, 0, 1_999_999_999, &nonce, &sig);
assert_eq!(j["from"], from);
assert_eq!(j["to"], to);
assert_eq!(j["value"], "1000000000000000");
assert!(j["value"].is_string());
assert_eq!(j["validAfter"], 0);
assert_eq!(j["validBefore"], 1_999_999_999u64);
let nonce_s = j["nonce"].as_str().unwrap();
let sig_s = j["signature"].as_str().unwrap();
assert_eq!(nonce_s.len(), 2 + 64);
assert!(nonce_s.starts_with("0x"));
assert_eq!(sig_s.len(), 2 + 130);
assert!(sig_s.starts_with("0x"));
}
#[test]
fn mcp_tools_call_body_is_ask_agent_jsonrpc() {
let b = mcp_tools_call_body("claude", "hello");
assert_eq!(b["jsonrpc"], "2.0");
assert_eq!(b["method"], "tools/call");
assert_eq!(b["params"]["name"], "ask_agent");
assert_eq!(b["params"]["arguments"]["name"], "claude");
assert_eq!(b["params"]["arguments"]["message"], "hello");
}
#[test]
fn mcp_random_nonce_is_32_bytes_and_fresh() {
let a = registry::random_x402_nonce();
let b = registry::random_x402_nonce();
assert_eq!(a.len(), 32);
assert_eq!(b.len(), 32);
assert_ne!(a, b);
}
#[test]
fn mcp_endpoint_is_proxy_slash_mcp() {
let url = mcp_endpoint_url();
assert!(url.ends_with("/mcp"));
assert!(!url.contains("//mcp")); }
#[test]
fn parse_call_rejects_empty() {
assert!(parse_call_args(&args(&[])).is_err());
}
#[test]
fn parse_call_rejects_as_without_name() {
assert!(parse_call_args(&args(&["--as"])).is_err());
}
#[test]
fn parse_call_rejects_as_name_without_target_or_message() {
assert!(parse_call_args(&args(&["--as", "bob"])).is_err());
assert!(parse_call_args(&args(&["--as", "bob", "alice"])).is_err());
}
#[test]
fn thread_file_target_parses_own_files_only() {
assert_eq!(
thread_file_target("claude", "claude__alice.gemini.bin").as_deref(),
Some("alice")
);
assert_eq!(
thread_file_target("claude", "claude__alice.anthropic.bin").as_deref(),
Some("alice")
);
assert_eq!(
thread_file_target("claude", "claude__alice.bin").as_deref(),
Some("alice")
);
assert_eq!(
thread_file_target("claude", "claude__a__b.gemini.bin").as_deref(),
Some("a__b")
);
assert_eq!(thread_file_target("claude", "bob__alice.gemini.bin"), None);
assert_eq!(thread_file_target("claude", "claude__alice.txt"), None);
assert_eq!(thread_file_target("claude", "claude__.bin"), None);
assert_eq!(thread_file_target("claude", "claude__.gemini.bin"), None);
assert_eq!(thread_file_target("claude", "unrelated.bin"), None);
}
#[test]
fn thread_file_target_roundtrips_history_path() {
for backend in ["gemini", "anthropic"] {
let p = history_path("claude", "alice", backend);
let name = p.file_name().unwrap().to_str().unwrap();
assert_eq!(thread_file_target("claude", name).as_deref(), Some("alice"));
}
}
#[test]
fn model_backend_tag_routes_claude_to_anthropic() {
assert_eq!(model_backend_tag(Some("claude-opus-4")), "anthropic");
assert_eq!(model_backend_tag(Some("claude")), "anthropic");
assert_eq!(model_backend_tag(Some("gemini-3.5-flash")), "gemini");
assert_eq!(model_backend_tag(None), "gemini");
}
#[test]
fn history_path_keys_on_backend_so_formats_never_collide() {
let g = history_path("claude", "alice", "gemini");
let a = history_path("claude", "alice", "anthropic");
assert_ne!(g, a, "backends must not share a history file");
assert!(g.ends_with("claude__alice.gemini.bin"));
assert!(a.ends_with("claude__alice.anthropic.bin"));
}
#[test]
fn take_as_flag_extracts_caller() {
let a = args(&["--as", "bob", "threads"]);
let (caller, rest) = take_as_flag(&a).unwrap();
assert_eq!(caller.as_deref(), Some("bob"));
assert_eq!(rest, vec!["threads".to_string()]);
let b = args(&["alice"]);
let (caller, rest) = take_as_flag(&b).unwrap();
assert_eq!(caller, None);
assert_eq!(rest, vec!["alice".to_string()]);
assert!(take_as_flag(&args(&["--as"])).is_err());
}
#[test]
fn take_as_flag_scans_any_position() {
let (caller, rest) = take_as_flag(&args(&["--deep", "--as", "fleet"])).unwrap();
assert_eq!(caller.as_deref(), Some("fleet"));
assert_eq!(rest, vec!["--deep".to_string()]);
let (caller, rest) = take_as_flag(&args(&["a", "b", "--as", "me", "c"])).unwrap();
assert_eq!(caller.as_deref(), Some("me"));
assert_eq!(rest, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
assert!(take_as_flag(&args(&["--deep", "--as"])).is_err());
assert!(take_as_flag(&args(&["--as", "a", "--as", "b"])).is_err());
}
#[test]
fn history_path_keys_on_caller_and_target() {
let p = history_path("claude", "alice", "gemini");
assert!(p.ends_with("claude__alice.gemini.bin"));
assert_ne!(
history_path("claude", "alice", "gemini"),
history_path("bob", "alice", "gemini")
);
assert_ne!(
history_path("claude", "alice", "gemini"),
history_path("claude", "bob", "gemini")
);
assert!(p.starts_with(".localharness"));
}
#[test]
fn format_whoami_unregistered_is_one_line() {
let info = WhoamiInfo {
name: "ghost".into(),
owner: None,
token_id: 0,
tba: None,
has_persona: false,
public_face: None,
};
assert_eq!(format_whoami(&info), "ghost is unregistered");
}
#[test]
fn format_whoami_full_profile() {
let info = WhoamiInfo {
name: "claude".into(),
owner: Some("0xabc".into()),
token_id: 8,
tba: Some("0xdef".into()),
has_persona: true,
public_face: Some("app".into()),
};
let out = format_whoami(&info);
assert!(out.starts_with("claude.localharness.xyz\n"));
assert!(out.contains("owner 0xabc"));
assert!(out.contains("tokenId 8"));
assert!(out.contains("wallet 0xdef (token-bound account)"));
assert!(out.contains("persona published"));
assert!(out.contains("face app"));
}
#[test]
fn format_whoami_absent_persona_and_face() {
let info = WhoamiInfo {
name: "bare".into(),
owner: Some("0x1".into()),
token_id: 3,
tba: None,
has_persona: false,
public_face: None,
};
let out = format_whoami(&info);
assert!(out.contains("persona none"));
assert!(out.contains("face unset (directory)"));
assert!(out.contains("wallet —"));
}
#[test]
fn format_whoami_json_registered_roundtrips() {
let info = WhoamiInfo {
name: "claude".into(),
owner: Some("0xabc".into()),
token_id: 8,
tba: Some("0xdef".into()),
has_persona: true,
public_face: Some("app".into()),
};
let v: serde_json::Value = serde_json::from_str(&format_whoami_json(&info)).unwrap();
assert_eq!(v["name"], "claude");
assert_eq!(v["registered"], true);
assert_eq!(v["owner"], "0xabc");
assert_eq!(v["tokenId"], 8);
assert_eq!(v["wallet"], "0xdef");
assert_eq!(v["persona"], true);
assert_eq!(v["face"], "app");
}
#[test]
fn format_whoami_json_unregistered_nulls() {
let info = WhoamiInfo {
name: "ghost".into(),
owner: None,
token_id: 0,
tba: None,
has_persona: false,
public_face: None,
};
let v: serde_json::Value = serde_json::from_str(&format_whoami_json(&info)).unwrap();
assert_eq!(v["registered"], false);
assert!(v["owner"].is_null());
assert!(v["wallet"].is_null());
assert!(v["face"].is_null());
assert_eq!(v["persona"], false);
}
#[test]
fn hint_for_call_error_classifies_common_failures() {
for s in [
"HTTP 402 Payment Required",
"proxy: no session for 0xabc",
"insufficient credit",
] {
assert!(
hint_for_call_error(s).unwrap().contains("$LH"),
"expected $LH hint for {s:?}"
);
}
for s in ["401 Unauthorized", "bad signature", "403 Forbidden"] {
assert!(
hint_for_call_error(s).unwrap().contains("signature"),
"expected auth hint for {s:?}"
);
}
assert!(hint_for_call_error("429 Too Many Requests")
.unwrap()
.contains("rate limited"));
}
#[test]
fn hint_for_call_error_is_case_insensitive_and_silent_on_unknown() {
assert!(hint_for_call_error("PAYMENT REQUIRED").is_some());
assert_eq!(hint_for_call_error("connection reset by peer"), None);
assert_eq!(hint_for_call_error("some unrelated parse error"), None);
}
#[test]
fn rustlite_compiles_a_minimal_cartridge() {
let src = "fn frame(t: i32) {\n \
let w: i32 = host::display::width();\n \
host::display::clear(0);\n \
host::display::fill_rect(0, 0, w, 8, 16777215);\n \
host::display::present();\n}";
let wasm = localharness::rustlite::compile(src).expect("minimal cartridge compiles");
assert_eq!(&wasm[0..4], b"\0asm", "valid wasm magic header");
assert!(wasm.len() <= PUBLISH_CAP);
}
#[test]
fn rustlite_rejects_garbage() {
assert!(localharness::rustlite::compile("this is not rustlite").is_err());
}
#[test]
fn cartridge_entry_detection() {
let with =
localharness::rustlite::compile("fn frame(t: i32) { host::display::present(); }")
.unwrap();
assert!(cartridge_has_entry(&with), "frame() must be detected");
let without = localharness::rustlite::compile("fn helper(n: i32) -> i32 { n + 1 }").unwrap();
assert!(!cartridge_has_entry(&without), "no entry must be rejected");
let bitmask = localharness::rustlite::compile(include_str!("../../bitmask.rl")).unwrap();
assert!(cartridge_has_entry(&bitmask));
assert!(!cartridge_has_entry(b""));
assert!(!cartridge_has_entry(b"\0asm")); assert!(!cartridge_has_entry(b"\0asm\x01\0\0\0\x07\xff")); }
#[test]
fn name_validation_matches_registry_rule() {
assert!(name_is_valid("alice"));
assert!(name_is_valid("a-1-b"));
assert!(!name_is_valid("Alice")); assert!(!name_is_valid("a_b")); assert!(!name_is_valid("")); assert!(name_is_valid(&"a".repeat(63)));
assert!(!name_is_valid(&"a".repeat(64))); assert!(!name_is_valid("🤖")); assert!(!name_is_valid("-foo")); assert!(!name_is_valid("foo-")); assert!(!name_is_valid("-")); assert!(name_is_valid("a-b-c")); }
#[test]
fn usage_documents_every_command() {
for cmd in [
"create", "compile", "publish", "face", "persona", "call", "list",
"feedback", "probe", "triage", "threads", "forget", "whoami",
] {
assert!(
USAGE.contains(cmd),
"`{cmd}` is dispatchable but missing from the help/USAGE text"
);
}
}
#[test]
fn parse_list_flags_handles_as_and_json_any_order() {
assert_eq!(parse_list_flags(&args(&[])).unwrap(), (None, false));
assert_eq!(parse_list_flags(&args(&["--json"])).unwrap(), (None, true));
let (c, j) = parse_list_flags(&args(&["--as", "bob", "--json"])).unwrap();
assert_eq!((c.as_deref(), j), (Some("bob"), true));
let (c, j) = parse_list_flags(&args(&["--json", "--as", "bob"])).unwrap();
assert_eq!((c.as_deref(), j), (Some("bob"), true));
assert!(parse_list_flags(&args(&["--as"])).is_err()); assert!(parse_list_flags(&args(&["alice"])).is_err()); }
#[test]
fn format_owned_text_and_json() {
let toks = vec![
registry::OwnedToken { token_id: 8, name: "claude".into(), tba: Some("0xabc".into()) },
registry::OwnedToken { token_id: 3, name: "alice".into(), tba: None },
];
let text = format_owned("0xowner", &toks, false);
assert!(text.contains("2 subdomain"));
assert!(text.contains("claude (tokenId 8) 0xabc"));
assert!(text.contains("alice (tokenId 3) —"));
let v: serde_json::Value =
serde_json::from_str(&format_owned("0xowner", &toks, true)).unwrap();
assert_eq!(v["count"], 2);
assert_eq!(v["owner"], "0xowner");
assert_eq!(v["subdomains"][0]["name"], "claude");
assert_eq!(v["subdomains"][0]["tokenId"], 8);
assert!(v["subdomains"][1]["wallet"].is_null());
}
#[test]
fn read_file_clean_maps_not_found_without_leaking_os_error() {
let err = read_file_clean("definitely-nonexistent-file-xyz123.rl").unwrap_err();
assert!(err.contains("file not found"), "got: {err}");
assert!(err.contains("definitely-nonexistent-file-xyz123.rl"), "got: {err}");
assert!(!err.contains("os error"), "must not leak raw OS error: {err}");
}
#[test]
fn looks_like_path_distinguishes_files_from_prose() {
assert!(looks_like_path("persona.txt"));
assert!(looks_like_path("prompts/agent.md"));
assert!(looks_like_path("C:\\agents\\bob.prompt"));
assert!(looks_like_path("./x.rl"));
assert!(!looks_like_path("You are bob, a helpful agent"));
assert!(!looks_like_path("bob"));
}
#[test]
fn resolve_persona_arg_literal_text_passthrough() {
let p = resolve_persona_arg("You are bob, answer tersely").unwrap();
assert_eq!(p, "You are bob, answer tersely");
}
#[test]
fn resolve_persona_arg_missing_file_is_clean_error() {
let err = resolve_persona_arg("definitely-nonexistent-xyz123.txt").unwrap_err();
assert!(err.contains("file not found"), "got: {err}");
assert!(!err.contains("os error"), "must not leak raw OS error: {err}");
}
#[test]
fn qa_checks_pass_on_a_healthy_platform() {
let fails = run_qa_checks();
assert!(fails.is_empty(), "probe found issues on a healthy build: {fails:?}");
}
#[test]
fn triage_dedups_and_ranks_by_recurrence() {
let bodies = vec![
"Compile leaks OS error".to_string(),
"compile leaks os error".to_string(), " Compile leaks OS error ".to_string(), "whoami is slow".to_string(),
];
let ranked = triage_findings(&bodies);
assert_eq!(ranked.len(), 2, "two distinct issues after dedup");
assert_eq!(ranked[0].1, 3, "the recurring one ranks first with count 3");
assert!(ranked[0].0.to_lowercase().contains("compile leaks"));
assert_eq!(ranked[1].1, 1);
}
#[test]
fn triage_skips_empty_bodies() {
let bodies = vec!["".to_string(), " ".to_string(), "real bug".to_string()];
let ranked = triage_findings(&bodies);
assert_eq!(ranked, vec![("real bug".to_string(), 1)]);
}
#[test]
fn parse_qa_envelope_accepts_valid_rejects_others() {
let env =
parse_qa_envelope("qa/v1 source=qa-probe v0.20.0: compile leaked os error").unwrap();
assert_eq!(env.source, "qa-probe");
assert_eq!(env.version, "0.20.0");
assert!(env.body.contains("compile leaked"));
assert!(parse_qa_envelope("just some human feedback").is_none());
assert!(parse_qa_envelope("qa/v1 source=x v1.0.0: ").is_none()); assert!(parse_qa_envelope("qa/v1 no source or colon").is_none());
assert!(parse_qa_envelope("qa/v1 source=x vNOTVERSION: body").is_none());
}
#[test]
fn feedback_json_emits_fields_and_fleet_envelope() {
let entries = vec![
registry::FeedbackEntry {
sender: "0xabc".into(),
timestamp: 100,
text: "[BUG] something broke".into(),
},
registry::FeedbackEntry {
sender: "0xdef".into(),
timestamp: 200,
text: "qa/v1 source=qa-probe v0.20.0: a real bug".into(),
},
];
let v: serde_json::Value = serde_json::from_str(&feedback_json(&entries)).unwrap();
let arr = v.as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[0]["sender"], "0xabc");
assert_eq!(arr[0]["timestamp"], 100);
assert_eq!(arr[0]["text"], "[BUG] something broke");
assert!(arr[0].get("fleet_source").is_none());
assert_eq!(arr[1]["fleet_source"], "qa-probe");
assert!(arr[1]["body"].as_str().unwrap().contains("a real bug"));
let empty: serde_json::Value = serde_json::from_str(&feedback_json(&[])).unwrap();
assert_eq!(empty.as_array().unwrap().len(), 0);
}
#[test]
fn format_feedback_tags_fleet_envelopes_only() {
let entries = vec![
registry::FeedbackEntry {
sender: "0x1".into(),
timestamp: 1,
text: "qa/v1 source=qa-probe v0.20.0: a real bug".into(),
},
registry::FeedbackEntry {
sender: "0x2".into(),
timestamp: 2,
text: "a human note".into(),
},
];
let out = format_feedback(&entries);
assert!(out.contains("[fleet:qa-probe]"));
assert!(
out.lines().any(|l| l.contains("0x2") && !l.contains("[fleet")),
"human feedback must not be tagged as fleet"
);
}
#[test]
fn format_feedback_empty_and_entries() {
assert!(format_feedback(&[]).contains("no on-chain feedback"));
let entries = vec![
registry::FeedbackEntry {
sender: "0xabc".into(),
timestamp: 1700000000,
text: "create flow worked\nbut whoami was slow".into(),
},
];
let out = format_feedback(&entries);
assert!(out.contains("1 on-chain feedback"));
assert!(out.contains("0xabc"));
assert!(out.contains("create flow worked but whoami was slow"));
}
#[test]
fn format_owned_empty() {
assert!(format_owned("0xo", &[], false).contains("no subdomains"));
let v: serde_json::Value = serde_json::from_str(&format_owned("0xo", &[], true)).unwrap();
assert_eq!(v["count"], 0);
assert!(v["subdomains"].as_array().unwrap().is_empty());
}
#[test]
fn sponsor_key_is_valid_and_derives_documented_address() {
let signer = wallet::from_private_key_hex(SPONSOR_KEY).expect("SPONSOR_KEY must parse");
let addr = format!("0x{}", to_hex(&wallet::address(&signer)));
assert_eq!(
addr.to_ascii_lowercase(),
"0x0aff88ad13ef24cac5befd0f9dc3a05df79a922c",
"SPONSOR_KEY no longer derives the documented sponsor address"
);
}
#[test]
fn llms_txt_publishes_canonical_onchain_constants() {
let spec = include_str!("../../web/llms.txt");
assert!(
spec.contains(registry::REGISTRY_ADDRESS),
"llms.txt missing canonical diamond address {}",
registry::REGISTRY_ADDRESS
);
assert!(
spec.contains(registry::LOCALHARNESS_TOKEN_ADDRESS),
"llms.txt missing the $LH token address {}",
registry::LOCALHARNESS_TOKEN_ADDRESS
);
assert!(
spec.contains(registry::RPC_URL),
"llms.txt missing the RPC URL {}",
registry::RPC_URL
);
assert!(
spec.contains(®istry::CHAIN_ID.to_string()),
"llms.txt missing chain id {}",
registry::CHAIN_ID
);
}
#[test]
fn parse_addr20_roundtrips_registry_address() {
let a = parse_addr20(registry::REGISTRY_ADDRESS).expect("valid registry addr");
assert_eq!(a.len(), 20);
assert_eq!(parse_addr20("0x00"), None); assert!(parse_addr20(&"0".repeat(40)).is_some());
}
}