commit 7e9023919e2ad6b1748be4ea17d2213321bfb864
parent fb2711c02bec2c399a5a533d19568b759ec68332
Author: triesap <tyson@radroots.org>
Date: Sat, 27 Jun 2026 08:27:15 +0000
cli: harden farm recovery actions
- centralize approval-aware farm rebind recovery commands
- include dry-run and approval-token actions for stale farm bindings
- reuse farm-bound seller recovery actions from listing defaults
- prove terminal and JSON recovery output in signer tests
Diffstat:
3 files changed, 74 insertions(+), 23 deletions(-)
diff --git a/src/runtime/farm.rs b/src/runtime/farm.rs
@@ -212,10 +212,7 @@ fn rebind_inner(
"farm seller binding updated".to_owned()
}),
actions: if dry_run {
- vec![format!(
- "radroots --approval-token approve farm rebind {}",
- args.selector
- )]
+ vec![farm_rebind_live_action(args.selector.as_str())]
} else {
vec!["radroots farm readiness check".to_owned()]
},
@@ -522,8 +519,7 @@ pub fn status(
};
let mut actions = Vec::new();
if account.is_none() {
- actions.push("radroots account import <path>".to_owned());
- actions.push("radroots farm rebind <selector>".to_owned());
+ actions.extend(farm_bound_seller_recovery_actions("<selector>"));
} else if draft_missing.is_empty() {
actions.extend(publish.actions.clone());
} else {
@@ -722,10 +718,7 @@ pub fn publish(
resolved.document.selection.account
),
vec!["Farm-bound seller account".to_owned()],
- vec![
- "radroots account import <path>".to_owned(),
- "radroots farm rebind <selector>".to_owned(),
- ],
+ farm_bound_seller_recovery_actions("<selector>"),
config.output.dry_run,
true,
resolved.document.selection.account.clone(),
@@ -1583,7 +1576,7 @@ fn init_document(
&& document.selection.account != account.record.account_id.to_string()
{
let message = format!(
- "account mismatch: farm config is bound to seller account `{}`; use `radroots farm rebind {}` to change the farm-bound seller account",
+ "account mismatch: farm config is bound to seller account `{}`; use `radroots --approval-token approve farm rebind {}` to change the farm-bound seller account",
document.selection.account, account.record.account_id
);
return Err(account::AccountRuntimeFailure::mismatch_with_detail(
@@ -1592,7 +1585,7 @@ fn init_document(
"seller_actor_source": FARM_SELLER_ACTOR_SOURCE,
"farm_bound_seller_account_id": document.selection.account,
"attempted_seller_account_id": account.record.account_id.to_string(),
- "actions": [format!("radroots farm rebind {}", account.record.account_id)],
+ "actions": farm_rebind_recovery_actions(account.record.account_id.as_str()),
}),
)
.into());
@@ -1768,7 +1761,7 @@ fn farm_setup_actions(
) -> Vec<String> {
let mut actions = vec!["radroots farm readiness check".to_owned()];
if account.is_none() {
- actions.extend(farm_bound_seller_recovery_actions());
+ actions.extend(farm_bound_seller_recovery_actions("<selector>"));
return actions;
}
if farm_config::missing_fields(document).is_empty()
@@ -1785,13 +1778,27 @@ fn missing_farm_bound_seller_reason(account_id: &str) -> String {
format!("farm-bound seller account `{account_id}` is not present in the local account store")
}
-fn farm_bound_seller_recovery_actions() -> Vec<String> {
+pub(crate) fn farm_bound_seller_recovery_actions(selector: &str) -> Vec<String> {
+ let mut actions = vec!["radroots account import <path>".to_owned()];
+ actions.extend(farm_rebind_recovery_actions(selector));
+ actions
+}
+
+pub(crate) fn farm_rebind_recovery_actions(selector: &str) -> Vec<String> {
vec![
- "radroots account import <path>".to_owned(),
- "radroots farm rebind <selector>".to_owned(),
+ farm_rebind_dry_run_action(selector),
+ farm_rebind_live_action(selector),
]
}
+pub(crate) fn farm_rebind_dry_run_action(selector: &str) -> String {
+ format!("radroots --dry-run farm rebind {selector}")
+}
+
+pub(crate) fn farm_rebind_live_action(selector: &str) -> String {
+ format!("radroots --approval-token approve farm rebind {selector}")
+}
+
fn account_recovery_actions() -> Vec<String> {
vec![
"radroots account import <path>".to_owned(),
diff --git a/src/runtime/listing.rs b/src/runtime/listing.rs
@@ -2901,10 +2901,7 @@ fn authoring_defaults(config: &RuntimeConfig) -> Result<ListingAuthoringDefaults
json!({
"seller_actor_source": "farm_config",
"farm_bound_seller_account_id": account_id,
- "actions": [
- "radroots account import <path>",
- "radroots farm rebind <selector>",
- ],
+ "actions": crate::runtime::farm::farm_bound_seller_recovery_actions("<selector>"),
}),
)
.into());
diff --git a/tests/signer_runtime_modes.rs b/tests/signer_runtime_modes.rs
@@ -1571,7 +1571,46 @@ fn farm_rebind_is_explicit_and_publish_defaults_ignore_ambient_selection() {
);
assert_next_action_present(
&retarget,
- format!("radroots farm rebind {second_account_id}").as_str(),
+ format!("radroots --dry-run farm rebind {second_account_id}").as_str(),
+ );
+ assert_next_action_present(
+ &retarget,
+ format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(),
+ );
+
+ let terminal_retarget_output = sandbox
+ .command()
+ .args([
+ "farm",
+ "create",
+ "--name",
+ "Green Farm Retarget",
+ "--location",
+ "farmstand",
+ "--city",
+ "San Francisco",
+ "--country",
+ "US",
+ "--geohash",
+ "9q8yy",
+ "--delivery-method",
+ "pickup",
+ ])
+ .output()
+ .expect("terminal retarget");
+ assert!(!terminal_retarget_output.status.success());
+ assert!(terminal_retarget_output.stdout.is_empty());
+ let terminal_stderr =
+ String::from_utf8(terminal_retarget_output.stderr).expect("terminal stderr");
+ assert!(
+ terminal_stderr.contains("Next\n radroots --dry-run farm rebind"),
+ "{terminal_stderr}"
+ );
+ assert!(
+ terminal_stderr.contains(
+ format!("radroots --approval-token approve farm rebind {second_account_id}").as_str(),
+ ),
+ "{terminal_stderr}"
);
let (missing_rebind_output, missing_rebind) = sandbox.json_output(&[
@@ -1790,7 +1829,11 @@ fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() {
assert_eq!(updated["operation_id"], "farm.profile.update");
assert_contains(&updated["result"]["reason"], "farm-bound seller account");
assert_action_present(&updated, "radroots account import <path>");
- assert_action_present(&updated, "radroots farm rebind <selector>");
+ assert_action_present(&updated, "radroots --dry-run farm rebind <selector>");
+ assert_action_present(
+ &updated,
+ "radroots --approval-token approve farm rebind <selector>",
+ );
let listing_path = sandbox.root().join("missing-seller-listing.toml");
let (listing_output, listing) = sandbox.json_output(&[
@@ -1841,7 +1884,11 @@ fn missing_farm_bound_seller_blocks_listing_create_and_guides_setup_repair() {
first_account_id
);
assert_next_action_present(&listing, "radroots account import <path>");
- assert_next_action_present(&listing, "radroots farm rebind <selector>");
+ assert_next_action_present(&listing, "radroots --dry-run farm rebind <selector>");
+ assert_next_action_present(
+ &listing,
+ "radroots --approval-token approve farm rebind <selector>",
+ );
assert!(!listing_path.exists());
}