Skip to main content

soma_palette/
state.rs

1use std::sync::{
2    atomic::{AtomicU64, Ordering},
3    Arc,
4};
5
6use soma_application::{ExecutionContext, SomaApplication};
7use soma_domain::{AuthorizationMode, Principal, RequestId, ScopeSet, Surface};
8
9#[cfg(test)]
10#[path = "state_tests.rs"]
11mod tests;
12
13/// Axum `State` for `/v1/palette/*` routes: the shared product application
14/// plus the mounted authorization mode.
15#[derive(Clone)]
16pub struct PaletteState {
17    application: Arc<SomaApplication>,
18    authorization_mode: AuthorizationMode,
19}
20
21impl PaletteState {
22    #[must_use]
23    pub fn new(application: Arc<SomaApplication>, authorization_mode: AuthorizationMode) -> Self {
24        Self {
25            application,
26            authorization_mode,
27        }
28    }
29
30    #[must_use]
31    pub fn application(&self) -> &SomaApplication {
32        self.application.as_ref()
33    }
34
35    /// Build an [`ExecutionContext`] for a Palette request, tagged with
36    /// `Surface::Palette` so downstream provider dispatch applies Palette
37    /// surface policy (see `ToolSpec::exposed_on`).
38    #[must_use]
39    pub fn execution_context(&self, subject: Option<&str>, scopes: &[String]) -> ExecutionContext {
40        ExecutionContext {
41            principal: subject
42                .map(|subject| Principal::new(subject, ScopeSet::new(scopes.iter().cloned()))),
43            authorization_mode: self.authorization_mode,
44            surface: Surface::Palette,
45            trace: None,
46            destructive_confirmation: Default::default(),
47            response_limit: None,
48            request_id: next_request_id(),
49        }
50    }
51}
52
53fn next_request_id() -> RequestId {
54    static REQUEST_SEQUENCE: AtomicU64 = AtomicU64::new(1);
55    let sequence = REQUEST_SEQUENCE.fetch_add(1, Ordering::Relaxed);
56    RequestId::new(format!("palette-{}-{sequence}", std::process::id()))
57        .expect("generated palette request ids are valid")
58}