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
2 changes: 1 addition & 1 deletion crates/busbar-core-admin/src/v1/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use crate::v1::contract::{
};
use busbar_kernel::config::named_map::NamedMapSection;
use busbar_kernel::config::{
DeployCfg, HookCfg, HookKind, HookStage, PluginsCfg, PromptAccess, ProviderDef, UserAccess,
DeployCfg, HookCfg, HookKind, HookStage, PromptAccess, ProviderDef, UserAccess,
};

/// The KEY NAMES of one opaque `settings:` bag, sorted — the REDACTED projection EVERY admin read
Expand Down
39 changes: 18 additions & 21 deletions crates/busbar-core-admin/src/v1/service_operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,19 @@
//! where the block now lives.

use super::*;
use busbar_kernel::plugin_admission::sign::{TrustInput, TrustPolicy};
use busbar_kernel::plugin_admission::sign::TrustPolicy;

/// `plugins.trust` resolved as boot resolves it (the loader's `TrustPolicy::from_config`), after
/// warning about any malformed anti-downgrade floor — every admin moment the policy resolves
/// (catalog, reload, upload, rollback) warns exactly as boot does. `binary_version` is carried for
/// `plugins.trust` resolved by THE ROOT'S ONE BUILDER — the one boot admits plugins under, carried
/// on the snapshot (`App::plugin_trust`) — after warning about any malformed anti-downgrade floor,
/// so every admin moment the policy resolves (catalog, install, inspect, rollback) reaches boot's
/// verdict and warns exactly as boot does. No builder lives here. `binary_version` is carried for
/// diagnostics only.
pub(crate) fn trust_policy(cfg: &PluginsCfg, binary_version: &str) -> Result<TrustPolicy, String> {
cfg.warn_invalid_floors();
TrustPolicy::from_config(TrustInput {
publishers: &cfg.publisher_keys(),
allow_unsigned: cfg.trust.allow_unsigned,
allow_third_party: cfg.trust.allow_third_party,
min_versions: &cfg.min_versions,
first_party_floors: &cfg.first_party_floors,
binary_version,
})
pub(crate) fn trust_policy(app: &App, binary_version: &str) -> Result<TrustPolicy, String> {
app.plugins_cfg.warn_invalid_floors();
let build = app.plugin_trust.ok_or_else(|| {
"no plugin trust policy is installed (the composition root's)".to_string()
})?;
build(&app.plugins_cfg, binary_version)
}

impl AdminService {
Expand Down Expand Up @@ -610,7 +607,7 @@ impl AdminService {
// `store.module` config concern (read via `GET /config`), not summarized per-row here,
// the same posture the compiled-in hook rows take (`active: None`).
let mut out = linked_store_rows();
let Ok(policy) = trust_policy(&self.app.plugins_cfg, env!("CARGO_PKG_VERSION")) else {
let Ok(policy) = trust_policy(&self.app, env!("CARGO_PKG_VERSION")) else {
return out;
};

Expand Down Expand Up @@ -881,8 +878,8 @@ impl AdminService {
// ── 1. filename sanity: a bare tarball filename ──
let file = validate_plugin_filename(file)?;

let policy = trust_policy(&self.app.plugins_cfg, env!("CARGO_PKG_VERSION"))
.map_err(AdminError::Validation)?;
let policy =
trust_policy(&self.app, env!("CARGO_PKG_VERSION")).map_err(AdminError::Validation)?;

// ── 2. STRUCTURAL: in-memory unpack + manifest completeness + integrity + abi ──
let unpacked = busbar_kernel::plugin_admission::tarball::unpack(tarball)
Expand Down Expand Up @@ -1057,8 +1054,8 @@ impl AdminService {
)));
}

let policy = trust_policy(&self.app.plugins_cfg, env!("CARGO_PKG_VERSION"))
.map_err(AdminError::Validation)?;
let policy =
trust_policy(&self.app, env!("CARGO_PKG_VERSION")).map_err(AdminError::Validation)?;

let unpacked = busbar_kernel::plugin_admission::tarball::unpack(tarball)
.map_err(|e| AdminError::Validation(format!("invalid plugin tarball: {e}")))?;
Expand Down Expand Up @@ -1205,8 +1202,8 @@ impl AdminService {
// third-party plugin can also roll back. Anything the target does NOT satisfy (a broken
// signature, an un-opted-in third party) still fails: a rollback authenticates the OPERATOR,
// never the ARTIFACT.
let mut policy = trust_policy(&self.app.plugins_cfg, &manifest.version)
.map_err(AdminError::Validation)?;
let mut policy =
trust_policy(&self.app, &manifest.version).map_err(AdminError::Validation)?;
policy
.min_versions
.insert(manifest.name.clone(), manifest.version.clone());
Expand Down
43 changes: 36 additions & 7 deletions crates/busbar-core-admin/src/v1/tests/service_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4347,30 +4347,59 @@ fn the_store_catalog_heads_with_the_linked_store_rows() {
assert_eq!(head, want);
}

/// EVERY ADMIN MOMENT THE TRUST POLICY RESOLVES WARNS AS BOOT DOES: the catalog, reload, upload and
/// rollback all resolve through the one `trust_policy` (none resolves it any other way), and it
/// warns about a malformed anti-downgrade floor before resolving.
/// EVERY ADMIN MOMENT THE TRUST POLICY RESOLVES WARNS AS BOOT DOES: the catalog, install, inspect
/// and rollback all resolve through the one `trust_policy` (none resolves it any other way), which
/// builds nothing itself (the root's builder is installed), and it warns about a malformed
/// anti-downgrade floor before resolving.
#[test]
fn every_admin_trust_resolution_warns_about_a_malformed_floor() {
use busbar_kernel::diagnostics::CONFIG_ANTIDOWNGRADE_FLOOR_INVALID;
use tracing_subscriber::layer::SubscriberExt as _;
let src = include_str!("../service_operations.rs");
assert_eq!(
src.matches("trust_policy(&self.app.plugins_cfg").count(),
src.matches("trust_policy(&self.app, ").count(),
4,
"catalog, reload, upload and rollback resolve through trust_policy"
"catalog, install, inspect and rollback resolve through trust_policy"
);
assert_eq!(
src.matches("TrustPolicy::from_config(").count(),
0,
"the admin surface builds no trust policy of its own: it resolves through the root's"
);
assert_eq!(src.matches("TrustPolicy::from_config(").count(), 1);
let mut cfg = busbar_kernel::config::PluginsCfg::default();
cfg.min_versions.insert("p".into(), "v2".into());
let app = crate::new_test_app().plugins_cfg(cfg).build();
let cap = busbar_kernel::test_support::warn_capture::WarnCapture::default();
let subscriber = tracing_subscriber::registry().with(cap.clone());
tracing::subscriber::with_default(subscriber, || {
super::operations::trust_policy(&cfg, "1.6.0").expect("the policy resolves");
super::operations::trust_policy(&app, "1.6.0").expect("the policy resolves");
});
assert!(
cap.contains(&CONFIG_ANTIDOWNGRADE_FLOOR_INVALID.banner().to_string()),
"{:?}",
cap.messages()
);
}

/// THE ADMIN SURFACE JUDGES UNDER THE SNAPSHOT'S BUILDER AND NO OTHER: an App handed a root builder
/// that refuses is refused by every admin trust moment with that builder's words, and an App with no
/// builder resolves nothing (the catalog lists the linked rows alone).
#[test]
fn every_admin_trust_moment_resolves_through_the_snapshots_builder() {
fn refusing_root(
_: &busbar_kernel::config::PluginsCfg,
_: &str,
) -> Result<busbar_kernel::plugin_admission::sign::TrustPolicy, String> {
Err("the root's refusal".into())
}
let app = crate::new_test_app().plugin_trust(refusing_root).build();
assert_eq!(
super::operations::trust_policy(&app, "1.6.0")
.err()
.as_deref(),
Some("the root's refusal")
);
let mut none = (*app).clone();
none.plugin_trust = None;
assert!(super::operations::trust_policy(&none, "1.6.0").is_err());
}
1 change: 1 addition & 0 deletions crates/busbar-kernel/src/appbuild.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2036,6 +2036,7 @@ pub fn build_app_from_config(
cost,
plugins_dir: std::path::PathBuf::from(&plugins_cfg.dir),
plugins_cfg,
plugin_trust: crate::preflight::root_rows().trust_policy,
// Where `auth.key_ttl` is finally READ: the self-serve mint's token lifetime.
// Config-validate already proved this parses; the fallback keeps a bad value from panicking.
self_key_ttl_secs: cfg
Expand Down
14 changes: 14 additions & 0 deletions crates/busbar-kernel/src/preflight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ pub struct RootInstall {
/// the process's one dispatcher (ARCHITECT ruling 2026-09-29, the opener seam). `None` = no
/// export module resolves.
pub export_axis: Option<&'static dyn busbar_contract::export_calls::ExportAxis>,
/// The root's ONE plugin trust builder — the one boot admits plugins under — carried onto every
/// [`crate::state::App`] so the operator surface judges a plugin exactly as boot does. `None` =
/// no trust resolves (the admin catalog lists the linked rows alone; an upload is refused).
pub trust_policy: Option<TrustPolicyBuild>,
}

/// THE ROOT'S HOOK AXIS over one plugin registry (each configuration's registry gets its own).
Expand All @@ -145,6 +149,15 @@ pub type HookAxisBuild =
&std::sync::Arc<PluginRegistry>,
) -> Result<std::sync::Arc<dyn busbar_contract::hook_calls::HookAxis>, String>;

/// THE ROOT'S PLUGIN TRUST BUILDER: the `plugins:` block, and the binary version a floor reads, to
/// the policy a plugin is judged under (the root's `trust_policy`, a `test-harness` build's
/// first-party key included). No other builder judges a plugin.
///
/// # Errors
/// A reserved or malformed publisher, or the root's own refusal.
pub type TrustPolicyBuild =
fn(&config::PluginsCfg, &str) -> Result<busbar_plugin_loader::sign::TrustPolicy, String>;

/// A test build has no root: its store double ([`crate::test_support::store_double`]) and ranking
/// double stand in for the root's entries, the in-crate secret double
/// ([`crate::test_support::secrets`]) as the secret axis, and the test axis for the exports.
Expand All @@ -157,6 +170,7 @@ const STAND_IN: RootInstall = RootInstall {
store_axis: Some(crate::test_support::store_axis_stand_in),
secret_axis: Some(&crate::test_support::SecretsStandIn),
export_axis: Some(&crate::test_support::export_axis::STAND_IN),
trust_policy: Some(crate::test_support::trust_policy_stand_in),
};

/// The hook doors a test build links in place of the root's (the stand-in hook axis,
Expand Down
5 changes: 5 additions & 0 deletions crates/busbar-kernel/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ pub struct App {
/// catalog entry's trust verdict. Carried on the snapshot (not a global) so it is testable and
/// survives swaps.
pub plugins_cfg: crate::config::PluginsCfg,
/// The composition root's ONE plugin trust builder ([`crate::preflight::RootInstall`]'s
/// `trust_policy`, the one boot admits plugins under): every admin moment that judges a plugin
/// (catalog, install, inspect, rollback) resolves `plugins_cfg` through it and through nothing
/// else, so the operator surface reports the verdict boot reaches. `None` = no root installed one.
pub plugin_trust: Option<crate::preflight::TrustPolicyBuild>,
// The cross-protocol translation seam's global fallback max-output-tokens and effort→budget table
// no longer live on `App`: they are plane-specific vocabulary and now ride that plane's own
// per-generation runtime object, populated from the neutral `PlaneBuildInput` carrier `appbuild`
Expand Down
19 changes: 19 additions & 0 deletions crates/busbar-kernel/src/test_support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ pub struct TestApp {
explicit_no_overlay: bool,
plugins_dir: Option<std::path::PathBuf>,
plugins_cfg: Option<crate::config::PluginsCfg>,
plugin_trust: Option<crate::preflight::TrustPolicyBuild>,
hook_env: Option<crate::hooks::HookEnv>,
disk_paths: Option<(std::path::PathBuf, std::path::PathBuf)>,
/// When `true`, build an App that booted with NO plugin routes at all — an EMPTY live table AND an
Expand Down Expand Up @@ -1045,6 +1046,7 @@ impl TestApp {
explicit_no_overlay: false,
plugins_dir: None,
plugins_cfg: None,
plugin_trust: None,
hook_env: None,
disk_paths: None,
no_plugin_routes: false,
Expand Down Expand Up @@ -1168,6 +1170,12 @@ impl TestApp {
self.plugins_cfg = Some(cfg);
self
}
/// Judge plugins under `build` — a composition root's own trust builder — instead of the test
/// build's [`trust_policy_stand_in`].
pub fn plugin_trust(mut self, build: crate::preflight::TrustPolicyBuild) -> Self {
self.plugin_trust = Some(build);
self
}

/// Give the snapshot DISK TRUTH — the `config.yaml` / `providers.yaml` paths a rebuild reads.
/// Without these the app is EPHEMERAL and every rebuild-from-disk path takes its no-disk branch,
Expand Down Expand Up @@ -2061,6 +2069,7 @@ impl TestApp {
.plugins_dir
.unwrap_or_else(|| std::path::PathBuf::from("plugins")),
plugins_cfg: self.plugins_cfg.unwrap_or_default(),
plugin_trust: Some(self.plugin_trust.unwrap_or(trust_policy_stand_in)),
self_key_ttl_secs: crate::governance::mint_policy::DEFAULT_KEY_TTL_SECS,
mint_policy: std::sync::Arc::new(self.mint_policy.unwrap_or_default()),
request_id_counter: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(
Expand Down Expand Up @@ -2465,6 +2474,16 @@ pub fn trust_policy(
Ok(policy)
}

/// [`trust_policy`] as a test build's root trust row ([`crate::preflight::TrustPolicyBuild`]): what
/// a [`TestApp`] judges plugins under unless the test hands it the root's own. `binary_version` is
/// carried for diagnostics only.
pub fn trust_policy_stand_in(
cfg: &crate::config::PluginsCfg,
_binary_version: &str,
) -> Result<busbar_plugin_loader::sign::TrustPolicy, String> {
trust_policy(cfg)
}

/// A test build's store axis (it has no composition root): the loader's axis over a dispatcher of
/// the test process's own, minting `op_id`s from the kernel's one allocator.
pub fn store_axis_stand_in() -> std::sync::Arc<dyn busbar_contract::store_calls::StoreAxis> {
Expand Down
4 changes: 3 additions & 1 deletion crates/busbar/src/root/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ pub fn registry(
/// THE `plugins:` BLOCK'S TRUST, resolved: the embedded first-party key, the configured
/// publishers and opt-ins and the per-name floors (`min_versions`, the rollback pins), through the
/// loader's `TrustPolicy::from_config`. `binary_version` is carried for diagnostics only. The
/// automatic first-party floor is the registry build's to arm.
/// automatic first-party floor is the registry build's to arm. THE ONE BUILDER: the root hands this
/// same function to the kernel (`RootInstall::trust_policy`, `root::linked::register_stores`), and
/// every snapshot carries it to the admin surface, which judges a plugin under nothing else.
///
/// # Errors
///
Expand Down
1 change: 1 addition & 0 deletions crates/busbar/src/root/linked.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ pub fn register_stores(linked: &Linked) {
store_axis: Some(store_axis),
secret_axis: Some(secrets),
export_axis: Some(&crate::root::exports::EXPORTS),
trust_policy: Some(crate::root::boot::trust_policy),
}),
Err(refusal) => {
eprintln!("busbar: {refusal}");
Expand Down
79 changes: 79 additions & 0 deletions crates/busbar/src/root/tests/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,85 @@ fn a_test_harness_build_takes_its_first_party_key_from_the_harness_variable() {
));
}

/// THE ADMIN CATALOG'S ROW for a plugin a harness key signed first-party, read over
/// `GET /api/v1/admin/plugins?type=store` on a snapshot carrying the root's trust builder
/// ([`trust_policy`], what `root::linked::register_stores` installs for every snapshot), with the
/// harness variable set to that key: the row's (`valid`, `trust`). The key is not the kernel test
/// build's stand-in release key, so only the root's harness seam can admit it.
async fn the_admin_catalog_row_for_a_harness_signed_plugin(
tag: &str,
) -> (serde_json::Value, serde_json::Value) {
use crate::root::loader::sign::SigningKey;
use tower::ServiceExt as _;
let test_key = SigningKey::from_bytes(&[0x6b; 32]);
let artifact = b"\x7fELF admin catalog under the root's trust";
let manifest = first_party_signed_by(&test_key, artifact);
let dir = scratch(tag);
std::fs::write(
dir.join("harness-seam.tar.gz"),
crate::root::loader::tarball::package(&manifest, "lib.so", artifact).expect("it packs"),
)
.expect("the tarball is written");
let app = busbar_kernel::test_support::TestApp::new()
.admin_chain(vec![])
.plugins_dir(dir.clone())
.plugin_trust(trust_policy)
.build();
let router = busbar_core_admin::build_router(app);
let request = axum::http::Request::builder()
.uri("/api/v1/admin/plugins?type=store")
.body(axum::body::Body::empty())
.expect("a request");
std::env::set_var(
HARNESS_KEY_VAR,
hex::encode(test_key.verifying_key().as_bytes()),
);
let response = router.oneshot(request).await;
std::env::remove_var(HARNESS_KEY_VAR);
let response = response.expect("the admin surface answers");
assert_eq!(response.status().as_u16(), 200);
let bytes = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("the body");
let _ = std::fs::remove_dir_all(&dir);
let page: serde_json::Value = serde_json::from_slice(&bytes).expect("a JSON page");
let row = page["items"]
.as_array()
// The row is found by its tarball's name, wherever the row states it.
.and_then(|rows| {
rows.iter()
.find(|r| r.to_string().contains("harness-seam.tar.gz"))
})
.unwrap_or_else(|| panic!("the dropped plugin is listed: {page}"))
.clone();
(row["valid"].clone(), row["trust"].clone())
}

/// **THE ADMIN CATALOG JUDGES UNDER BOOT'S TRUST (the fleet e2e that read it red).** In a
/// `test-harness` build with the harness key set, the catalog row for a plugin that key signed
/// first-party reads valid and trusted — the verdict boot admits it under — because the admin
/// surface resolves through the root's one builder, harness key included, and builds none of its
/// own. RED before: the admin crate built its own policy without the harness key and reported the
/// row rejected (`first-party plugin only`) while boot admitted the plugin.
#[cfg(feature = "test-harness")]
#[tokio::test]
async fn a_test_harness_admin_catalog_reads_a_harness_signed_plugin_as_valid() {
let (valid, trust) =
the_admin_catalog_row_for_a_harness_signed_plugin("admin-trust-green").await;
assert_eq!(valid, serde_json::json!(true), "trust: {trust}");
assert_eq!(trust, serde_json::json!("trusted"));
}

/// The red arm: in a build WITHOUT `test-harness`, the same plugin under the same variable is
/// refused by the admin catalog, as boot refuses it — the harness key reaches neither.
#[cfg(not(feature = "test-harness"))]
#[tokio::test]
async fn an_admin_catalog_without_test_harness_refuses_a_harness_signed_plugin() {
let (valid, trust) = the_admin_catalog_row_for_a_harness_signed_plugin("admin-trust-red").await;
assert_eq!(valid, serde_json::json!(false), "trust: {trust}");
assert_eq!(trust, serde_json::json!("rejected"));
}

/// The features a `[features]` table turns on from `root`, transitively: only the package's own
/// features (a `dep:` or `crate/feature` entry enables nothing of this package's own).
fn features_reached(manifest: &str, root: &str) -> std::collections::BTreeSet<String> {
Expand Down