platform_provider/
admin_action.rs1use crate::ProviderHostEffectCoordinator;
2use crate::config::ProviderConfig;
3use crate::invocation::{self, InvocationContext};
4use crate::protocol::{
5 ProviderActionInvokeResponse, ProviderAdminActionInvokeRequest, ProviderInvocationMode,
6 ProviderOperationKind,
7};
8use platform_core::{ActorContext, AppError, AppResult, ErrorCode, TraceContext};
9use platform_module::AdminActionSource;
10use serde_json::Value;
11use std::time::Duration;
12
13#[derive(Debug, Clone)]
14pub struct ProviderAdminActionSource {
15 client: reqwest::Client,
16 config: ProviderConfig,
17 effects: ProviderHostEffectCoordinator,
18}
19
20impl ProviderAdminActionSource {
21 pub fn new(config: ProviderConfig) -> AppResult<Self> {
22 let client = reqwest::Client::builder()
23 .timeout(Duration::from_millis(config.timeout_ms))
24 .build()
25 .map_err(|error| {
26 AppError::new(
27 ErrorCode::Internal,
28 format!("failed to build Provider Service client: {error}"),
29 )
30 })?;
31 Ok(Self {
32 client,
33 config,
34 effects: ProviderHostEffectCoordinator::rejecting(),
35 })
36 }
37
38 #[must_use]
39 pub fn with_effect_coordinator(mut self, effects: ProviderHostEffectCoordinator) -> Self {
40 self.effects = effects;
41 self
42 }
43}
44
45#[async_trait::async_trait]
46impl AdminActionSource for ProviderAdminActionSource {
47 async fn invoke(&self, action: &str, input: Value) -> AppResult<Value> {
48 validate_action_name(action)?;
49 let invocation_id = uuid::Uuid::now_v7().to_string();
50 let invocation = invocation::build(
51 &self.config,
52 ProviderOperationKind::AdminAction,
53 action,
54 "1",
55 ProviderInvocationMode::Durable,
56 InvocationContext {
57 request_id: invocation_id.clone(),
58 invocation_id,
59 attempt: 1,
60 actor: ActorContext::System,
61 tenant_id: None,
62 correlation_id: uuid::Uuid::now_v7().to_string(),
63 causation_id: None,
64 trace: TraceContext::default(),
65 },
66 serde_json::to_value(ProviderAdminActionInvokeRequest {
67 action: action.to_owned(),
68 input,
69 })
70 .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
71 )?;
72 let outcome = invocation::send(
73 &self.client,
74 &self.config,
75 &self.effects,
76 "admin:act",
77 &invocation,
78 )
79 .await?;
80 let envelope: ProviderActionInvokeResponse =
81 serde_json::from_value(invocation::result(&invocation, outcome)?).map_err(|error| {
82 AppError::new(
83 ErrorCode::ExternalDependency,
84 format!("Provider admin action result violated its contract: {error}"),
85 )
86 })?;
87 Ok(envelope.result)
88 }
89}
90
91fn validate_action_name(action: &str) -> AppResult<()> {
92 let valid = !action.is_empty()
93 && action.chars().all(|character| {
94 character.is_ascii_alphanumeric()
95 || character == '.'
96 || character == '_'
97 || character == '-'
98 });
99 if valid {
100 return Ok(());
101 }
102
103 Err(AppError::new(
104 ErrorCode::Validation,
105 "provider admin action name must be a stable path segment",
106 ))
107}