Skip to main content

termivar_scanner/decision_runner/
registry.rs

1//! Deterministic executor registration and action routing.
2
3use super::{Arc, BTreeMap, DecisionActionExecutor, DecisionExecutionStage, DecisionRunnerError};
4
5/// Deterministic executor lookup used by the decision runner.
6#[derive(Clone, Default)]
7pub struct DecisionExecutorRegistry {
8    executors: BTreeMap<String, Arc<dyn DecisionActionExecutor>>,
9    routes: BTreeMap<(DecisionExecutionStage, String), String>,
10}
11
12impl DecisionExecutorRegistry {
13    /// Creates an empty executor registry.
14    pub fn new() -> Self {
15        Self::default()
16    }
17
18    /// Registers one executor identity.
19    pub fn register(
20        &mut self,
21        executor: Arc<dyn DecisionActionExecutor>,
22    ) -> Result<(), DecisionRunnerError> {
23        let id = non_empty(executor.id(), "executor id")?;
24        if self.executors.contains_key(&id) {
25            return Err(DecisionRunnerError::ExecutorIdentityConflict { executor_id: id });
26        }
27        self.executors.insert(id, executor);
28        Ok(())
29    }
30
31    /// Routes an action to an executor when the command does not name one.
32    ///
33    /// Active probes and explicitly host-owned low-level commands may carry
34    /// only an action ID. Separate stage routes allow the explicit probe to use
35    /// a stricter executor than the original passive action; high-level
36    /// adaptive and retry commands pin the planner-authorized executor.
37    pub fn route_action(
38        &mut self,
39        stage: DecisionExecutionStage,
40        action_id: impl Into<String>,
41        executor_id: impl Into<String>,
42    ) -> Result<(), DecisionRunnerError> {
43        let action_id = non_empty(action_id, "action id")?;
44        let executor_id = non_empty(executor_id, "executor id")?;
45        if !self.executors.contains_key(&executor_id) {
46            return Err(DecisionRunnerError::UnknownExecutor { executor_id });
47        }
48
49        let key = (stage, action_id.clone());
50        if let Some(existing) = self.routes.get(&key) {
51            return if existing == &executor_id {
52                Ok(())
53            } else {
54                Err(DecisionRunnerError::ActionRouteConflict { stage, action_id })
55            };
56        }
57        self.routes.insert(key, executor_id);
58        Ok(())
59    }
60
61    /// Returns whether an executor identity is registered.
62    pub fn contains(&self, executor_id: &str) -> bool {
63        self.executors.contains_key(executor_id)
64    }
65
66    /// Returns the number of registered executors.
67    pub fn len(&self) -> usize {
68        self.executors.len()
69    }
70
71    /// Returns whether the registry contains no executors.
72    pub fn is_empty(&self) -> bool {
73        self.executors.is_empty()
74    }
75
76    pub(super) fn resolve(
77        &self,
78        stage: DecisionExecutionStage,
79        action_id: &str,
80        requested_executor: Option<&str>,
81    ) -> Result<(String, Arc<dyn DecisionActionExecutor>), DecisionRunnerError> {
82        let executor_id = if let Some(requested) = requested_executor {
83            non_empty(requested, "executor id")?
84        } else {
85            self.routes
86                .get(&(stage, action_id.to_owned()))
87                .cloned()
88                .ok_or_else(|| DecisionRunnerError::MissingActionRoute {
89                    stage,
90                    action_id: action_id.to_owned(),
91                })?
92        };
93        let executor = self.executors.get(&executor_id).cloned().ok_or_else(|| {
94            DecisionRunnerError::UnknownExecutor {
95                executor_id: executor_id.clone(),
96            }
97        })?;
98        Ok((executor_id, executor))
99    }
100}
101
102fn non_empty(value: impl Into<String>, field: &'static str) -> Result<String, DecisionRunnerError> {
103    let value = value.into();
104    if value.trim().is_empty() {
105        return Err(DecisionRunnerError::EmptyValue { field });
106    }
107    Ok(value)
108}