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 correlation_id: uuid::Uuid::now_v7().to_string(),
62 causation_id: None,
63 trace: TraceContext::default(),
64 },
65 serde_json::to_value(ProviderAdminActionInvokeRequest {
66 action: action.to_owned(),
67 input,
68 })
69 .map_err(|error| AppError::new(ErrorCode::Internal, error.to_string()))?,
70 )?;
71 let outcome = invocation::send(
72 &self.client,
73 &self.config,
74 &self.effects,
75 "admin:act",
76 &invocation,
77 )
78 .await?;
79 let envelope: ProviderActionInvokeResponse =
80 serde_json::from_value(invocation::result(&invocation, outcome)?).map_err(|error| {
81 AppError::new(
82 ErrorCode::ExternalDependency,
83 format!("Provider admin action result violated its contract: {error}"),
84 )
85 })?;
86 Ok(envelope.result)
87 }
88}
89
90fn validate_action_name(action: &str) -> AppResult<()> {
91 let valid = !action.is_empty()
92 && action.chars().all(|character| {
93 character.is_ascii_alphanumeric()
94 || character == '.'
95 || character == '_'
96 || character == '-'
97 });
98 if valid {
99 return Ok(());
100 }
101
102 Err(AppError::new(
103 ErrorCode::Validation,
104 "provider admin action name must be a stable path segment",
105 ))
106}