cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit 18423233757059376f0c2ff4f805962dff5dc25a
parent a276c378cfd5c033d300da0350b580064e341d87
Author: triesap <tyson@radroots.org>
Date:   Sun, 26 Apr 2026 22:41:31 +0000

cli: add mvp operation registry

- define typed metadata for the approved CLI MVP operation set
- assert operation id, path, kind, tool, output, dry-run, and approval invariants
- wire a debug registry linkage check without changing public command behavior
- prove the registry with targeted tests and the nested check lane

Diffstat:
Msrc/main.rs | 2++
Asrc/operation_registry.rs | 1250+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 1252 insertions(+), 0 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -3,6 +3,7 @@ mod cli; mod commands; mod domain; +mod operation_registry; mod render; mod runtime; @@ -26,6 +27,7 @@ fn main() -> ExitCode { } fn run() -> Result<ExitCode, runtime::RuntimeError> { + debug_assert!(operation_registry::registry_linkage_is_valid()); let args = CliArgs::parse(); let config = RuntimeConfig::from_system(&args)?; validate_command_contracts(&args.command, &config)?; diff --git a/src/operation_registry.rs b/src/operation_registry.rs @@ -0,0 +1,1250 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct OperationSpec { + pub operation_id: &'static str, + pub cli_path: &'static str, + pub namespace: &'static str, + pub mcp_tool: &'static str, + pub rust_request: &'static str, + pub rust_result: &'static str, + pub json_kind: &'static str, + pub description: &'static str, + pub role: OperationRole, + pub mutates: bool, + pub approval_policy: ApprovalPolicy, + pub risk_level: RiskLevel, + pub supports_json: bool, + pub supports_ndjson: bool, + pub supports_dry_run: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApprovalPolicy { + None, + Conditional, + Required, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RiskLevel { + Low, + Medium, + High, + Critical, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OperationRole { + Any, + Buyer, + Seller, +} + +macro_rules! operation { + ( + $operation_id:literal, + $cli_path:literal, + $namespace:literal, + $mcp_tool:literal, + $rust_request:literal, + $rust_result:literal, + $description:literal, + $role:ident, + $mutates:literal, + $approval_policy:ident, + $risk_level:ident, + $supports_ndjson:literal, + $supports_dry_run:literal + ) => { + OperationSpec { + operation_id: $operation_id, + cli_path: $cli_path, + namespace: $namespace, + mcp_tool: $mcp_tool, + rust_request: $rust_request, + rust_result: $rust_result, + json_kind: $operation_id, + description: $description, + role: OperationRole::$role, + mutates: $mutates, + approval_policy: ApprovalPolicy::$approval_policy, + risk_level: RiskLevel::$risk_level, + supports_json: true, + supports_ndjson: $supports_ndjson, + supports_dry_run: $supports_dry_run, + } + }; +} + +pub const OPERATION_REGISTRY: &[OperationSpec] = &[ + operation!( + "workspace.init", + "radroots workspace init", + "workspace", + "workspace_init", + "WorkspaceInitRequest", + "WorkspaceInitResult", + "Initialize a local workspace/profile.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "workspace.get", + "radroots workspace get", + "workspace", + "workspace_get", + "WorkspaceGetRequest", + "WorkspaceGetResult", + "Read workspace/profile configuration and current context.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "health.status.get", + "radroots health status get", + "health", + "health_status_get", + "HealthStatusGetRequest", + "HealthStatusGetResult", + "Get concise health and readiness status.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "health.check.run", + "radroots health check run", + "health", + "health_check_run", + "HealthCheckRunRequest", + "HealthCheckRunResult", + "Run comprehensive diagnostics.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "config.get", + "radroots config get", + "config", + "config_get", + "ConfigGetRequest", + "ConfigGetResult", + "Read effective configuration.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "account.create", + "radroots account create", + "account", + "account_create", + "AccountCreateRequest", + "AccountCreateResult", + "Create a local account identity.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "account.import", + "radroots account import", + "account", + "account_import", + "AccountImportRequest", + "AccountImportResult", + "Import an existing account identity.", + Any, + true, + Required, + High, + false, + true + ), + operation!( + "account.get", + "radroots account get", + "account", + "account_get", + "AccountGetRequest", + "AccountGetResult", + "Get account details.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "account.list", + "radroots account list", + "account", + "account_list", + "AccountListRequest", + "AccountListResult", + "List known local accounts.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "account.remove", + "radroots account remove", + "account", + "account_remove", + "AccountRemoveRequest", + "AccountRemoveResult", + "Remove an account from local configuration/store.", + Any, + true, + Required, + High, + false, + true + ), + operation!( + "account.selection.get", + "radroots account selection get", + "account", + "account_selection_get", + "AccountSelectionGetRequest", + "AccountSelectionGetResult", + "Get selected account context.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "account.selection.update", + "radroots account selection update", + "account", + "account_selection_update", + "AccountSelectionUpdateRequest", + "AccountSelectionUpdateResult", + "Update selected account context.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "account.selection.clear", + "radroots account selection clear", + "account", + "account_selection_clear", + "AccountSelectionClearRequest", + "AccountSelectionClearResult", + "Clear selected account context.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "signer.status.get", + "radroots signer status get", + "signer", + "signer_status_get", + "SignerStatusGetRequest", + "SignerStatusGetResult", + "Get signer availability and permission status.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "relay.list", + "radroots relay list", + "relay", + "relay_list", + "RelayListRequest", + "RelayListResult", + "List configured relays.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "store.init", + "radroots store init", + "store", + "store_init", + "StoreInitRequest", + "StoreInitResult", + "Initialize local store.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "store.status.get", + "radroots store status get", + "store", + "store_status_get", + "StoreStatusGetRequest", + "StoreStatusGetResult", + "Read local store status.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "store.export", + "radroots store export", + "store", + "store_export", + "StoreExportRequest", + "StoreExportResult", + "Export local store data according to filters/policy.", + Any, + false, + Conditional, + Medium, + false, + false + ), + operation!( + "store.backup.create", + "radroots store backup create", + "store", + "store_backup_create", + "StoreBackupCreateRequest", + "StoreBackupCreateResult", + "Create local store backup.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "sync.status.get", + "radroots sync status get", + "sync", + "sync_status_get", + "SyncStatusGetRequest", + "SyncStatusGetResult", + "Read sync status.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "sync.pull", + "radroots sync pull", + "sync", + "sync_pull", + "SyncPullRequest", + "SyncPullResult", + "Pull remote updates into local store.", + Any, + true, + None, + Medium, + true, + true + ), + operation!( + "sync.push", + "radroots sync push", + "sync", + "sync_push", + "SyncPushRequest", + "SyncPushResult", + "Push local signed updates to relays.", + Any, + true, + Conditional, + High, + true, + true + ), + operation!( + "sync.watch", + "radroots sync watch", + "sync", + "sync_watch", + "SyncWatchRequest", + "SyncWatchResult", + "Stream sync events.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "runtime.status.get", + "radroots runtime status get", + "runtime", + "runtime_status_get", + "RuntimeStatusGetRequest", + "RuntimeStatusGetResult", + "Get runtime status.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "runtime.start", + "radroots runtime start", + "runtime", + "runtime_start", + "RuntimeStartRequest", + "RuntimeStartResult", + "Start runtime.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "runtime.stop", + "radroots runtime stop", + "runtime", + "runtime_stop", + "RuntimeStopRequest", + "RuntimeStopResult", + "Stop runtime.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "runtime.restart", + "radroots runtime restart", + "runtime", + "runtime_restart", + "RuntimeRestartRequest", + "RuntimeRestartResult", + "Restart runtime.", + Any, + true, + None, + Medium, + false, + true + ), + operation!( + "runtime.log.watch", + "radroots runtime log watch", + "runtime", + "runtime_log_watch", + "RuntimeLogWatchRequest", + "RuntimeLogWatchResult", + "Stream runtime logs.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "runtime.config.get", + "radroots runtime config get", + "runtime", + "runtime_config_get", + "RuntimeConfigGetRequest", + "RuntimeConfigGetResult", + "Read runtime configuration.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "job.get", + "radroots job get", + "job", + "job_get", + "JobGetRequest", + "JobGetResult", + "Get job details.", + Any, + false, + None, + Low, + false, + false + ), + operation!( + "job.list", + "radroots job list", + "job", + "job_list", + "JobListRequest", + "JobListResult", + "List jobs.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "job.watch", + "radroots job watch", + "job", + "job_watch", + "JobWatchRequest", + "JobWatchResult", + "Stream job events.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "farm.create", + "radroots farm create", + "farm", + "farm_create", + "FarmCreateRequest", + "FarmCreateResult", + "Create farm identity/profile resource.", + Seller, + true, + None, + Medium, + false, + true + ), + operation!( + "farm.get", + "radroots farm get", + "farm", + "farm_get", + "FarmGetRequest", + "FarmGetResult", + "Get farm resource.", + Seller, + false, + None, + Low, + false, + false + ), + operation!( + "farm.profile.update", + "radroots farm profile update", + "farm", + "farm_profile_update", + "FarmProfileUpdateRequest", + "FarmProfileUpdateResult", + "Update farm public profile fields.", + Seller, + true, + Conditional, + Medium, + false, + true + ), + operation!( + "farm.location.update", + "radroots farm location update", + "farm", + "farm_location_update", + "FarmLocationUpdateRequest", + "FarmLocationUpdateResult", + "Update farm location fields.", + Seller, + true, + Conditional, + Medium, + false, + true + ), + operation!( + "farm.fulfillment.update", + "radroots farm fulfillment update", + "farm", + "farm_fulfillment_update", + "FarmFulfillmentUpdateRequest", + "FarmFulfillmentUpdateResult", + "Update farm fulfillment posture.", + Seller, + true, + Conditional, + Medium, + false, + true + ), + operation!( + "farm.readiness.check", + "radroots farm readiness check", + "farm", + "farm_readiness_check", + "FarmReadinessCheckRequest", + "FarmReadinessCheckResult", + "Check whether farm is publish-ready.", + Seller, + false, + None, + Low, + false, + false + ), + operation!( + "farm.publish", + "radroots farm publish", + "farm", + "farm_publish", + "FarmPublishRequest", + "FarmPublishResult", + "Publish farm identity/profile externally.", + Seller, + true, + Required, + High, + false, + true + ), + operation!( + "listing.create", + "radroots listing create", + "listing", + "listing_create", + "ListingCreateRequest", + "ListingCreateResult", + "Create seller-owned listing/offer.", + Seller, + true, + None, + Medium, + false, + true + ), + operation!( + "listing.get", + "radroots listing get", + "listing", + "listing_get", + "ListingGetRequest", + "ListingGetResult", + "Get seller-owned listing/offer.", + Seller, + false, + None, + Low, + false, + false + ), + operation!( + "listing.list", + "radroots listing list", + "listing", + "listing_list", + "ListingListRequest", + "ListingListResult", + "List seller-owned listings/offers.", + Seller, + false, + None, + Low, + true, + false + ), + operation!( + "listing.update", + "radroots listing update", + "listing", + "listing_update", + "ListingUpdateRequest", + "ListingUpdateResult", + "Update general listing fields.", + Seller, + true, + Conditional, + Medium, + false, + true + ), + operation!( + "listing.validate", + "radroots listing validate", + "listing", + "listing_validate", + "ListingValidateRequest", + "ListingValidateResult", + "Validate listing for publication or orderability.", + Seller, + false, + None, + Low, + false, + false + ), + operation!( + "listing.publish", + "radroots listing publish", + "listing", + "listing_publish", + "ListingPublishRequest", + "ListingPublishResult", + "Publish listing externally.", + Seller, + true, + Required, + High, + false, + true + ), + operation!( + "listing.archive", + "radroots listing archive", + "listing", + "listing_archive", + "ListingArchiveRequest", + "ListingArchiveResult", + "Archive listing resource.", + Seller, + true, + Required, + High, + false, + true + ), + operation!( + "market.refresh", + "radroots market refresh", + "market", + "market_refresh", + "MarketRefreshRequest", + "MarketRefreshResult", + "Refresh local market projection.", + Buyer, + true, + None, + Medium, + true, + true + ), + operation!( + "market.product.search", + "radroots market product search", + "market", + "market_product_search", + "MarketProductSearchRequest", + "MarketProductSearchResult", + "Search market products/listings.", + Buyer, + false, + None, + Low, + true, + false + ), + operation!( + "market.listing.get", + "radroots market listing get", + "market", + "market_listing_get", + "MarketListingGetRequest", + "MarketListingGetResult", + "Get market listing details.", + Buyer, + false, + None, + Low, + false, + false + ), + operation!( + "basket.create", + "radroots basket create", + "basket", + "basket_create", + "BasketCreateRequest", + "BasketCreateResult", + "Create local basket.", + Buyer, + true, + None, + Medium, + false, + true + ), + operation!( + "basket.get", + "radroots basket get", + "basket", + "basket_get", + "BasketGetRequest", + "BasketGetResult", + "Get local basket.", + Buyer, + false, + None, + Low, + false, + false + ), + operation!( + "basket.list", + "radroots basket list", + "basket", + "basket_list", + "BasketListRequest", + "BasketListResult", + "List local baskets.", + Buyer, + false, + None, + Low, + true, + false + ), + operation!( + "basket.item.add", + "radroots basket item add", + "basket", + "basket_item_add", + "BasketItemAddRequest", + "BasketItemAddResult", + "Add item to local basket.", + Buyer, + true, + None, + Medium, + false, + true + ), + operation!( + "basket.item.update", + "radroots basket item update", + "basket", + "basket_item_update", + "BasketItemUpdateRequest", + "BasketItemUpdateResult", + "Update local basket item.", + Buyer, + true, + None, + Medium, + false, + true + ), + operation!( + "basket.item.remove", + "radroots basket item remove", + "basket", + "basket_item_remove", + "BasketItemRemoveRequest", + "BasketItemRemoveResult", + "Remove item from local basket.", + Buyer, + true, + None, + Medium, + false, + true + ), + operation!( + "basket.validate", + "radroots basket validate", + "basket", + "basket_validate", + "BasketValidateRequest", + "BasketValidateResult", + "Validate basket orderability.", + Buyer, + false, + None, + Low, + false, + false + ), + operation!( + "basket.quote.create", + "radroots basket quote create", + "basket", + "basket_quote_create", + "BasketQuoteCreateRequest", + "BasketQuoteCreateResult", + "Create deterministic basket quote.", + Buyer, + true, + Conditional, + Medium, + false, + true + ), + operation!( + "order.submit", + "radroots order submit", + "order", + "order_submit", + "OrderSubmitRequest", + "OrderSubmitResult", + "Submit quoted basket as an order.", + Buyer, + true, + Required, + Critical, + false, + true + ), + operation!( + "order.get", + "radroots order get", + "order", + "order_get", + "OrderGetRequest", + "OrderGetResult", + "Get order details.", + Buyer, + false, + None, + Low, + false, + false + ), + operation!( + "order.list", + "radroots order list", + "order", + "order_list", + "OrderListRequest", + "OrderListResult", + "List orders.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "order.event.list", + "radroots order event list", + "order", + "order_event_list", + "OrderEventListRequest", + "OrderEventListResult", + "List order events.", + Any, + false, + None, + Low, + true, + false + ), + operation!( + "order.event.watch", + "radroots order event watch", + "order", + "order_event_watch", + "OrderEventWatchRequest", + "OrderEventWatchResult", + "Stream order events.", + Any, + false, + None, + Low, + true, + false + ), +]; + +pub fn get_operation(operation_id: &str) -> Option<&'static OperationSpec> { + OPERATION_REGISTRY + .iter() + .find(|operation| operation.operation_id == operation_id) +} + +pub fn registry_linkage_is_valid() -> bool { + OPERATION_REGISTRY.iter().all(|operation| { + get_operation(operation.operation_id).is_some() + && operation.operation_id == operation.json_kind + && operation.mcp_tool == operation.operation_id.replace('.', "_") + && operation.supports_json + }) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use super::{ApprovalPolicy, OPERATION_REGISTRY, OperationRole, RiskLevel, get_operation}; + + const EXPECTED_OPERATION_IDS: &[&str] = &[ + "workspace.init", + "workspace.get", + "health.status.get", + "health.check.run", + "config.get", + "account.create", + "account.import", + "account.get", + "account.list", + "account.remove", + "account.selection.get", + "account.selection.update", + "account.selection.clear", + "signer.status.get", + "relay.list", + "store.init", + "store.status.get", + "store.export", + "store.backup.create", + "sync.status.get", + "sync.pull", + "sync.push", + "sync.watch", + "runtime.status.get", + "runtime.start", + "runtime.stop", + "runtime.restart", + "runtime.log.watch", + "runtime.config.get", + "job.get", + "job.list", + "job.watch", + "farm.create", + "farm.get", + "farm.profile.update", + "farm.location.update", + "farm.fulfillment.update", + "farm.readiness.check", + "farm.publish", + "listing.create", + "listing.get", + "listing.list", + "listing.update", + "listing.validate", + "listing.publish", + "listing.archive", + "market.refresh", + "market.product.search", + "market.listing.get", + "basket.create", + "basket.get", + "basket.list", + "basket.item.add", + "basket.item.update", + "basket.item.remove", + "basket.validate", + "basket.quote.create", + "order.submit", + "order.get", + "order.list", + "order.event.list", + "order.event.watch", + ]; + + #[test] + fn registry_contains_exact_mvp_operation_set() { + let actual = operation_ids(); + let expected = EXPECTED_OPERATION_IDS + .iter() + .copied() + .collect::<BTreeSet<_>>(); + assert_eq!(actual, expected); + assert_eq!(OPERATION_REGISTRY.len(), 62); + } + + #[test] + fn registry_identity_fields_are_consistent() { + let mut operation_ids = BTreeSet::new(); + let mut cli_paths = BTreeSet::new(); + let mut mcp_tools = BTreeSet::new(); + + for operation in OPERATION_REGISTRY { + assert!(operation_ids.insert(operation.operation_id)); + assert!(cli_paths.insert(operation.cli_path)); + assert!(mcp_tools.insert(operation.mcp_tool)); + assert_eq!(operation.operation_id, operation.json_kind); + assert_eq!(operation.mcp_tool, operation.operation_id.replace('.', "_")); + assert!(operation.cli_path.starts_with("radroots ")); + assert_eq!( + operation.namespace, + operation.operation_id.split('.').next().unwrap() + ); + assert_eq!( + operation.rust_request, + format!("{}Request", pascal_case(operation.operation_id)) + ); + assert_eq!( + operation.rust_result, + format!("{}Result", pascal_case(operation.operation_id)) + ); + assert!(operation.supports_json); + assert!(!operation.description.is_empty()); + } + } + + #[test] + fn registry_policy_invariants_hold() { + let required = OPERATION_REGISTRY + .iter() + .filter(|operation| operation.approval_policy == ApprovalPolicy::Required) + .map(|operation| operation.operation_id) + .collect::<BTreeSet<_>>(); + let expected_required = [ + "account.import", + "account.remove", + "farm.publish", + "listing.publish", + "listing.archive", + "order.submit", + ] + .into_iter() + .collect::<BTreeSet<_>>(); + + assert_eq!(required, expected_required); + + for operation in OPERATION_REGISTRY { + if operation.mutates { + assert!(operation.supports_dry_run, "{}", operation.operation_id); + } + + if operation.approval_policy == ApprovalPolicy::Required { + assert!( + matches!(operation.risk_level, RiskLevel::High | RiskLevel::Critical), + "{}", + operation.operation_id + ); + } + } + } + + #[test] + fn registry_ndjson_support_is_explicit() { + let actual = OPERATION_REGISTRY + .iter() + .filter(|operation| operation.supports_ndjson) + .map(|operation| operation.operation_id) + .collect::<BTreeSet<_>>(); + let expected = [ + "account.list", + "relay.list", + "sync.pull", + "sync.push", + "sync.watch", + "runtime.log.watch", + "job.list", + "job.watch", + "listing.list", + "market.refresh", + "market.product.search", + "basket.list", + "order.list", + "order.event.list", + "order.event.watch", + ] + .into_iter() + .collect::<BTreeSet<_>>(); + + assert_eq!(actual, expected); + } + + #[test] + fn deferred_namespaces_are_absent() { + let namespaces = OPERATION_REGISTRY + .iter() + .map(|operation| operation.namespace) + .collect::<BTreeSet<_>>(); + + assert!(!namespaces.contains("product")); + assert!(!namespaces.contains("message")); + assert!(!namespaces.contains("approval")); + assert!(!namespaces.contains("agent")); + } + + #[test] + fn roles_are_assigned_to_marketplace_operations() { + assert_eq!( + get_operation("listing.publish").unwrap().role, + OperationRole::Seller + ); + assert_eq!( + get_operation("basket.quote.create").unwrap().role, + OperationRole::Buyer + ); + assert_eq!( + get_operation("order.list").unwrap().role, + OperationRole::Any + ); + } + + fn operation_ids() -> BTreeSet<&'static str> { + OPERATION_REGISTRY + .iter() + .map(|operation| operation.operation_id) + .collect() + } + + fn pascal_case(operation_id: &str) -> String { + operation_id + .split('.') + .flat_map(|part| part.split('_')) + .map(|part| { + let mut chars = part.chars(); + let first = chars.next().unwrap().to_ascii_uppercase(); + format!("{first}{}", chars.as_str()) + }) + .collect() + } +}