diff --git a/src/capabilities/evidence.rs b/src/capabilities/evidence.rs index 5cbe8b6c..31bfdedb 100644 --- a/src/capabilities/evidence.rs +++ b/src/capabilities/evidence.rs @@ -274,6 +274,8 @@ pub(crate) struct CapabilityObservation { struct RuntimeCapabilityObservation { observation: CapabilityObservation, configuration_generation: Option, + operation_domain: &'static str, + operation_method: &'static str, } const MAX_SOURCE_CONFIGURATIONS: usize = 8; @@ -611,6 +613,8 @@ impl CapabilityRegistry { observation: Option, configuration_generation: Option, operation: &Value, + operation_domain: &'static str, + operation_method: &'static str, ) -> Result<()> { let Some(observation) = observation else { return Ok(()); @@ -621,7 +625,13 @@ impl CapabilityRegistry { "runtime operation admission is missing operationId" )); }; - if !runtime_operation_matches(operation, operation_id, configuration_generation) { + if !runtime_operation_matches( + operation, + operation_id, + configuration_generation, + operation_domain, + operation_method, + ) { self.abandon_observation(Some(observation))?; return Err(anyhow!( "runtime operation admission does not match capability observation" @@ -635,6 +645,8 @@ impl CapabilityRegistry { RuntimeCapabilityObservation { observation, configuration_generation, + operation_domain, + operation_method, }, ); Ok(()) @@ -664,7 +676,13 @@ impl CapabilityRegistry { let Some(lease) = state.runtime_observations.get(operation_id) else { return Ok(()); }; - if !runtime_operation_matches(operation, operation_id, lease.configuration_generation) { + if !runtime_operation_matches( + operation, + operation_id, + lease.configuration_generation, + lease.operation_domain, + lease.operation_method, + ) { return Ok(()); } let Some(lease) = state.runtime_observations.remove(operation_id) else { @@ -735,10 +753,12 @@ fn runtime_operation_matches( operation: &Value, operation_id: &str, configuration_generation: Option, + operation_domain: &str, + operation_method: &str, ) -> bool { runtime_operation_string(operation, "operationId") == Some(operation_id) - && runtime_operation_string(operation, "domain") == Some("blockchain") - && runtime_operation_string(operation, "method") == Some("blockchainNode") + && runtime_operation_string(operation, "domain") == Some(operation_domain) + && runtime_operation_string(operation, "method") == Some(operation_method) && runtime_operation_configuration_generation(operation) == configuration_generation } @@ -762,7 +782,7 @@ fn runtime_operation_failure_detail(operation: &Value, status: &str) -> String { .iter() .find_map(|key| runtime_operation_string(operation, key)) .map_or_else( - || format!("blockchain operation ended with status `{status}`"), + || format!("runtime operation ended with status `{status}`"), str::to_owned, ) } @@ -1015,6 +1035,8 @@ mod tests { observation, Some(generation), &runtime_blockchain_operation(operation_id, "running", generation, Value::Null, ""), + "blockchain", + "blockchainNode", ) } @@ -1220,6 +1242,43 @@ mod tests { Ok(()) } + #[test] + fn local_nodes_runtime_operation_admission_matches_declared_identity() -> Result<()> { + let registry = CapabilityRegistry::default(); + let operation_id = "channel-indexer-stop"; + let observation = + registry.begin_runtime_observation("channelIndexerAction", &json!([]), None)?; + registry.track_runtime_operation( + observation, + None, + &json!({ + "operationId": operation_id, + "domain": "localNodes", + "method": "channelIndexerAction", + "status": "running", + }), + "localNodes", + "channelIndexerAction", + )?; + + registry.complete_runtime_operation(&json!({ + "operationId": operation_id, + "domain": "localNodes", + "method": "channelIndexerAction", + "status": "completed", + "result": { "status": "stopped" }, + }))?; + + let state = registry + .state + .lock() + .map_err(|_| anyhow!("capability evidence lock is poisoned"))?; + if !state.runtime_observations.is_empty() || !state.latest.is_empty() { + bail!("local-node runtime observation was not completed: {state:?}"); + } + Ok(()) + } + #[test] fn late_a_completion_cannot_replace_reselected_a_generation() -> Result<()> { let registry = CapabilityRegistry::default(); @@ -1559,6 +1618,8 @@ mod tests { RuntimeCapabilityObservation { observation, configuration_generation: Some(generation), + operation_domain: "blockchain", + operation_method: "blockchainNode", }, ); } diff --git a/src/inspector/command_surface.rs b/src/inspector/command_surface.rs index 2de009e6..a7bdf87c 100644 --- a/src/inspector/command_surface.rs +++ b/src/inspector/command_surface.rs @@ -213,6 +213,8 @@ impl InspectorCommandSurface { observation, request.configuration_generation(), &value, + request.domain_name(), + request.method_name(), )?; self.capability_registry .complete_runtime_operation(&value)?; @@ -800,6 +802,8 @@ mod tests { "status": "running", "context": { "configurationGeneration": 0 } }), + "blockchain", + "blockchainNode", )?; let reply = surface.call_inspector( diff --git a/src/inspector/commands/operations/request.rs b/src/inspector/commands/operations/request.rs index 8d072d1d..65e94f8e 100644 --- a/src/inspector/commands/operations/request.rs +++ b/src/inspector/commands/operations/request.rs @@ -58,7 +58,7 @@ impl RuntimeOperationRequest { self.definition.method() } - pub(super) fn domain_name(&self) -> &'static str { + pub(crate) fn domain_name(&self) -> &'static str { self.definition.domain().as_str() }