fix(plugins): revalidate snapshot and receipt before enabling [skip ci]

Reject an absent or invalid staged snapshot and require the freshly locked state generation and trust receipt to match the reviewed plugin before setting enablement. Preserve existing trust and revoke semantics. The filesystem check is point-in-time; automatic restoration and stage/GC races remain open.

Verified: existing stale-enable test passed on Mac (1/1); Codewhalebot Linux production plugin library 202 passed, 1 ignored with matching two source-file hashes and the same Cargo.lock on earlier base 1105bede; rustfmt and diff checks. No production stage-disappearance regression or exact final candidate was qualified.
This commit is contained in:
CodeWhale Bot committed 2026-11-06 22:22:46 -08:00
1 parent 38fb1a6e8e
commit 792817bfcb
2 files changed
+31 -3

No files matched your search

+19 -2
View File
@@ -599,7 +599,9 @@ impl PluginRegistry {
plugin.trust_status.as_str()
));
}
if plugin.staged_root.is_none() {
if plugin.staged_root.as_deref().is_none_or(|root| {
!staged_bundle_matches(root, &plugin.content_hash, &plugin.capability_hash)
}) {
return Err(format!(
"Plugin bundle `{}` has no verified Codewhale runtime snapshot; review and trust it again before enablement",
plugin.name()
@@ -621,7 +623,22 @@ impl PluginRegistry {
}
let id = plugin.id.clone();
self.commit_state_change(|state| {
let entry = state.plugins.entry(id).or_default();
let entry = state
.plugins
.get_mut(&id)
.filter(|entry| {
entry.generation == plugin.state_generation
&& entry.trust.as_ref().is_some_and(|receipt| {
receipt.content_hash == plugin.content_hash
&& receipt.capability_hash == plugin.capability_hash
})
})
.ok_or_else(|| {
format!(
"Plugin bundle `{}` state or review changed; refresh plugin state and review it if needed before enabling again",
plugin.name()
)
})?;
entry.generation = entry
.generation
.checked_add(1)
+12 -1
View File
@@ -490,11 +490,22 @@ fn stale_enable_cannot_resurrect_revoked_trust() {
revoker.revoke_trust("demo").unwrap();
assert!(super::registry::verify_plugin_state_authority(&authority).is_err());
stale.enable("demo").unwrap();
let revoked_state = fs::read(&config.state_path).unwrap();
assert!(stale.enable("demo").unwrap_err().contains("changed"));
assert_eq!(fs::read(&config.state_path).unwrap(), revoked_state);
let fresh = discover_with_config(&config);
assert!(fresh.get("demo").unwrap().enabled);
assert!(!fresh.get("demo").unwrap().trusted());
assert!(!fresh.is_active("demo"));
revoker.trust("demo").unwrap();
let reviewed_state = fs::read(&config.state_path).unwrap();
assert!(stale.enable("demo").unwrap_err().contains("changed"));
assert_eq!(fs::read(&config.state_path).unwrap(), reviewed_state);
let fresh = discover_with_config(&config);
assert!(!fresh.get("demo").unwrap().enabled);
assert!(fresh.get("demo").unwrap().trusted());
assert!(!fresh.is_active("demo"));
}
#[cfg(unix)]