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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## [0.14.8] — 2026-09-25

`b10x-harness` joins the catalog, and `b10x` chooses a prebuilt archive only where one exists for
the machine.

- `catalog.json`: `b10x-harness` (the Beyond10x agent loop that Metaharness's `b10x` adapter runs)
is an optional binary of `aep`, installed with `b10x install b10x-harness`. It runs on Linux only:
a new `platforms` field on a catalog binary keeps `b10x` from planning it elsewhere.
- `b10x` reads which targets the newest release's `SHA256SUMS` lists and chooses prebuilt only when
this machine's target is among them; otherwise `cargo`, or a warning that names the target. A
release with an archive for some machines no longer sends the others to a download that fails.
- `b10x install <cli>` follows the same default. Since 0.14.7 it still chose `cargo` whenever `cargo`
was on `PATH`, although its help and the docs said prebuilt.
- `aep:init`: `metaharness` is optional in the plan and installed with `b10x install metaharness`;
the skill said `b10x init aep` offers it.
- Docs name `b10x-harness` beside `metaharness`.

## [0.14.7] — 2026-09-25

The operator's decision on the install method, asked for by a team adopting ESS: anyone with Rust
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/agentplugins-check", "crates/b10x"]

[workspace.package]
version = "0.14.7"
version = "0.14.8"
edition = "2021"
rust-version = "1.85"
license = "Apache-2.0"
Expand Down
9 changes: 9 additions & 0 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@
"archive": { "repository": "beyond10x/metaharness" },
"cargo": { "repository": "beyond10x/metaharness", "package": "metaharness-cli" }
}
},
{
"name": "b10x-harness",
"optional": true,
"platforms": ["linux"],
"install": {
"archive": { "repository": "beyond10x/harness" },
"cargo": { "repository": "beyond10x/harness", "package": "b10x-harness-cli" }
}
}
]
},
Expand Down
9 changes: 8 additions & 1 deletion crates/agentplugins-check/src/concept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ impl Plugins {
const LIFECYCLE: &[&str] = &["init", "upgrade"];

/// CLIs whose versions a plugin must not quote (R5): the newest release is the only one they describe.
const CLIS: &[&str] = &["aep", "ess", "worktree", "metaharness", "protocol"];
const CLIS: &[&str] = &[
"aep",
"ess",
"worktree",
"metaharness",
"b10x-harness",
"protocol",
];

/// Every `<cli> x.y.z` in a line, case-insensitive, with optional backticks and a `v`.
#[must_use]
Expand Down
37 changes: 37 additions & 0 deletions crates/b10x/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,28 @@ pub struct Binary {
/// Reported but never installed unprompted.
#[serde(default)]
pub optional: bool,
/// Operating systems it runs on (`linux`, `macos`); empty means every one.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub platforms: Vec<String>,
/// The ways it can be installed.
pub install: Install,
}

impl Binary {
/// Whether it runs on the operating system of `target` (a release-archive triple).
#[must_use]
pub fn runs_on(&self, target: Option<&str>) -> bool {
self.platforms.is_empty()
|| target.is_some_and(|target| {
self.platforms.iter().any(|os| match os.as_str() {
"linux" => target.contains("-linux"),
"macos" => target.contains("-apple-darwin"),
_ => false,
})
})
}
}

/// The ways a binary can be installed; at least one is present.
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Install {
Expand Down Expand Up @@ -189,6 +207,25 @@ impl Catalog {

#[cfg(test)]
mod tests {
#[test]
fn a_binary_limited_to_linux_runs_only_on_linux_targets() {
let mut binary = super::Binary {
name: "b10x-harness".to_owned(),
optional: true,
platforms: vec!["linux".to_owned()],
install: super::Install {
archive: None,
cargo: None,
},
};
assert!(binary.runs_on(Some("x86_64-unknown-linux-gnu")));
assert!(binary.runs_on(Some("aarch64-unknown-linux-gnu")));
assert!(!binary.runs_on(Some("aarch64-apple-darwin")));
assert!(!binary.runs_on(None));
binary.platforms.clear();
assert!(binary.runs_on(None));
}

use super::*;

#[test]
Expand Down
64 changes: 59 additions & 5 deletions crates/b10x/src/install.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Installing one binary at one exact release: a checksummed release archive, or `cargo install`
//! from the tag. Either way the binary is staged first and moved into place with one rename.

use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;

Expand Down Expand Up @@ -36,15 +37,42 @@ fn run(program: &str, arguments: &[&str]) -> Result<(), String> {
}
}

/// The hex SHA-256 listed for `file` in a `SHA256SUMS` document.
/// The release archive of `name` at `tag` for `target`: `<name>-<version>-<target>.tar.gz`.
#[must_use]
pub fn listed_digest<'a>(sums: &'a str, file: &str) -> Option<&'a str> {
sums.lines().find_map(|line| {
pub fn archive_name(name: &str, tag: &str, target: &str) -> String {
format!("{}{target}.tar.gz", archive_prefix(name, tag))
}

/// What every archive name of `name` at `tag` starts with, before the target.
fn archive_prefix(name: &str, tag: &str) -> String {
format!("{name}-{}-", tag.trim_start_matches('v'))
}

/// `(digest, file)` for every line of a `SHA256SUMS` document.
fn listed(sums: &str) -> impl Iterator<Item = (&str, &str)> {
sums.lines().filter_map(|line| {
let (digest, name) = line.split_once(char::is_whitespace)?;
(name.trim().trim_start_matches('*') == file).then_some(digest)
Some((digest, name.trim().trim_start_matches('*')))
})
}

/// The hex SHA-256 listed for `file` in a `SHA256SUMS` document.
#[must_use]
pub fn listed_digest<'a>(sums: &'a str, file: &str) -> Option<&'a str> {
listed(sums).find_map(|(digest, name)| (name == file).then_some(digest))
}

/// The targets a `SHA256SUMS` document lists an archive of `name` at `tag` for.
#[must_use]
pub fn listed_targets(sums: &str, name: &str, tag: &str) -> BTreeSet<String> {
let prefix = archive_prefix(name, tag);
listed(sums)
.filter_map(|(_, file)| file.strip_prefix(&prefix)?.strip_suffix(".tar.gz"))
.filter(|target| !target.is_empty() && !target.contains('/'))
.map(str::to_owned)
.collect()
}

/// Lower-case hex of bytes.
#[must_use]
pub fn hex(bytes: &[u8]) -> String {
Expand Down Expand Up @@ -72,7 +100,7 @@ fn staging(name: &str) -> Result<PathBuf, String> {
fn from_archive(name: &str, tag: &str, repository: &str, stage: &Path) -> Result<PathBuf, String> {
let target = target()?;
let version = tag.trim_start_matches('v');
let archive = format!("{name}-{version}-{target}.tar.gz");
let archive = archive_name(name, tag, target);
let base = format!("/{repository}/releases/download/{tag}");
let archive_path = stage.join(&archive);
let sums_path = stage.join("SHA256SUMS");
Expand Down Expand Up @@ -199,4 +227,30 @@ mod tests {
);
assert_eq!(listed_digest(sums, "ess-0.30.0.tar.gz"), None);
}

#[test]
fn targets_are_read_from_the_archive_names_sums_lists() {
let sums = "\
aaa b10x-harness-0.13.2-x86_64-unknown-linux-gnu.tar.gz
bbb b10x-harness-0.13.2-x86_64-unknown-linux-gnu.tar.gz.sig
ccc *b10x-harness-0.13.1-aarch64-unknown-linux-gnu.tar.gz
ddd harness-0.13.2-aarch64-apple-darwin.tar.gz
eee b10x-harness-lsp-0.13.2-aarch64-unknown-linux-gnu.tar.gz
fff b10x-harness-0.13.2-.tar.gz
";
assert_eq!(
listed_targets(sums, "b10x-harness", "0.13.2"),
BTreeSet::from(["x86_64-unknown-linux-gnu".to_owned()])
);
assert_eq!(
listed_targets(sums, "b10x-harness", "v0.13.2"),
listed_targets(sums, "b10x-harness", "0.13.2"),
"a leading v on the tag is not part of the archive name"
);
assert!(listed_targets("", "b10x-harness", "0.13.2").is_empty());
for target in listed_targets(sums, "b10x-harness", "0.13.2") {
let name = archive_name("b10x-harness", "0.13.2", &target);
assert_eq!(listed_digest(sums, &name), Some("aaa"));
}
}
}
47 changes: 33 additions & 14 deletions crates/b10x/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ enum Top {
/// Products, comma separated: aep, ess, worktree, connectors.
#[arg(value_delimiter = ',')]
products: Vec<String>,
/// How to install CLIs; default: prebuilt, cargo when a release has no archive.
/// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine.
#[arg(long, value_enum)]
method: Option<MethodArg>,
/// Hosts to plan for.
Expand All @@ -55,7 +55,7 @@ enum Top {
/// Products, comma separated; default: every installed product.
#[arg(value_delimiter = ',')]
products: Vec<String>,
/// How to install CLIs; default: prebuilt, cargo when a release has no archive.
/// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine.
#[arg(long, value_enum)]
method: Option<MethodArg>,
/// Hosts to plan for.
Expand Down Expand Up @@ -91,7 +91,7 @@ enum Top {
/// Target directory; defaults to `~/.local/bin` (prebuilt) or `~/.cargo/bin` (cargo).
#[arg(long)]
dir: Option<PathBuf>,
/// How to install; default: prebuilt, cargo when a release has no archive.
/// How to install; default: prebuilt when the release has an archive for this machine, else cargo.
#[arg(long, value_enum)]
method: Option<MethodArg>,
},
Expand All @@ -108,7 +108,7 @@ enum Setup {
/// Hosts to plan for.
#[arg(long, value_enum, default_value_t = Hosts::All)]
host: Hosts,
/// How to install CLIs; default: prebuilt, cargo when a release has no archive.
/// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine.
#[arg(long, value_enum)]
method: Option<MethodArg>,
/// Print the plan as JSON.
Expand Down Expand Up @@ -327,6 +327,7 @@ fn make_plan(
home: &home,
only,
method,
target: install::target().ok().map(str::to_owned),
upgrade,
};
Ok(plan::plan(&context, &inventory))
Expand Down Expand Up @@ -570,16 +571,34 @@ fn install_one(
None => resolve::latest_tag(repository)
.ok_or_else(|| format!("no release found for {repository}"))?,
};
let method = method.map_or_else(
|| {
if inventory::copies_on_path("cargo").is_empty() {
catalog::Method::Prebuilt
} else {
catalog::Method::Cargo
}
},
MethodArg::method,
);
let target = install::target().ok();
if !binary.runs_on(target) {
return Err(format!(
"`{name}` runs on {} only",
binary.platforms.join(", ")
));
}
let method = if let Some(method) = method {
method.method()
} else {
let listed = binary.install.archive.is_some()
&& target.is_some_and(|target| {
resolve::fetch(&format!(
"/{repository}/releases/download/{tag}/SHA256SUMS"
))
.is_some_and(|sums| install::listed_targets(&sums, name, &tag).contains(target))
});
if listed {
catalog::Method::Prebuilt
} else if binary.install.cargo.is_some() && !inventory::copies_on_path("cargo").is_empty() {
catalog::Method::Cargo
} else {
return Err(format!(
"{tag} of `{name}` has no prebuilt archive for {} and `cargo` is not on PATH",
target.unwrap_or("this machine")
));
}
};
let home = inventory::home();
let directory = dir.unwrap_or_else(|| match method {
catalog::Method::Prebuilt => home.join(".local/bin"),
Expand Down
Loading
Loading