- providers add|list|remove|describe|test|free|reference: the person's own providers in their registry, a credential read from standard input (never a command line) into the keychain, or named by an environment variable; describe and the conformance test; what anyone can use free; and the reference provider served by this binary, as a template. - run --provider NAME [--dry-run] [--yes]: the plan with the exact native compile, its check and the provider's estimate; a run that is not free refused (exit 3) until --yes; the counts sealed with this user's key, checked by verify. - MCP: providers_list, provider_estimate and run_on_provider; the last says it can spend the person's money and refuses without confirm_cost: true when the run is not free. - A `providers` feature (default) carries it, so the WASI build without default features stays as it was. CI: the command line's workflow follows wai-quantum-qpu and pins the five end-to-end tests by name. Co-Authored-By: Claude Opus 5.5 <[email protected]>
280 lines
16 KiB
Rust
280 lines
16 KiB
Rust
//! `wai-quantum providers …`, `run --provider` and the MCP tools, as a person
|
|
//! and an agent use them, against the reference provider that this binary
|
|
//! serves itself (`wai-quantum providers reference`), over a local process
|
|
//! and over HTTP. Each run has its own configuration directory, and its
|
|
//! credentials go to files there (`WAI_QUANTUM_CREDENTIALS=file`), so no
|
|
//! keychain is touched. Nothing leaves this machine and nothing spends money:
|
|
//! the priced provider is the reference with a price in its configuration.
|
|
#![cfg(feature = "providers")]
|
|
|
|
use serde_json::{Value, json};
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Child, Command, Output, Stdio};
|
|
|
|
const SECRET: &str = "wai-cli-test-credential-93ad1b";
|
|
const BELL: &str = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
|
|
|
|
fn bin() -> &'static str {
|
|
env!("CARGO_BIN_EXE_wai-quantum")
|
|
}
|
|
|
|
/// A configuration directory of its own, and everything printed in it.
|
|
struct Home {
|
|
dir: PathBuf,
|
|
seen: std::cell::RefCell<String>,
|
|
}
|
|
|
|
impl Home {
|
|
fn new(tag: &str) -> Home {
|
|
let dir = std::env::temp_dir().join(format!("wai-quantum-cli-byoq-{tag}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
Home { dir, seen: Default::default() }
|
|
}
|
|
|
|
fn cmd(&self) -> Command {
|
|
let mut c = Command::new(bin());
|
|
c.env("XDG_CONFIG_HOME", self.dir.join("config")).env("HOME", &self.dir).env("WAI_QUANTUM_CREDENTIALS", "file").env_remove("WAI_QUANTUM_PROVIDERS").env_remove("WAI_QUANTUM_KEY");
|
|
c
|
|
}
|
|
|
|
/// Runs `args` with `stdin`, keeping what it printed.
|
|
fn run(&self, args: &[&str], stdin: &str) -> Output {
|
|
let mut child = self.cmd().args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
|
child.stdin.take().unwrap().write_all(stdin.as_bytes()).unwrap();
|
|
let out = child.wait_with_output().unwrap();
|
|
let mut seen = self.seen.borrow_mut();
|
|
seen.push_str(&String::from_utf8_lossy(&out.stdout));
|
|
seen.push_str(&String::from_utf8_lossy(&out.stderr));
|
|
out
|
|
}
|
|
|
|
fn ok(&self, args: &[&str], stdin: &str) -> String {
|
|
let out = self.run(args, stdin);
|
|
assert!(out.status.success(), "{args:?}: {}\n{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
|
String::from_utf8(out.stdout).unwrap()
|
|
}
|
|
|
|
fn program(&self) -> String {
|
|
let p = self.dir.join("bell.qasm");
|
|
std::fs::write(&p, BELL).unwrap();
|
|
p.display().to_string()
|
|
}
|
|
|
|
/// Every file under the directory, as text.
|
|
fn files(&self) -> String {
|
|
fn walk(p: &Path, out: &mut String) {
|
|
for e in std::fs::read_dir(p).into_iter().flatten().flatten() {
|
|
let p = e.path();
|
|
if p.is_dir() {
|
|
walk(&p, out);
|
|
} else if let Ok(t) = std::fs::read_to_string(&p) {
|
|
out.push_str(&format!("== {}\n{t}\n", p.display()));
|
|
}
|
|
}
|
|
}
|
|
let mut s = String::new();
|
|
walk(&self.dir, &mut s);
|
|
s
|
|
}
|
|
}
|
|
|
|
impl Drop for Home {
|
|
fn drop(&mut self) {
|
|
let _ = std::fs::remove_dir_all(&self.dir);
|
|
}
|
|
}
|
|
|
|
fn reference_command(extra: &[&str]) -> Vec<String> {
|
|
let mut c = vec![bin().to_string(), "providers".into(), "reference".into(), "--stdio".into()];
|
|
c.extend(extra.iter().map(|s| s.to_string()));
|
|
c
|
|
}
|
|
|
|
fn add_process(h: &Home, name: &str, extra: &[&str], credential: bool) -> String {
|
|
let mut args = vec!["providers", "add", name, "--kind", "process"];
|
|
if credential {
|
|
args.push("--credential-stdin");
|
|
}
|
|
args.push("--");
|
|
let command = reference_command(extra);
|
|
let refs: Vec<&str> = command.iter().map(String::as_str).collect();
|
|
args.extend(refs);
|
|
h.ok(&args, if credential { SECRET } else { "" })
|
|
}
|
|
|
|
#[test]
|
|
fn a_person_adds_tests_plans_and_runs_on_a_provider_and_the_receipt_verifies() {
|
|
let h = Home::new("person");
|
|
let prog = h.program();
|
|
assert!(h.ok(&["providers", "list"], "").contains("no providers yet"));
|
|
add_process(&h, "lab", &[], false);
|
|
let list = h.ok(&["providers", "list", "--json"], "");
|
|
let v: Value = serde_json::from_str(&list).unwrap();
|
|
assert_eq!((v["providers"][0]["name"].as_str(), v["providers"][0]["protocol"].as_str()), (Some("lab"), Some("wai.quantum.provider/1")));
|
|
let d = h.ok(&["providers", "describe", "lab"], "");
|
|
assert!(d.contains("reference:line-5, 5 qubits") && d.contains("billing free"), "{d}");
|
|
let t = h.ok(&["providers", "test", "lab"], "");
|
|
assert!(t.contains("31 passed, 0 failed, 0 skipped"), "{t}");
|
|
let free = h.ok(&["providers", "free"], "");
|
|
assert!(free.contains("exact") && free.contains("quantum-jobs") && free.contains("lab") && free.contains("free, as it states"), "{free}");
|
|
// The dry run: the compile, its check and the estimate, nothing sent.
|
|
let dry = h.ok(&["run", &prog, "--shots", "300", "--provider", "lab", "--dry-run"], "");
|
|
assert!(dry.contains("compile 2 qubits, 300 shots") && dry.contains("estimate free") && dry.contains("nothing has been sent"), "{dry}");
|
|
// The run, sealed with this user's key, and checked by verify.
|
|
let body = h.ok(&["run", &prog, "--shots", "300", "--seed", "4", "--provider", "lab", "--json"], "");
|
|
let job: Value = serde_json::from_str(&body).unwrap();
|
|
assert_eq!((job["method"].as_str(), job["result"]["accuracy"]["class"].as_str()), (Some("provider"), Some("measured")));
|
|
assert_eq!(job["request"]["provider"], "lab");
|
|
let saved = h.dir.join("job.json");
|
|
std::fs::write(&saved, &body).unwrap();
|
|
let check = h.ok(&["verify", saved.to_str().unwrap()], "");
|
|
assert!(check.contains("signature VALID"), "{check}");
|
|
// As a person reads it.
|
|
let text = h.ok(&["run", &prog, "--provider", "lab"], "");
|
|
assert!(text.contains("accuracy measured") && text.contains("ideal total variation distance") && text.contains("cost free") && text.contains("receipt verifies"), "{text}");
|
|
// A job the engine's methods would take and a device cannot is refused.
|
|
let out = h.run(&["run", &prog, "--provider", "lab", "-o", "Z0"], "");
|
|
assert!(!out.status.success() && String::from_utf8_lossy(&out.stderr).contains("counts"));
|
|
assert!(h.ok(&["providers", "remove", "lab"], "").contains("removed lab"));
|
|
assert!(!h.run(&["providers", "describe", "lab"], "").status.success());
|
|
}
|
|
|
|
#[test]
|
|
fn a_run_that_is_not_free_is_refused_until_the_person_confirms_it() {
|
|
let h = Home::new("priced");
|
|
let prog = h.program();
|
|
add_process(&h, "paid", &["--price", "0.0003", "--currency", "USD"], false);
|
|
let out = h.run(&["run", &prog, "--shots", "1000", "--provider", "paid"], "");
|
|
assert_eq!(out.status.code(), Some(3), "{}", String::from_utf8_lossy(&out.stderr));
|
|
let (o, e) = (String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr));
|
|
assert!(o.contains("estimate 0.3 USD (billed by the shot)") && e.contains("--yes"), "{o}\n{e}");
|
|
let body: Value = serde_json::from_str(&h.ok(&["run", &prog, "--shots", "1000", "--provider", "paid", "--yes", "--json"], "")).unwrap();
|
|
assert_eq!((body["result"]["cost"]["confirmed"].as_bool(), body["result"]["cost"]["charged"]["amount"].as_str()), (Some(true), Some("0.3")));
|
|
// Its conformance check runs no job without leave.
|
|
let t = h.ok(&["providers", "test", "paid"], "");
|
|
assert!(t.contains("skip the programs run: not every run is free"), "{t}");
|
|
}
|
|
|
|
#[test]
|
|
fn a_credential_is_kept_in_a_file_only_its_owner_reads_and_appears_in_nothing_printed() {
|
|
let h = Home::new("secret");
|
|
let prog = h.program();
|
|
let hash = wai_quantum_qpu::reference::credential_hash(SECRET);
|
|
let added = add_process(&h, "lab", &["--credential-blake3", &hash], true);
|
|
assert!(added.contains("kept in a file only you can read"), "{added}");
|
|
let list = h.ok(&["providers", "list"], "");
|
|
assert!(list.contains("credential: file (readable by you only; there is no keychain here)"), "{list}");
|
|
let run = h.ok(&["run", &prog, "--provider", "lab", "--json"], "");
|
|
assert!(run.contains("\"receipt\""));
|
|
h.ok(&["providers", "describe", "lab"], "");
|
|
h.ok(&["providers", "test", "lab", "--json"], "");
|
|
h.ok(&["run", &prog, "--provider", "lab", "--dry-run", "--json"], "");
|
|
// The credential is in its own file, readable by its owner only, and in
|
|
// nothing else: not the list, not a receipt, not anything printed.
|
|
let file = h.dir.join("config/wai/quantum-provider-credentials/lab");
|
|
assert_eq!(std::fs::read_to_string(&file).unwrap(), SECRET);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
assert_eq!(std::fs::metadata(&file).unwrap().permissions().mode() & 0o777, 0o600);
|
|
}
|
|
std::fs::write(h.dir.join("run.json"), &run).unwrap();
|
|
let everything = h.files().replace(&format!("== {}\n{SECRET}\n", file.display()), "");
|
|
assert!(!everything.contains(SECRET), "the credential is in a file other than its own");
|
|
assert!(!h.seen.borrow().contains(SECRET), "the credential was printed");
|
|
// A credential named by an environment variable is read from it, and kept nowhere.
|
|
h.ok(&["providers", "add", "env", "--kind", "process", "--credential-env", "MY_PROVIDER_TOKEN", "--", bin(), "providers", "reference", "--stdio", "--credential-blake3", &hash], "");
|
|
let mut c = h.cmd();
|
|
let out = c.env("MY_PROVIDER_TOKEN", SECRET).args(["run", &prog, "--provider", "env", "--json"]).output().unwrap();
|
|
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
|
assert!(!String::from_utf8_lossy(&out.stdout).contains(SECRET) && !String::from_utf8_lossy(&out.stderr).contains(SECRET));
|
|
// Without it, the provider refuses.
|
|
let out = h.run(&["run", &prog, "--provider", "env"], "");
|
|
assert!(!out.status.success() && String::from_utf8_lossy(&out.stderr).contains("unauthorized"), "{}", String::from_utf8_lossy(&out.stderr));
|
|
// A credential is never a setting.
|
|
let out = h.run(&["providers", "add", "bad", "--kind", "http", "--endpoint", "https://example.invalid", "--set", "api_token=x"], "");
|
|
assert!(!out.status.success() && String::from_utf8_lossy(&out.stderr).contains("looks like a credential"));
|
|
}
|
|
|
|
/// The reference served over HTTP by this binary, and its URL.
|
|
fn serve_http(h: &Home, extra: &[&str]) -> (Child, String) {
|
|
let mut child = h.cmd().args(["providers", "reference", "--http", "127.0.0.1:0"]).args(extra).stdout(Stdio::piped()).spawn().unwrap();
|
|
let mut line = String::new();
|
|
BufReader::new(child.stdout.take().unwrap()).read_line(&mut line).unwrap();
|
|
let url = line.trim().strip_prefix("listening ").unwrap_or_else(|| panic!("{line}")).to_string();
|
|
(child, url)
|
|
}
|
|
|
|
#[test]
|
|
fn a_provider_over_http_runs_with_its_credential_in_the_header() {
|
|
if Command::new("curl").arg("--version").output().is_err() {
|
|
println!("no curl on this host: the HTTP transport runs where curl is");
|
|
return;
|
|
}
|
|
let h = Home::new("http");
|
|
let prog = h.program();
|
|
let hash = wai_quantum_qpu::reference::credential_hash(SECRET);
|
|
let (mut child, url) = serve_http(&h, &["--credential-blake3", &hash, "--basis", "prxcz", "--all-to-all"]);
|
|
h.ok(&["providers", "add", "web", "--kind", "http", "--endpoint", &url, "--credential-stdin"], SECRET);
|
|
let t = h.ok(&["providers", "test", "web"], "");
|
|
let body = h.ok(&["run", &prog, "--provider", "web", "--json"], "");
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
assert!(t.contains("31 passed, 0 failed"), "{t}");
|
|
let job: Value = serde_json::from_str(&body).unwrap();
|
|
assert_eq!(job["result"]["compiled"]["basis"], "prxcz");
|
|
assert!(!h.seen.borrow().contains(SECRET));
|
|
// A credential over plain http to another machine is refused.
|
|
h.ok(&["providers", "add", "far", "--kind", "http", "--endpoint", "http://192.0.2.1/q", "--credential-stdin"], SECRET);
|
|
let out = h.run(&["run", &prog, "--provider", "far", "--dry-run"], "");
|
|
assert!(!out.status.success() && String::from_utf8_lossy(&out.stderr).contains("only over https"), "{}", String::from_utf8_lossy(&out.stderr));
|
|
}
|
|
|
|
/// One MCP message and its answer.
|
|
fn rpc(stdin: &mut impl Write, stdout: &mut impl BufRead, id: u64, method: &str, params: Value) -> Value {
|
|
writeln!(stdin, "{}", json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params })).unwrap();
|
|
stdin.flush().unwrap();
|
|
let mut line = String::new();
|
|
stdout.read_line(&mut line).unwrap();
|
|
serde_json::from_str(&line).unwrap_or_else(|e| panic!("{e}: {line}"))
|
|
}
|
|
|
|
#[test]
|
|
fn an_agent_lists_estimates_is_refused_and_runs_only_with_confirm_cost() {
|
|
let h = Home::new("agent");
|
|
add_process(&h, "free-lab", &[], false);
|
|
add_process(&h, "paid", &["--price", "0.25", "--currency", "EUR", "--per", "job"], false);
|
|
let mut child = h.cmd().args(["mcp", "--demo-key"]).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::null()).spawn().unwrap();
|
|
let mut stdin = child.stdin.take().unwrap();
|
|
let mut stdout = BufReader::new(child.stdout.take().unwrap());
|
|
rpc(&mut stdin, &mut stdout, 1, "initialize", json!({ "protocolVersion": "2025-06-18" }));
|
|
let tools = rpc(&mut stdin, &mut stdout, 2, "tools/list", json!({}));
|
|
let names: Vec<String> = tools["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect();
|
|
for n in ["providers_list", "provider_estimate", "run_on_provider"] {
|
|
assert!(names.iter().any(|x| x == n), "{n} not in {names:?}");
|
|
}
|
|
let call = |stdin: &mut std::process::ChildStdin, stdout: &mut BufReader<std::process::ChildStdout>, id: u64, name: &str, args: Value| rpc(stdin, stdout, id, "tools/call", json!({ "name": name, "arguments": args }))["result"].clone();
|
|
let list = call(&mut stdin, &mut stdout, 3, "providers_list", json!({ "free": true }));
|
|
assert_eq!(list["isError"], false, "{list}");
|
|
assert_eq!(list["structuredContent"]["providers"].as_array().unwrap().len(), 2);
|
|
assert_eq!(list["structuredContent"]["free"]["providers"][0]["name"], "free-lab", "{list}");
|
|
let est = call(&mut stdin, &mut stdout, 4, "provider_estimate", json!({ "provider": "paid", "qasm": BELL, "shots": 100 }));
|
|
assert_eq!((est["structuredContent"]["free"].as_bool(), est["structuredContent"]["estimate"]["amount"].as_str()), (Some(false), Some("0.25")), "{est}");
|
|
let refused = call(&mut stdin, &mut stdout, 5, "run_on_provider", json!({ "provider": "paid", "qasm": BELL, "shots": 100 }));
|
|
assert_eq!(refused["isError"], true, "{refused}");
|
|
assert!(refused["content"][0]["text"].as_str().unwrap().contains("confirm_cost: true"), "{refused}");
|
|
let ran = call(&mut stdin, &mut stdout, 6, "run_on_provider", json!({ "provider": "paid", "qasm": BELL, "shots": 100, "confirm_cost": true }));
|
|
assert_eq!(ran["isError"], false, "{ran}");
|
|
let job = ran["structuredContent"].clone();
|
|
let free = call(&mut stdin, &mut stdout, 7, "run_on_provider", json!({ "provider": "free-lab", "qasm": BELL }));
|
|
assert_eq!(free["isError"], false, "a free run needs no confirmation: {free}");
|
|
let checked = call(&mut stdin, &mut stdout, 8, "verify_quantum_result", json!({ "job": job }));
|
|
assert_eq!(checked["structuredContent"]["valid"], true, "{checked}");
|
|
// The engine's own run tool does not stand in for a provider.
|
|
let stand_in = call(&mut stdin, &mut stdout, 9, "run_quantum_circuit", json!({ "qasm": BELL, "provider": "paid" }));
|
|
assert_eq!(stand_in["isError"], true, "{stand_in}");
|
|
drop(stdin);
|
|
let _ = child.wait();
|
|
}
|