rhi

Coordinated trade for connected markets
git clone https://radroots.dev/git/rhi.git
Log | Files | Refs | README | LICENSE

services_hardening_operator_contract.rs (16426B)


      1 #![forbid(unsafe_code)]
      2 
      3 use serde_json::Value;
      4 use sha2::{Digest, Sha256};
      5 use std::collections::BTreeSet;
      6 
      7 const OPERATOR_CONTRACT: &str =
      8     include_str!("../contracts/services_hardening/operator_contract.v1.json");
      9 const CONSUMER_ROOT: &str = include_str!("../.radroots-consumer-root");
     10 
     11 fn contract() -> Value {
     12     serde_json::from_str(OPERATOR_CONTRACT).expect("operator contract must be valid JSON")
     13 }
     14 
     15 fn sha256_hex(bytes: &[u8]) -> String {
     16     Sha256::digest(bytes)
     17         .iter()
     18         .map(|byte| format!("{byte:02x}"))
     19         .collect()
     20 }
     21 
     22 fn decision_sections_digest(value: &Value) -> String {
     23     let sections = serde_json::json!({
     24         "identity_contract": value["admin"]["identity_contract"],
     25         "model_wire_contract": value["admin"]["model_wire_contract"],
     26         "models": value["admin"]["models"],
     27         "mutation_contract": value["admin"]["mutation_contract"],
     28         "pagination": value["admin"]["pagination"],
     29         "path_parameters": value["admin"]["path_parameters"],
     30         "report_contract": value["admin"]["report_contract"],
     31         "types": value["admin"]["types"]
     32     });
     33     sha256_hex(&serde_json::to_vec(&sections).expect("serialize decision sections"))
     34 }
     35 
     36 #[test]
     37 fn source_lock_identity_and_shared_host_reference_are_exact() {
     38     assert_eq!(CONSUMER_ROOT, "rhi\n");
     39     let value = contract();
     40     assert_eq!(value["schema"], "radroots.rhi.operator-contract.v1");
     41     assert_eq!(value["contract_version"], 1);
     42     assert_eq!(value["decision_state"], "reserved_preimplementation");
     43     assert_eq!(value["service"], "rhi");
     44     assert_eq!(
     45         value["shared_host_contract"],
     46         serde_json::json!({
     47             "repository": "https://github.com/radrootslabs/lib",
     48             "path": "contracts/architecture/decisions/services_hardening_host.v1.json",
     49             "schema": "radroots.services-hardening.host-decisions.v1",
     50             "contract_version": 1
     51         })
     52     );
     53 }
     54 
     55 #[test]
     56 fn admin_inventory_is_closed_unique_and_model_complete() {
     57     let value = contract();
     58     assert_eq!(
     59         value["admin"]["transport"],
     60         "http_1_1_over_unix_domain_socket"
     61     );
     62     assert_eq!(value["admin"]["base_path"], "/v1");
     63     assert_eq!(value["admin"]["route_inventory_closed"], true);
     64     let routes = value["admin"]["routes"].as_array().expect("routes");
     65     let exact_routes = routes
     66         .iter()
     67         .map(|route| {
     68             format!(
     69                 "{}|{}|{}|{}|{}|{}",
     70                 route["method"].as_str().unwrap(),
     71                 route["path"].as_str().unwrap(),
     72                 route["operation_id"].as_str().unwrap(),
     73                 route["request_model"].as_str().unwrap(),
     74                 route["response_model"].as_str().unwrap(),
     75                 route["mutation"].as_bool().unwrap()
     76             )
     77         })
     78         .collect::<Vec<_>>();
     79     assert_eq!(
     80         exact_routes,
     81         [
     82             "GET|/v1/status|radroots.rhi.status.get.v1|empty|service_status_v1|false",
     83             "GET|/v1/config/effective|radroots.rhi.config.effective.get.v1|empty|effective_config_v1|false",
     84             "GET|/v1/identity/status|radroots.rhi.identity.status.get.v1|identity_status_query_v1|identity_status_v1|false",
     85             "GET|/v1/identity/public|radroots.rhi.identity.public.get.v1|identity_public_query_v1|identity_public_v1|false",
     86             "GET|/v1/state/status|radroots.rhi.state.status.get.v1|empty|state_status_v1|false",
     87             "POST|/v1/state/backup|radroots.rhi.state.backup.create.v1|state_backup_request_v1|state_backup_receipt_v1|true",
     88             "GET|/v1/metrics/snapshot|radroots.rhi.metrics.snapshot.get.v1|empty|metrics_snapshot_v1|false",
     89             "GET|/v1/reconciliation/status|radroots.rhi.reconciliation.status.get.v1|empty|reconciliation_status_v1|false",
     90             "GET|/v1/reconciliation/jobs|radroots.rhi.reconciliation.jobs.list.v1|reconciliation_jobs_query_v1|reconciliation_jobs_page_v1|false",
     91             "POST|/v1/reconciliation/refresh|radroots.rhi.reconciliation.refresh.v1|reconciliation_refresh_request_v1|reconciliation_refresh_receipt_v1|true",
     92             "GET|/v1/sources|radroots.rhi.sources.list.v1|sources_query_v1|sources_page_v1|false",
     93             "GET|/v1/trades/{trade_id}/projection|radroots.rhi.trade.projection.get.v1|empty|trade_projection_v1|false",
     94             "GET|/v1/trades/{trade_id}/reports/current|radroots.rhi.trade.report.current.get.v1|empty|report_detail_v1|false",
     95             "GET|/v1/trades/{trade_id}/reports|radroots.rhi.trade.reports.list.v1|reports_query_v1|reports_page_v1|false",
     96             "GET|/v1/publication/backlog|radroots.rhi.publication.backlog.list.v1|publication_backlog_query_v1|publication_backlog_page_v1|false",
     97             "GET|/v1/publication/targets|radroots.rhi.publication.targets.list.v1|publication_targets_query_v1|publication_targets_page_v1|false",
     98             "POST|/v1/publication/retry|radroots.rhi.publication.retry.v1|publication_retry_request_v1|publication_retry_receipt_v1|true",
     99             "GET|/v1/presence/desired|radroots.rhi.presence.desired.get.v1|empty|presence_desired_v1|false",
    100             "POST|/v1/presence/render|radroots.rhi.presence.render.v1|presence_render_request_v1|presence_render_receipt_v1|true",
    101             "POST|/v1/presence/refresh|radroots.rhi.presence.refresh.v1|presence_refresh_request_v1|presence_refresh_receipt_v1|true"
    102         ]
    103     );
    104     assert_eq!(routes.len(), 20);
    105     let route_keys = routes
    106         .iter()
    107         .map(|route| format!("{} {}", route["method"], route["path"]))
    108         .collect::<BTreeSet<_>>();
    109     let operation_ids = routes
    110         .iter()
    111         .map(|route| route["operation_id"].as_str().expect("operation ID"))
    112         .collect::<BTreeSet<_>>();
    113     assert_eq!(route_keys.len(), routes.len());
    114     assert_eq!(operation_ids.len(), routes.len());
    115     assert!(
    116         operation_ids
    117             .iter()
    118             .all(|id| id.starts_with("radroots.rhi.") && id.ends_with(".v1"))
    119     );
    120 
    121     let models = value["admin"]["models"].as_object().expect("models");
    122     let types = value["admin"]["types"].as_object().expect("types");
    123     assert_eq!(models.len(), 33);
    124     for route in routes {
    125         for key in ["request_model", "response_model"] {
    126             let model = route[key].as_str().expect("model reference");
    127             assert!(models.contains_key(model), "missing model {model}");
    128         }
    129         assert_eq!(route["mutation"], route["method"] == "POST");
    130     }
    131     for (model_name, model) in models {
    132         assert_eq!(
    133             model
    134                 .as_object()
    135                 .unwrap()
    136                 .keys()
    137                 .map(String::as_str)
    138                 .collect::<Vec<_>>(),
    139             ["fields"],
    140             "model {model_name} must be a closed field inventory"
    141         );
    142         for (field_name, field) in model["fields"].as_object().unwrap() {
    143             assert_eq!(
    144                 field
    145                     .as_object()
    146                     .unwrap()
    147                     .keys()
    148                     .map(String::as_str)
    149                     .collect::<Vec<_>>(),
    150                 ["presence", "type"],
    151                 "field {model_name}.{field_name} must bind only type and presence"
    152             );
    153             assert!(matches!(
    154                 field["presence"].as_str(),
    155                 Some("required" | "optional")
    156             ));
    157             let type_name = field["type"].as_str().unwrap();
    158             assert!(types.contains_key(type_name), "unknown type {type_name}");
    159         }
    160     }
    161     for (type_name, descriptor) in types {
    162         let referenced = match descriptor["kind"].as_str().unwrap() {
    163             "array" => vec![descriptor["items"].as_str().unwrap()],
    164             "map" => vec![
    165                 descriptor["key"].as_str().unwrap(),
    166                 descriptor["value"].as_str().unwrap(),
    167             ],
    168             "closed_object" => descriptor["fields"]
    169                 .as_object()
    170                 .unwrap()
    171                 .values()
    172                 .map(|field| field.as_str().unwrap())
    173                 .collect(),
    174             "tagged_union" => descriptor["variants"]
    175                 .as_array()
    176                 .unwrap()
    177                 .iter()
    178                 .map(|variant| variant.as_str().unwrap())
    179                 .collect(),
    180             "alias" => vec![descriptor["target"].as_str().unwrap()],
    181             "optional" => vec![descriptor["value"].as_str().unwrap()],
    182             "boolean" | "canonical_json_object" | "enum" | "integer" | "literal" | "string" => {
    183                 Vec::new()
    184             }
    185             kind => panic!("unknown descriptor kind {kind} for {type_name}"),
    186         };
    187         for reference in referenced {
    188             assert!(
    189                 types.contains_key(reference),
    190                 "type {type_name} references missing type {reference}"
    191             );
    192         }
    193     }
    194     assert_eq!(
    195         value["admin"]["identity_contract"],
    196         serde_json::json!({
    197             "roles": [{ "id": "service", "required": true, "disabled_allowed": false, "providers": ["encrypted_file"] }],
    198             "rotation_mode": "offline_create_new_configuration_apply_restart",
    199             "live_rekey_route": false,
    200             "live_replace_route": false
    201         })
    202     );
    203     for removed in [
    204         "identity_rekey_request_v1",
    205         "identity_replace_request_v1",
    206         "identity_mutation_receipt_v1",
    207     ] {
    208         assert!(!models.contains_key(removed));
    209     }
    210     for removed in [
    211         "credential_reference",
    212         "encrypted_file_provider",
    213         "identity_provider_replacement",
    214         "encrypted_file_replacement",
    215     ] {
    216         assert!(!types.contains_key(removed));
    217     }
    218     assert_eq!(
    219         value["admin"]["types"]["service_phase"],
    220         serde_json::json!({ "kind": "enum", "values": ["starting", "ready", "degraded", "unready", "stopping", "failed"] })
    221     );
    222     assert_eq!(
    223         value["admin"]["types"]["coverage"],
    224         serde_json::json!({ "kind": "enum", "values": ["Missing", "Partial", "ScopeSatisfied", "Unsupported"] })
    225     );
    226     assert_eq!(
    227         value["admin"]["types"]["outcome"],
    228         serde_json::json!({ "kind": "enum", "values": ["Valid", "Invalid", "Indeterminate"] })
    229     );
    230     assert_eq!(
    231         value["admin"]["types"]["provider_state"]["fields"],
    232         serde_json::json!({
    233             "health": "provider_health",
    234             "identity": "identity_health",
    235             "reason_codes": "reason_codes"
    236         })
    237     );
    238     assert_eq!(
    239         value["admin"]["types"]["transport_state"]["fields"],
    240         serde_json::json!({
    241             "health": "transport_health",
    242             "required_sources_ready": "bool",
    243             "subscriber_active": "bool",
    244             "configured_source_count": "u64",
    245             "reachable_source_count": "u64",
    246             "reason_codes": "reason_codes"
    247         })
    248     );
    249     assert_eq!(
    250         value["admin"]["types"]["rhi_status"]["fields"],
    251         serde_json::json!({
    252             "identity": "identity_health",
    253             "reconciliation": "reconciliation_status",
    254             "publication": "publication_status",
    255             "presence": "presence_status"
    256         })
    257     );
    258     assert_eq!(
    259         value["admin"]["models"]["service_status_v1"]["fields"]
    260             .as_object()
    261             .unwrap()
    262             .keys()
    263             .map(String::as_str)
    264             .collect::<Vec<_>>(),
    265         [
    266             "build_info",
    267             "configuration",
    268             "contract_version",
    269             "instance",
    270             "persistence",
    271             "phase",
    272             "provider",
    273             "ready",
    274             "reason_codes",
    275             "rhi",
    276             "service",
    277             "transport",
    278             "uptime_millis"
    279         ]
    280     );
    281     assert_eq!(
    282         value["admin"]["path_parameters"],
    283         serde_json::json!({
    284             "trade_id": { "type": "trade_id", "source": "percent_decoded_single_path_segment", "slash_allowed": false }
    285         })
    286     );
    287     assert_eq!(
    288         value["admin"]["report_contract"],
    289         serde_json::json!({
    290             "content_contract": "radroots.rhi.evidence_attestation.v1",
    291             "report_id_equals_statement_digest": true,
    292             "supersession_reference_presence": "supersedes_report_id_and_supersedes_event_id_both_or_neither",
    293             "current_selection": ["trade_generation_descending", "observed_at_unix_s_descending", "statement_digest_descending"],
    294             "relay_arrival_order_authoritative": false
    295         })
    296     );
    297     assert_eq!(
    298         value["admin"]["pagination"],
    299         serde_json::json!({
    300             "cursor_type": "page_cursor",
    301             "limit_min": 1,
    302             "limit_max": 200,
    303             "terminal_page": "next_cursor_field_absent",
    304             "cursor_reuse": "same_route_same_filters_only",
    305             "filter_or_route_mismatch": "invalid_cursor",
    306             "jobs_order": ["scheduled_at_utc_ascending", "job_id_ascending"],
    307             "jobs_snapshot": "maximum_job_sequence_fixed_by_first_page_cursor",
    308             "sources_order": ["source_id_ascending"],
    309             "sources_snapshot": "configuration_generation_fixed_by_first_page_cursor",
    310             "reports_order": ["observed_at_utc_descending", "report_id_descending"],
    311             "reports_snapshot": "maximum_report_sequence_fixed_by_first_page_cursor",
    312             "publication_backlog_order": ["next_attempt_at_utc_ascending_nulls_first", "workflow_id_ascending"],
    313             "publication_backlog_snapshot": "maximum_publication_sequence_fixed_by_first_page_cursor",
    314             "publication_targets_order": ["workflow_id_ascending", "target_id_ascending"],
    315             "publication_targets_snapshot": "maximum_publication_target_sequence_fixed_by_first_page_cursor"
    316         })
    317     );
    318     let mutation_operations = routes
    319         .iter()
    320         .filter(|route| route["mutation"] == true)
    321         .map(|route| route["operation_id"].as_str().unwrap())
    322         .collect::<BTreeSet<_>>();
    323     let committed_effects = value["admin"]["mutation_contract"]["committed_effects"]
    324         .as_object()
    325         .unwrap()
    326         .keys()
    327         .map(String::as_str)
    328         .collect::<BTreeSet<_>>();
    329     assert_eq!(mutation_operations, committed_effects);
    330     assert_eq!(
    331         decision_sections_digest(&value),
    332         "49376e3bdf0e44c35f877ecd382f26bc9c38fec8fea7a674aa7d4da52ee00c62"
    333     );
    334 }
    335 
    336 #[test]
    337 fn doctor_exit_and_tcp_contracts_are_exact() {
    338     let value = contract();
    339     assert_eq!(
    340         value["doctor"]["shared_schema"],
    341         "radroots.service.doctor.v1"
    342     );
    343     assert_eq!(value["doctor"]["contract_version"], 1);
    344     assert_eq!(value["doctor"]["execution"], "ordered");
    345     assert_eq!(value["doctor"]["pass_requires_all_scope"], true);
    346     assert_eq!(
    347         value["doctor"]["checks"]
    348             .as_array()
    349             .expect("doctor checks")
    350             .len(),
    351         15
    352     );
    353     assert_eq!(
    354         value["exit_codes"],
    355         serde_json::json!([
    356             { "code": 0, "name": "success", "meaning": "successful command or completed graceful first-signal shutdown" },
    357             { "code": 1, "name": "unexpected_internal", "meaning": "unexpected invariant, critical task, or internal failure" },
    358             { "code": 2, "name": "input_or_configuration", "meaning": "CLI, config, validation, or unsupported contract input" },
    359             { "code": 3, "name": "service_or_dependency_unavailable", "meaning": "daemon, required provider, relay, source, or local dependency unavailable" },
    360             { "code": 4, "name": "state_or_identity_unavailable", "meaning": "state, schema, lock, credential, or identity unavailable" },
    361             { "code": 5, "name": "operation_rejected_or_conflict", "meaning": "authorization rejection, idempotency conflict, stale generation, or domain conflict" },
    362             { "code": 6, "name": "doctor_required_check_failed", "meaning": "one or more required doctor checks failed or timed out" }
    363         ])
    364     );
    365     assert_eq!(
    366         value["tcp_operations"],
    367         serde_json::json!({
    368             "routes": [
    369                 { "method": "GET", "path": "/livez", "source": "cached_supervisor_state" },
    370                 { "method": "GET", "path": "/readyz", "source": "cached_readiness_state" },
    371                 { "method": "GET", "path": "/metrics", "source": "cached_bounded_metrics_snapshot" }
    372             ],
    373             "active_probe_per_request": false,
    374             "additional_routes": false
    375         })
    376     );
    377 }