`wai channel verify ... | head` exited 7 with "failed printing to stdout: Broken pipe". Rust's runtime ignores SIGPIPE, so a write to a pipe whose reader has gone returns EPIPE, and print!/println! panic on it; the door's catch_unwind turned the panic into an internal error. Every command of the binary did the same (an operation's text, its JSON envelope, the help the door prints itself), and so did wai-pack (exit 101) and wai-quantum (101). The fix is at the write sites, not in SIGPIPE's disposition at start. Restoring SIG_DFL at start would end the process on a write to any pipe whose reader has gone, a child's standard input among them: `wai key` writes a secret to a helper's input and `wai mcp`'s dev room writes to its child's, and there the command should say what failed rather than vanish. It would also leave Windows, which has no SIGPIPE, as it was. So each binary declares one module first, with #[macro_use], whose print!, println!, eprint! and eprintln! write through one function: a failure of kind BrokenPipe on the process's own standard output or error ends the process as a Unix tool's ends, by restoring SIGPIPE's default action and raising it (the shell reports 141, and pipefail sees what it sees for cat), and on Windows with status 141; any other failure panics with std's own message, as before. The handful of direct writes to stdout (prior-set's listing, the log follower's lines, wai-pack's listing and fixture bytes, wai-quantum's convert output) go through the same check. wai-quantum's completions are generated into a buffer first, since clap_complete panics on a failed write of its own. The unit-test builds keep std's macros, whose output the harness captures. The log follower of `wai logs` and `wai session list` ended with success on any write error; a real one is now an internal error. Tests, each through the binary with a pipe whose reader is closed before it starts (so its first write finds the reader gone): tests/cli_dx.rs (wai channel verify, wai ops list --json, wai --help), wai-pack tests/stdout_pipe.rs (fixture list, fixture gen) and wai-quantum-cli tests/stdout_pipe.rs (methods, methods --json, completions bash) require SIGPIPE on Unix, 141 on Windows, and nothing on standard error. A real write error still fails, on Linux to /dev/full (and on Windows, in cli_dx, to a handle open for reading alone): wai exits 7 saying "failed printing to stdout: No space left on device", wai-pack and wai-quantum fail as before. On the Framework (Linux): with main's wiring every new reader-gone test fails (wai exit 7 with the panic's message, wai-pack and wai-quantum 101), and with this change all pass; with the BrokenPipe arm planted out of each binary's check, each reader-gone test fails again. Clippy on the wai binary reports the same findings before and after (all in the library, none new); wai-pack's and wai-quantum-cli's clippy -D warnings, as their workflows run it, pass. On the old prod Mac Studio (macOS arm64), cli_dx's tests that run there and both crates' reader-gone tests pass. On cbio-device-0000 (Windows x86_64), cli_dx's two new tests pass: status 141 with nothing on standard error, and exit 7 for a write to a handle open for reading alone. Stacked on main, the floor, cli_dx, fleet, studio, doctor, mcp, setup-agent, ops, key, toolchain, target, folds and channel-lineup tests pass on the Framework, and wai breaking finds no break. Co-Authored-By: Claude Opus 5.5 <[email protected]>
257 lines
14 KiB
Rust
257 lines
14 KiB
Rust
//! The developer's path through the `wai` binary, as a person runs it: `wai hello` writes an
|
|
//! object, its receipt index and its group's receipt file, and `wai verify` checks them
|
|
//! offline and refuses each when it is altered; `wai create` writes each example of
|
|
//! `wai/examples/` as a project, file for file and byte for byte, and never into a directory
|
|
//! that holds anything. `wai verify` also reads the envelopes under a path: a planted envelope
|
|
//! that pins nothing where its capability decodes against a parameter set is reported
|
|
//! `prior_unpinned`, and still verifies; one whose pin breaks SPEC §3.1 does not. Piped to a
|
|
//! reader that stops reading (`wai ... | head`), `wai` ends as a Unix tool does, without a word;
|
|
//! any other failure to write its output is still an error.
|
|
#![cfg(feature = "quickstart")]
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
|
|
fn tmp(name: &str) -> PathBuf {
|
|
let d = std::env::temp_dir().join(format!("wai-dx-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
d
|
|
}
|
|
|
|
fn wai(dir: &Path, args: &[&str]) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(dir).args(args).output().expect("wai runs")
|
|
}
|
|
|
|
fn text(o: &Output) -> String {
|
|
format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr))
|
|
}
|
|
|
|
/// The one file under `dir` whose name ends with `suffix`.
|
|
fn one(dir: &Path, suffix: &str) -> PathBuf {
|
|
let mut v: Vec<PathBuf> = std::fs::read_dir(dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.to_string_lossy().ends_with(suffix)).collect();
|
|
assert_eq!(v.len(), 1, "one {suffix} in {}", dir.display());
|
|
v.remove(0)
|
|
}
|
|
|
|
#[test]
|
|
fn hello_runs_the_four_steps_and_verify_checks_what_it_wrote() {
|
|
let d = tmp("hello");
|
|
let o = wai(&d, &["hello", "h"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
let t = text(&o);
|
|
for step in ["1 Name", "2 Dispatch", "3 Rebuild", "4 Receipt", "byte for byte", "delivered-verified"] {
|
|
assert!(t.contains(step), "hello says {step}: {t}");
|
|
}
|
|
let h = d.join("h");
|
|
let v = wai(&d, &["verify", "h"]);
|
|
assert!(v.status.success(), "{}", text(&v));
|
|
// The object is an envelope there too, and its pins are read.
|
|
assert!(text(&v).contains("delivered-verified") && text(&v).contains("1 receipt index and 1 envelope verified"), "{}", text(&v));
|
|
|
|
// A byte of the object changed: its content hash is not the receipt's.
|
|
let obj = one(&h, ".wai");
|
|
let good = std::fs::read(&obj).unwrap();
|
|
let mut bad = good.clone();
|
|
let n = bad.len();
|
|
bad[n - 1] ^= 1;
|
|
std::fs::write(&obj, &bad).unwrap();
|
|
let v = wai(&d, &["verify", "h"]);
|
|
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
|
|
assert!(text(&v).contains("FAIL"), "{}", text(&v));
|
|
std::fs::write(&obj, &good).unwrap();
|
|
|
|
// A byte of the group's receipt file changed: its name is no longer its link hash, or its
|
|
// signature no longer verifies.
|
|
let rcpt = one(&h.join("rcpt"), ".json");
|
|
let g = std::fs::read_to_string(&rcpt).unwrap();
|
|
let i = g.find("\"sig\":\"").expect("a signature") + 7;
|
|
let mut b = g.into_bytes();
|
|
b[i] = if b[i] == b'0' { b'1' } else { b'0' };
|
|
std::fs::write(&rcpt, &b).unwrap();
|
|
let v = wai(&d, &["verify", "h"]);
|
|
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
|
|
|
|
// Nothing to verify is an error, not a pass.
|
|
std::fs::create_dir_all(d.join("empty")).unwrap();
|
|
let v = wai(&d, &["verify", "empty"]);
|
|
assert_eq!(v.status.code(), Some(2), "{}", text(&v));
|
|
// And hello never writes into a directory of someone else's files.
|
|
std::fs::write(d.join("empty/mine.txt"), "mine").unwrap();
|
|
assert_eq!(wai(&d, &["hello", "empty"]).status.code(), Some(2));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
/// SPEC §3.1, *Pin by default*: `wai verify` reports `prior_unpinned` on a planted envelope
|
|
/// that pins nothing over a capability that decodes against a parameter set, and the envelope
|
|
/// still verifies; it reports nothing on one over a standard codec or on a pinned one; and it
|
|
/// refuses one whose pin a verifier can tell is wrong without holding a set.
|
|
#[test]
|
|
fn verify_reports_an_unpinned_envelope() {
|
|
use wai::container::{Conditioning, Manifest, ModelRequirement, Prior, Wai};
|
|
let d = tmp("unpinned");
|
|
let p = d.join("planted");
|
|
std::fs::create_dir_all(&p).unwrap();
|
|
std::fs::write(d.join("payload.bin"), b"opaque test bytes").unwrap();
|
|
let wrap = |cap: &str, out: &str| {
|
|
let o = wai(&d, &["wrap", cap, "payload.bin", "-o", out, "--intent", "improve"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
o
|
|
};
|
|
let w = wrap("wai.neural.bmshj2018", "planted/unpinned.wai");
|
|
assert!(text(&w).contains("note prior_unpinned: wai.neural.bmshj2018"), "wrap says it pinned nothing: {}", text(&w));
|
|
wrap("wai.image.png", "planted/png.wai");
|
|
let pin = Prior::new("int_mlicpp/test", "7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4", "int-rans/p16+int-mlicpp/i64");
|
|
let pinned = Wai::new(Manifest::new("image", "improve", ModelRequirement::new("wai.neural.int_mlicpp"),
|
|
Conditioning::new("int_mlicpp").with_prior(pin.clone())), b"M".to_vec());
|
|
std::fs::write(p.join("pinned.wai"), pinned.to_bytes().unwrap()).unwrap();
|
|
|
|
let v = wai(&d, &["verify", "planted"]);
|
|
let t = text(&v);
|
|
assert!(v.status.success(), "a report is not a refusal: {t}");
|
|
assert!(t.contains("note prior_unpinned: wai.neural.bmshj2018 decodes against a parameter set"), "{t}");
|
|
assert_eq!(t.matches("prior_unpinned").count(), 1, "the PNG and the pinned envelope are not reported: {t}");
|
|
// The two `wai wrap` wrote leave their receipts beside them, which `wai verify` reads too.
|
|
assert!(t.contains("3 envelopes and 2 operation receipts verified"), "{t}");
|
|
let v = wai(&d, &["verify", "planted/unpinned.wai", "--json"]);
|
|
assert!(v.status.success() && text(&v).contains("prior_unpinned"), "{}", text(&v));
|
|
|
|
// A pin over a capability that reads no set: the envelope does not conform.
|
|
let bad = Wai::new(Manifest::new("image", "replicate", ModelRequirement::new("wai.image.png"),
|
|
Conditioning::new("png").with_prior(pin)), b"PNG".to_vec());
|
|
std::fs::write(p.join("bad.wai"), bad.to_bytes().unwrap()).unwrap();
|
|
let v = wai(&d, &["verify", "planted"]);
|
|
assert_eq!(v.status.code(), Some(1), "{}", text(&v));
|
|
assert!(text(&v).contains("FAIL WAI1 wai.image.png: prior_not_applicable"), "{}", text(&v));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
/// Every file of an example but its `test/` directory, relative to it, sorted; of a project, but the
|
|
/// toolchain's own `.wai/`, where `wai create` keeps its receipt.
|
|
fn example_files(dir: &Path) -> Vec<String> {
|
|
fn walk(root: &Path, p: &Path, out: &mut Vec<String>) {
|
|
for e in std::fs::read_dir(p).unwrap().flatten() {
|
|
let q = e.path();
|
|
let rel = q.strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/");
|
|
if rel == "test" || rel.starts_with("test/") || rel.ends_with(".DS_Store") || rel == "target" || rel.starts_with("target/") || rel == ".wai" || rel.starts_with(".wai/") {
|
|
continue;
|
|
}
|
|
if q.is_dir() {
|
|
walk(root, &q, out);
|
|
} else {
|
|
out.push(rel);
|
|
}
|
|
}
|
|
}
|
|
let mut v = Vec::new();
|
|
walk(dir, dir, &mut v);
|
|
v.sort();
|
|
v
|
|
}
|
|
|
|
#[test]
|
|
fn create_writes_every_example_as_it_is_in_the_repository() {
|
|
let d = tmp("create");
|
|
let examples = Path::new(env!("CARGO_MANIFEST_DIR")).join("../examples");
|
|
let o = wai(&d, &["create", "--list", "--json"]);
|
|
assert!(o.status.success(), "{}", text(&o));
|
|
// The JSON envelope (envelope/1): the list is its data.
|
|
let env: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap();
|
|
assert_eq!((env["ok"].as_bool(), env["op"].as_str()), (Some(true), Some("project.create")), "{env}");
|
|
let list = env["data"].as_array().unwrap();
|
|
let names: Vec<&str> = list.iter().map(|t| t["template"].as_str().unwrap()).collect();
|
|
// Every example directory is a template, and every template an example.
|
|
let mut dirs: Vec<String> = std::fs::read_dir(&examples).unwrap().flatten().filter(|e| e.path().is_dir()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
|
|
dirs.sort();
|
|
let mut sorted = names.clone();
|
|
sorted.sort();
|
|
assert_eq!(sorted, dirs, "the templates are the examples");
|
|
for t in list {
|
|
let name = t["template"].as_str().unwrap();
|
|
let generated: Vec<&str> = t["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap()).filter(|f| !examples.join(name).join(f).exists()).collect();
|
|
let o = wai(&d, &["create", name, "--template", name]);
|
|
assert!(o.status.success(), "{name}: {}", text(&o));
|
|
let made = example_files(&d.join(name));
|
|
let mut want = example_files(&examples.join(name));
|
|
want.extend(generated.iter().map(|g| g.to_string()));
|
|
want.sort();
|
|
assert_eq!(made, want, "{name}: the project holds the example's files, and those made for it");
|
|
// And the receipt of what `wai create` wrote, in the toolchain's own directory.
|
|
assert!(d.join(name).join(".wai/receipts/wai.json.wai-receipt.json").is_file(), "{name}: its receipt");
|
|
for f in example_files(&examples.join(name)) {
|
|
assert_eq!(std::fs::read(d.join(name).join(&f)).unwrap(), std::fs::read(examples.join(name).join(&f)).unwrap(), "{name}/{f}: byte for byte");
|
|
}
|
|
}
|
|
// Never into a directory that holds anything.
|
|
let o = wai(&d, &["create", "video-room", "--template", "video-room"]);
|
|
assert_eq!(o.status.code(), Some(2), "{}", text(&o));
|
|
assert!(text(&o).contains("not empty"));
|
|
let o = wai(&d, &["create", "x", "--template", "no-such"]);
|
|
assert_eq!(o.status.code(), Some(2));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
/// The lineup of the corpus's sample channel (`live-conformance/channels/`), written to `dir`
|
|
/// beside a copy of its receipt: what `wai channel verify` reads.
|
|
fn sample_channel(dir: &Path) -> (PathBuf, PathBuf) {
|
|
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../live-conformance/channels");
|
|
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(corpus.join("vectors.json")).unwrap()).unwrap();
|
|
let hex = v["imports"][0]["outcome"]["lineup"].as_str().expect("the sample's lineup");
|
|
let lineup: Vec<u8> = (0..hex.len()).step_by(2).map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap()).collect();
|
|
let (r, l) = (dir.join("sample.channel.json"), dir.join("lineup.wlu"));
|
|
std::fs::copy(corpus.join("sample.channel.json"), &r).unwrap();
|
|
std::fs::write(&l, lineup).unwrap();
|
|
(r, l)
|
|
}
|
|
|
|
/// A reader that stops reading (`wai channel verify ... | head`) ends `wai` as it ends a Unix
|
|
/// tool, with nothing on standard error: on Unix by SIGPIPE, which a shell reports as 141, and on
|
|
/// Windows with status 141. The pipe's reader is closed before `wai` starts, so its first write
|
|
/// finds the reader gone: an operation's text, its JSON envelope, and the help the door prints.
|
|
#[test]
|
|
fn a_reader_that_stops_reading_ends_wai_without_a_word() {
|
|
let d = tmp("pipe");
|
|
let (r, l) = sample_channel(&d);
|
|
let (r, l) = (r.to_str().unwrap(), l.to_str().unwrap());
|
|
// Read whole, the sample verifies.
|
|
let o = wai(&d, &["channel", "verify", "--receipt", r, "--lineup", l]);
|
|
assert!(o.status.success() && !o.stdout.is_empty(), "{}", text(&o));
|
|
for args in [&["channel", "verify", "--receipt", r, "--lineup", l][..], &["ops", "list", "--json"], &["--help"]] {
|
|
let (reader, writer) = std::io::pipe().unwrap();
|
|
drop(reader);
|
|
let o = Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(&d).args(args).stdout(writer).output().expect("wai runs");
|
|
assert!(o.stderr.is_empty(), "{args:?}: {}", String::from_utf8_lossy(&o.stderr));
|
|
#[cfg(unix)]
|
|
assert_eq!(std::os::unix::process::ExitStatusExt::signal(&o.status), Some(13), "{args:?}: ended by SIGPIPE, not {:?}", o.status);
|
|
#[cfg(windows)]
|
|
assert_eq!(o.status.code(), Some(141), "{args:?}");
|
|
}
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|
|
|
|
/// Any other failure to write standard output still fails `wai`, as the internal error it was
|
|
/// (exit 7), saying what failed: a full device on Linux, a handle open for reading alone on
|
|
/// Windows. (With `--json` the error's envelope cannot be written either, and that second
|
|
/// failure ends the run with the panic's status, 101, as before.) (macOS has neither to hand: a descriptor open for reading alone gives EBADF there,
|
|
/// which the standard library takes for a closed output and writes nothing to.)
|
|
#[cfg(any(target_os = "linux", windows))]
|
|
#[test]
|
|
fn a_write_that_fails_otherwise_still_fails_wai() {
|
|
let d = tmp("write-fails");
|
|
let (r, l) = sample_channel(&d);
|
|
#[cfg(target_os = "linux")]
|
|
let out = || std::fs::OpenOptions::new().write(true).open("/dev/full").unwrap();
|
|
#[cfg(windows)]
|
|
let out = || {
|
|
let p = d.join("read-only");
|
|
std::fs::write(&p, b"").unwrap();
|
|
std::fs::File::open(&p).unwrap()
|
|
};
|
|
for args in [&["channel", "verify", "--receipt", r.to_str().unwrap(), "--lineup", l.to_str().unwrap()][..], &["ops", "list"]] {
|
|
let o = Command::new(env!("CARGO_BIN_EXE_wai")).current_dir(&d).args(args).stdout(out()).output().expect("wai runs");
|
|
let err = String::from_utf8_lossy(&o.stderr);
|
|
assert_eq!(o.status.code(), Some(7), "{args:?}: {err}");
|
|
assert!(err.contains("failed printing to stdout"), "{args:?}: {err}");
|
|
}
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
}
|