Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions crates/maxplayer/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ where
Some("wallet") => crate::wallet_cli::run(&args[2..], out, err),
Some("profile") => crate::profile_cli::run(&args[2..], out, err),
Some("whoami") => crate::whoami::run(&args[2..], out, err),
// Where the agent documentation lives. Pure print — no home, key, wallet or network — so
// it works on a box that has installed nothing but the binary.
Some("skill") => crate::skill::run(&args[2..], out, err),
#[cfg(feature = "stub-pay")]
Some("stub-pay") => crate::stub_pay_cli::run(&args[2..], out, err),
Some("log") => run_log(&args[2..], out, err),
Expand Down Expand Up @@ -324,7 +327,7 @@ fn usage(err: &mut dyn Write) -> i32 {
fn write_usage(out: &mut dyn Write) {
let _ = write!(
out,
"Usage:\n maxplayer [--help | --version]\n maxplayer version\n maxplayer mcp\n maxplayer buyer # persistent per-home daemon (exclusive lock, unix-socket RPC); `maxplayer buyer status` = thin client\n maxplayer doctor # seller environment self-check (git, credential helper, relay, mint, agent)\n maxplayer wallet <setup|balance|mint|mint-complete|send|receive|melt|invoice|mints|reconcile> ...\n maxplayer profile set [--name <name>] [--about <about>] # publish kind-0 identity\n maxplayer whoami [--home <dir>] # print this seat's public identity (hex pubkey, npub, resolved home)\n"
"Usage:\n maxplayer [--help | --version]\n maxplayer version\n maxplayer skill # print where the agent documentation lives (orientation URL + skill index); no wallet, key or network\n maxplayer mcp\n maxplayer buyer # persistent per-home daemon (exclusive lock, unix-socket RPC); `maxplayer buyer status` = thin client\n maxplayer doctor # seller environment self-check (git, credential helper, relay, mint, agent)\n maxplayer wallet <setup|balance|mint|mint-complete|send|receive|melt|invoice|mints|reconcile> ...\n maxplayer profile set [--name <name>] [--about <about>] # publish kind-0 identity\n maxplayer whoami [--home <dir>] # print this seat's public identity (hex pubkey, npub, resolved home)\n"
);
#[cfg(feature = "stub-pay")]
let _ = write!(
Expand All @@ -341,7 +344,8 @@ fn write_usage(out: &mut dyn Write) {
);
let _ = writeln!(
out,
" maxplayer accept <job_id> <claim_id> [--result-id <id>] # buyer: bind a delivered result (collect folds this in)\n maxplayer collect <job_id> [--out <folder>] # buyer: accept-if-needed + verify + pay + materialize\n maxplayer log replay <path>\n maxplayer mock run --script <path> --log <path> [--job-id <id>] [--permission-policy allow|deny]\n maxplayer run --agent-command <cmd> --task <text> --log <path> [--cwd <dir>] [--job-id <id>] [--permission-policy allow|allow-always|deny] [--idle-timeout <secs>]\n\nExit codes: 0 success, 1 usage error, 2 runtime error"
" maxplayer accept <job_id> <claim_id> [--result-id <id>] # buyer: bind a delivered result (collect folds this in)\n maxplayer collect <job_id> [--out <folder>] # buyer: accept-if-needed + verify + pay + materialize\n maxplayer log replay <path>\n maxplayer mock run --script <path> --log <path> [--job-id <id>] [--permission-policy allow|deny]\n maxplayer run --agent-command <cmd> --task <text> --log <path> [--cwd <dir>] [--job-id <id>] [--permission-policy allow|allow-always|deny] [--idle-timeout <secs>]\n\nExit codes: 0 success, 1 usage error, 2 runtime error\n{}",
crate::skill::docs_pointer_line()
);
}

Expand Down Expand Up @@ -666,6 +670,53 @@ mod tests {
assert!(!is_help_request(&s(&["status"])));
}

// The docs pointer on the paths that had none. Measured on the base commit: the whole binary
// carried exactly one route to https://www.maxplayer.ai/skill.md, the MCP handshake text — so
// a seller-only operator, or an agent driving the CLI directly, was never told where the
// guides are. Every assertion is against the ONE shared constant, never a hand-copied URL: a
// copy in the test would let the constant change while the test keeps passing against the old
// text. `maxplayer skill` is the route with no prerequisites (no home, no key, no network), and
// the top-level help is where an agent looks first.
#[test]
fn help_and_skill_carry_the_docs_pointer() {
let url = crate::skill::SKILL_URL;

let (code, out, _) = run_captured(["maxplayer", "--help"]);
assert_eq!(code, 0);
assert!(
out.contains(url),
"`maxplayer --help` must point at the docs:\n{out}"
);
assert!(
out.contains("maxplayer skill"),
"`maxplayer --help` must list the skill subcommand:\n{out}"
);

// A wrong invocation prints the same usage to stderr — a stranger who typed the wrong thing
// is exactly the reader who needs the pointer.
let (code, _, err) = run_captured(["maxplayer", "unknown"]);
assert_eq!(code, 1);
assert!(
err.contains(url),
"usage on stderr must carry the pointer too:\n{err}"
);

let (code, out, err) = run_captured(["maxplayer", "skill"]);
assert_eq!(code, 0, "stderr={err}");
assert!(
out.contains(url),
"`maxplayer skill` must print the orientation URL:\n{out}"
);
assert!(
out.contains(crate::skill::SKILL_INDEX_URL),
"`maxplayer skill` must print the skill index URL:\n{out}"
);
assert!(
err.is_empty(),
"`maxplayer skill` needs nothing and touches nothing:\n{err}"
);
}

// The shape #570 is about: a sole `--help` on ANY registered subcommand — at every nesting depth
// (`buyer status`, `wallet mints add`) — prints that command's usage to STDOUT and exits 0 with
// nothing on stderr (no parse, no home bootstrap, no daemon socket). #549 fixed only `seller`;
Expand Down Expand Up @@ -698,6 +749,7 @@ mod tests {
("profile --help", "maxplayer profile"),
("profile set --help", "maxplayer profile"),
("whoami --help", "maxplayer whoami"),
("skill --help", "maxplayer skill"),
("accept --help", "maxplayer accept"),
("collect --help", "maxplayer collect"),
("mcp --help", "maxplayer mcp"),
Expand Down
53 changes: 51 additions & 2 deletions crates/maxplayer/src/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2171,10 +2171,24 @@ mod checks {
fn write_usage(out: &mut dyn Write) {
let _ = writeln!(
out,
"Usage:\n maxplayer doctor [--home <dir>] # seller environment self-check (nix, credential helper, seller key, relay, mint, agent, sandbox, home permissions, harness credential permissions)\n\nExit codes: 0 all checks passed, 1 a blocking check FAILed"
"Usage:\n maxplayer doctor [--home <dir>] # seller environment self-check (nix, credential helper, seller key, relay, mint, agent, sandbox, home permissions, harness credential permissions)\n\nExit codes: 0 all checks passed, 1 a blocking check FAILed\n{}",
crate::skill::docs_pointer_line()
);
}

/// The lines `maxplayer doctor` prints before the first check: what this is, which home it read,
/// and where the documentation lives. Pure, so the pointer is testable without a relay, a mint or
/// a network — the report itself needs all three. An operator reads doctor output when something
/// is wrong, which is exactly when the route to the guides matters and, before this, was absent.
#[cfg(feature = "wallet")]
fn report_preamble(home_root: &std::path::Path) -> String {
format!(
"maxplayer doctor — seller environment self-check (home={})\n{}\n",
home_root.display(),
crate::skill::docs_pointer_line()
)
}

/// Entry from `cli::run` for `maxplayer doctor`.
///
/// Honors `--home <dir>` (mirroring `maxplayer seller`) so an operator can diagnose a specific seat,
Expand Down Expand Up @@ -2449,7 +2463,7 @@ fn run_doctor(
}
};

let _ = writeln!(out, "maxplayer doctor — seller environment self-check (home={})", home.root.display());
let _ = write!(out, "{}", report_preamble(&home.root));

// `doctor` reports; it never boots a seller, so there is nothing here for an unsafe override to
// waive. The containment check is read at its own severity.
Expand Down Expand Up @@ -2537,6 +2551,41 @@ pub fn sell_readiness_gate(
mod tests {
use super::*;

// The docs pointer on the doctor path. Before this, `maxplayer doctor` — the command an operator
// runs when something is wrong — carried no route to https://www.maxplayer.ai/skill.md; only the
// MCP handshake did, and a seller never sees that. Asserted against the ONE shared constant, not
// a copied URL, and on the pure preamble rather than a full report (which needs a relay, a mint
// and a network), so the guard runs everywhere the unit tests do.
#[test]
fn doctor_usage_and_report_preamble_carry_the_docs_pointer() {
let url = crate::skill::SKILL_URL;

let mut usage = Vec::new();
write_usage(&mut usage);
let usage = String::from_utf8(usage).expect("utf8");
assert!(
usage.contains(url),
"`doctor --help` must point at the docs:\n{usage}"
);

#[cfg(feature = "wallet")]
{
let preamble = report_preamble(std::path::Path::new("/tmp/example-home"));
assert!(
preamble.contains(url),
"the doctor report must point at the docs:\n{preamble}"
);
assert!(
preamble.contains("home=/tmp/example-home"),
"the preamble still names the home it read:\n{preamble}"
);
assert!(
preamble.ends_with('\n'),
"each preamble line is terminated:\n{preamble:?}"
);
}
}

#[test]
fn registry_runs_every_check_even_after_an_early_fail() {
use std::cell::Cell;
Expand Down
4 changes: 4 additions & 0 deletions crates/maxplayer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ mod sandbox_probe;
// which needs the docker runner behind that feature — the same gate the boot reaper carries.
#[cfg(feature = "acp")]
mod sandbox_reap;
// `maxplayer skill` and the ONE docs-pointer constant `--help`, `doctor`, seller first-run and the
// MCP handshake all print from. Every build: a box with nothing but the binary must be able to
// find the documentation.
mod skill;
#[cfg(feature = "stub-pay")]
mod stub_pay_cli;
mod wallet_cli;
Expand Down
Loading
Loading