1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
//! The `capabilities` subcommand: print the capability surface this host
//! implements — the informational `boatramp:handlers` surface revision, the
//! imports a deploy may declare, and the capability **features** a guest may
//! `require` in its manifest (each with its stability) — so an operator (or a
//! remote deploy) can check that a host satisfies a guest's requirements before
//! shipping it. The `features` list is exactly what the deploy-time `requires`
//! admission check enforces. The same data backs `GET /api/capabilities`.
//!
//! `capabilities check <component.wasm>` is the shift-left form: it scans a
//! component's manifest `requires` and reports whether *this* build satisfies
//! them, exiting non-zero on a gap — so a guest author can gate CI on "my
//! function still fits the boatramp version I target" instead of finding out at
//! deploy.
use std::path::PathBuf;
/// Arguments for `boatramp capabilities`.
#[derive(Debug, clap::Args)]
pub struct CapabilitiesArgs {
/// Emit JSON instead of the human summary.
#[arg(long)]
pub json: bool,
/// Check a component's manifest `requires` against this build and exit non-zero
/// if any required capability is missing (for CI gating). Reads the given
/// `.wasm` component; needs the `handlers` feature.
#[arg(long, value_name = "COMPONENT.wasm")]
pub check: Option<PathBuf>,
}
/// Print the host's capability surface, or run a component compatibility check.
/// Infallible in the reporting path; the check path exits the process with a
/// status code (0 = fits, 1 = missing a capability, 2 = usage/read error) so it
/// reads cleanly in a CI pipeline.
pub fn run(args: CapabilitiesArgs) {
if let Some(component) = args.check.as_ref() {
check_component(component, args.json);
return;
}
let caps: crate::handler_validate::HostCapabilities =
crate::handler_validate::host_capabilities();
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&caps).expect("host capabilities serialize")
);
} else {
println!("package: {}@{}", caps.package, caps.version);
println!("declarable imports: {}", caps.declarable_imports.join(", "));
let feats = caps
.features
.iter()
.map(|c| format!("{} ({})", c.name, c.lifecycle.as_str()))
.collect::<Vec<_>>()
.join(", ");
println!("capability features: {feats}");
}
}
/// Check a component's `requires` against this build. See [`run`] for exit codes.
#[cfg(feature = "handlers")]
fn check_component(component: &std::path::Path, json: bool) {
let bytes = match std::fs::read(component) {
Ok(b) => b,
Err(err) => {
eprintln!("cannot read {}: {err}", component.display());
std::process::exit(2);
}
};
let required = boatramp_server::component_requires(&bytes);
let unmet = boatramp_server::unmet_requires(&bytes);
if json {
let report = serde_json::json!({
"component": component.display().to_string(),
"requires": required,
"unmet": unmet,
"ok": unmet.is_empty(),
});
println!(
"{}",
serde_json::to_string_pretty(&report).expect("check report serialize")
);
} else if required.is_empty() {
println!(
"ok: {} declares no capability requirements",
component.display()
);
} else if unmet.is_empty() {
println!(
"ok: this host satisfies all {} requirement(s): {}",
required.len(),
required.join(", ")
);
} else {
eprintln!(
"MISSING: this host does not implement {}: {}",
if unmet.len() == 1 {
"capability"
} else {
"capabilities"
},
unmet.join(", ")
);
eprintln!("required: {}", required.join(", "));
eprintln!("upgrade boatramp or enable those features — see `boatramp capabilities`.");
}
// Non-zero exit AFTER any report so `--json` still emits the machine-readable body.
if !unmet.is_empty() {
std::process::exit(1);
}
}
/// Without the `handlers` feature there is no capability registry to check against.
#[cfg(not(feature = "handlers"))]
fn check_component(_component: &std::path::Path, _json: bool) {
eprintln!("`capabilities check` needs a build with the `handlers` feature");
std::process::exit(2);
}