diff --git a/crates/targets/src/control_plane.rs b/crates/targets/src/control_plane.rs index 2ba8639d4..226b4ab47 100644 --- a/crates/targets/src/control_plane.rs +++ b/crates/targets/src/control_plane.rs @@ -13,10 +13,13 @@ // limitations under the License. use crate::manifest::{ - TargetPluginDistributionManifest, TargetPluginExternalRuntimeContract, TargetPluginManifest, TargetPluginRuntimeTransport, + TargetPluginDistributionManifest, TargetPluginExternalRuntimeContract, TargetPluginManifest, TargetPluginMarketplaceManifest, + TargetPluginPackaging, TargetPluginRuntimeTransport, }; +use crate::runtime::sidecar::{SidecarRuntimePolicy, SidecarRuntimeSafetyChecks}; use crate::runtime::sidecar_protocol::SIDECAR_RUNTIME_PROTOCOL_VERSION; use serde::{Deserialize, Serialize}; +use thiserror::Error; use url::Url; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -163,6 +166,159 @@ pub fn runtime_state_from_status_label(status: &str) -> TargetPluginRuntimeState } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TargetPluginExternalAction { + Install, + Enable, + Disable, + Rollback, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct TargetPluginExternalActionDecision { + pub action: TargetPluginExternalAction, + pub plugin_id: String, + pub installation: TargetPluginInstallation, + pub operational_state: TargetPluginOperationalState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TargetPluginExternalFlowGate { + pub enabled: bool, + pub install_policy: TargetPluginInstallPolicy, + pub runtime_policy: SidecarRuntimePolicy, + pub runtime_safety_checks: SidecarRuntimeSafetyChecks, + pub circuit_breaker_closed: bool, +} + +impl Default for TargetPluginExternalFlowGate { + fn default() -> Self { + Self { + enabled: false, + install_policy: TargetPluginInstallPolicy::default(), + runtime_policy: SidecarRuntimePolicy::default(), + runtime_safety_checks: SidecarRuntimeSafetyChecks { + sandboxed: false, + provenance_verified: false, + queue_depth: 0, + }, + circuit_breaker_closed: false, + } + } +} + +impl TargetPluginExternalFlowGate { + pub fn verified(runtime_policy: SidecarRuntimePolicy, runtime_safety_checks: SidecarRuntimeSafetyChecks) -> Self { + Self { + enabled: true, + install_policy: TargetPluginInstallPolicy::default(), + runtime_policy, + runtime_safety_checks, + circuit_breaker_closed: true, + } + } + + pub fn status(&self) -> TargetPluginExternalFlowGateStatus { + TargetPluginExternalFlowGateStatus { + enabled: self.enabled, + install_requires_signature: self.install_policy.require_signature, + install_requires_provenance: self.install_policy.require_provenance, + runtime_allows_external_sidecars: self.runtime_policy.allow_external_sidecars, + runtime_requires_sandbox: self.runtime_policy.require_sandbox, + runtime_requires_provenance: self.runtime_policy.require_provenance, + circuit_breaker_closed: self.circuit_breaker_closed, + max_queue_depth: self.runtime_policy.max_queue_depth, + failure_threshold: self.runtime_policy.failure_threshold(), + redacts_error_details: self.runtime_policy.redact_error_details, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct TargetPluginExternalFlowGateStatus { + pub enabled: bool, + pub install_requires_signature: bool, + pub install_requires_provenance: bool, + pub runtime_allows_external_sidecars: bool, + pub runtime_requires_sandbox: bool, + pub runtime_requires_provenance: bool, + pub circuit_breaker_closed: bool, + pub max_queue_depth: usize, + pub failure_threshold: usize, + pub redacts_error_details: bool, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum TargetPluginExternalActionError { + #[error("external plugin flow is disabled")] + ExternalFlowDisabled, + + #[error("plugin {plugin_id} is not an external plugin")] + NotExternalPlugin { plugin_id: String }, + + #[error("plugin {plugin_id} is not installed")] + NotInstalled { plugin_id: String }, + + #[error("plugin {plugin_id} has no previous revision for rollback")] + MissingPreviousRevision { plugin_id: String }, + + #[error("external plugin install policy denied action: {reason}")] + InstallPolicyDenied { reason: String }, + + #[error("external plugin runtime policy denied action: {reason}")] + RuntimePolicyDenied { reason: String }, + + #[error("external plugin circuit breaker is open")] + CircuitBreakerOpen, + + #[error("external plugin {plugin_id} has no installable artifact")] + MissingArtifact { plugin_id: String }, +} + +pub fn plan_external_target_plugin_action( + manifest: &TargetPluginMarketplaceManifest, + action: TargetPluginExternalAction, + installation: &TargetPluginInstallation, + gate: &TargetPluginExternalFlowGate, +) -> Result { + validate_external_action_subject(manifest)?; + validate_external_action_gate(gate)?; + + match action { + TargetPluginExternalAction::Install => plan_external_install(manifest, action, gate), + TargetPluginExternalAction::Enable => { + require_installed(manifest.plugin_id, installation)?; + Ok(TargetPluginExternalActionDecision { + action, + plugin_id: manifest.plugin_id.to_string(), + installation: installation.clone(), + operational_state: TargetPluginOperationalState { + install_state: TargetPluginInstallState::Installed, + enable_state: TargetPluginEnableState::Enabled, + runtime_state: TargetPluginRuntimeState::Running, + }, + }) + } + TargetPluginExternalAction::Disable => { + require_installed(manifest.plugin_id, installation)?; + Ok(TargetPluginExternalActionDecision { + action, + plugin_id: manifest.plugin_id.to_string(), + installation: installation.clone(), + operational_state: TargetPluginOperationalState { + install_state: TargetPluginInstallState::Installed, + enable_state: TargetPluginEnableState::Disabled, + runtime_state: TargetPluginRuntimeState::Offline, + }, + }) + } + TargetPluginExternalAction::Rollback => plan_external_rollback(manifest, action, installation), + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TargetPluginInstallPolicy { pub allowed_providers: Vec, @@ -245,6 +401,111 @@ pub fn validate_external_plugin_installation( Ok(()) } +fn validate_external_action_subject(manifest: &TargetPluginMarketplaceManifest) -> Result<(), TargetPluginExternalActionError> { + if manifest.packaging != TargetPluginPackaging::External { + return Err(TargetPluginExternalActionError::NotExternalPlugin { + plugin_id: manifest.plugin_id.to_string(), + }); + } + + Ok(()) +} + +fn validate_external_action_gate(gate: &TargetPluginExternalFlowGate) -> Result<(), TargetPluginExternalActionError> { + if !gate.enabled { + return Err(TargetPluginExternalActionError::ExternalFlowDisabled); + } + if !gate.circuit_breaker_closed { + return Err(TargetPluginExternalActionError::CircuitBreakerOpen); + } + gate.runtime_policy + .validate_activation(&gate.runtime_safety_checks) + .map_err(|reason| TargetPluginExternalActionError::RuntimePolicyDenied { + reason: reason.to_string(), + }) +} + +fn plan_external_install( + manifest: &TargetPluginMarketplaceManifest, + action: TargetPluginExternalAction, + gate: &TargetPluginExternalFlowGate, +) -> Result { + let install_manifest = TargetPluginManifest { + plugin_id: manifest.plugin_id, + display_name: manifest.display_name, + provider: manifest.provider, + version: manifest.version, + target_type: manifest.target_type, + supported_domains: manifest.supported_domains, + secret_fields: manifest.secret_fields, + }; + validate_external_plugin_installation( + &install_manifest, + &manifest.runtime_contract, + manifest.distribution, + &gate.install_policy, + ) + .map_err(|reason| TargetPluginExternalActionError::InstallPolicyDenied { reason })?; + + let artifact = manifest + .distribution + .and_then(|distribution| distribution.artifacts.first()) + .ok_or_else(|| TargetPluginExternalActionError::MissingArtifact { + plugin_id: manifest.plugin_id.to_string(), + })?; + + Ok(TargetPluginExternalActionDecision { + action, + plugin_id: manifest.plugin_id.to_string(), + installation: external_target_plugin_installation(manifest.version, artifact.digest_sha256, artifact.artifact_id, None), + operational_state: TargetPluginOperationalState { + install_state: TargetPluginInstallState::Installed, + enable_state: TargetPluginEnableState::Disabled, + runtime_state: TargetPluginRuntimeState::Offline, + }, + }) +} + +fn require_installed(plugin_id: &str, installation: &TargetPluginInstallation) -> Result<(), TargetPluginExternalActionError> { + if installation.install_state != TargetPluginInstallState::Installed || installation.current_revision.is_none() { + return Err(TargetPluginExternalActionError::NotInstalled { + plugin_id: plugin_id.to_string(), + }); + } + + Ok(()) +} + +fn plan_external_rollback( + manifest: &TargetPluginMarketplaceManifest, + action: TargetPluginExternalAction, + installation: &TargetPluginInstallation, +) -> Result { + require_installed(manifest.plugin_id, installation)?; + + let Some(current) = installation.current_revision.clone() else { + return Err(TargetPluginExternalActionError::NotInstalled { + plugin_id: manifest.plugin_id.to_string(), + }); + }; + let Some(previous) = installation.previous_revision.clone() else { + return Err(TargetPluginExternalActionError::MissingPreviousRevision { + plugin_id: manifest.plugin_id.to_string(), + }); + }; + + Ok(TargetPluginExternalActionDecision { + action, + plugin_id: manifest.plugin_id.to_string(), + installation: rollback_target_plugin_installation(current, previous), + operational_state: TargetPluginOperationalState { + install_state: TargetPluginInstallState::Installed, + enable_state: TargetPluginEnableState::Disabled, + runtime_state: TargetPluginRuntimeState::Offline, + }, + }) +} + fn validate_artifact_uri(label: &str, artifact_id: &str, uri: &str, policy: &TargetPluginInstallPolicy) -> Result<(), String> { if uri.is_empty() { return Ok(()); @@ -267,15 +528,19 @@ fn validate_artifact_uri(label: &str, artifact_id: &str, uri: &str, policy: &Tar #[cfg(test)] mod tests { use super::{ - TargetPluginEnableState, TargetPluginInstallPolicy, TargetPluginInstallState, TargetPluginRevision, + TargetPluginEnableState, TargetPluginExternalAction, TargetPluginExternalActionError, TargetPluginExternalFlowGate, + TargetPluginInstallPolicy, TargetPluginInstallState, TargetPluginInstallation, TargetPluginRevision, TargetPluginRuntimeState, builtin_target_plugin_installation, builtin_target_plugin_operational_state, - external_target_plugin_installation, failed_external_target_plugin_installation, rollback_target_plugin_installation, - runtime_state_from_status_label, validate_external_plugin_installation, + external_target_plugin_installation, failed_external_target_plugin_installation, plan_external_target_plugin_action, + rollback_target_plugin_installation, runtime_state_from_status_label, validate_external_plugin_installation, }; + use crate::catalog::example_external_webhook_plugin; use crate::manifest::{ TargetPluginArtifactManifest, TargetPluginDistributionManifest, TargetPluginExternalRuntimeContract, - TargetPluginManifest, TargetPluginRuntimeTransport, builtin_target_manifest, + TargetPluginManifest, TargetPluginRuntimeTransport, builtin_target_manifest, builtin_target_marketplace_manifest, }; + use crate::{SidecarRuntimePolicy, SidecarRuntimeSafetyChecks}; + use std::time::Duration; #[test] fn builtin_installation_maps_to_virtual_installed_revision() { @@ -376,6 +641,209 @@ mod tests { assert_eq!(installation.validation_error.as_deref(), Some("digest mismatch during install")); } + #[test] + fn external_action_gate_is_disabled_by_default() { + let example = example_external_webhook_plugin(); + let gate = TargetPluginExternalFlowGate::default(); + + let result = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Install, + &TargetPluginInstallation { + install_state: TargetPluginInstallState::NotInstalled, + current_revision: None, + previous_revision: None, + validation_error: None, + }, + &gate, + ); + + assert_eq!(result, Err(TargetPluginExternalActionError::ExternalFlowDisabled)); + assert!(!gate.status().enabled); + assert!(gate.status().install_requires_signature); + assert!(gate.status().install_requires_provenance); + } + + #[test] + fn external_action_rejects_builtin_manifest() { + let gate = TargetPluginExternalFlowGate::verified( + SidecarRuntimePolicy::verified_external(16, Duration::from_secs(5), 3), + SidecarRuntimeSafetyChecks::verified(0), + ); + + let result = plan_external_target_plugin_action( + &builtin_target_marketplace_manifest("webhook"), + TargetPluginExternalAction::Install, + &TargetPluginInstallation { + install_state: TargetPluginInstallState::NotInstalled, + current_revision: None, + previous_revision: None, + validation_error: None, + }, + &gate, + ); + + assert_eq!( + result, + Err(TargetPluginExternalActionError::NotExternalPlugin { + plugin_id: "builtin:webhook".to_string() + }) + ); + } + + #[test] + fn external_action_install_requires_signature_and_provenance() { + const MISSING_PROVENANCE_ARTIFACTS: &[TargetPluginArtifactManifest] = &[TargetPluginArtifactManifest { + artifact_id: "sidecar-linux-amd64", + target_triple: "x86_64-unknown-linux-gnu", + download_uri: "https://plugins.example.test/webhook-sidecar.tar.zst", + digest_sha256: "0123456789abcdef0123456789abcdef", + signature_uri: "https://plugins.example.test/webhook-sidecar.tar.zst.sig", + provenance_uri: "", + size_bytes: 8192, + }]; + + let mut example = example_external_webhook_plugin(); + example.manifest.distribution = Some(TargetPluginDistributionManifest { + artifacts: MISSING_PROVENANCE_ARTIFACTS, + }); + let gate = TargetPluginExternalFlowGate::verified( + SidecarRuntimePolicy::verified_external(16, Duration::from_secs(5), 3), + SidecarRuntimeSafetyChecks::verified(0), + ); + + let result = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Install, + &TargetPluginInstallation { + install_state: TargetPluginInstallState::NotInstalled, + current_revision: None, + previous_revision: None, + validation_error: None, + }, + &gate, + ); + + assert_eq!( + result, + Err(TargetPluginExternalActionError::InstallPolicyDenied { + reason: "artifact sidecar-linux-amd64 must declare a provenance uri".to_string() + }) + ); + } + + #[test] + fn external_action_enable_requires_sandbox_and_provenance() { + let example = example_external_webhook_plugin(); + let gate = TargetPluginExternalFlowGate::verified( + SidecarRuntimePolicy::verified_external(16, Duration::from_secs(5), 3), + SidecarRuntimeSafetyChecks { + sandboxed: false, + provenance_verified: true, + queue_depth: 0, + }, + ); + + let result = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Enable, + &example.installation, + &gate, + ); + + assert_eq!( + result, + Err(TargetPluginExternalActionError::RuntimePolicyDenied { + reason: "sidecar runtime requires sandbox isolation".to_string() + }) + ); + } + + #[test] + fn external_action_enable_requires_closed_circuit_breaker() { + let example = example_external_webhook_plugin(); + let mut gate = TargetPluginExternalFlowGate::verified( + SidecarRuntimePolicy::verified_external(16, Duration::from_secs(5), 3), + SidecarRuntimeSafetyChecks::verified(0), + ); + gate.circuit_breaker_closed = false; + + let result = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Enable, + &example.installation, + &gate, + ); + + assert_eq!(result, Err(TargetPluginExternalActionError::CircuitBreakerOpen)); + } + + #[test] + fn external_actions_plan_install_disable_and_rollback_without_execution() { + let example = example_external_webhook_plugin(); + let gate = TargetPluginExternalFlowGate::verified( + SidecarRuntimePolicy::verified_external(16, Duration::from_secs(5), 3), + SidecarRuntimeSafetyChecks::verified(0), + ); + + let install = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Install, + &TargetPluginInstallation { + install_state: TargetPluginInstallState::NotInstalled, + current_revision: None, + previous_revision: None, + validation_error: None, + }, + &gate, + ) + .expect("verified external install action should plan"); + assert_eq!(install.installation.install_state, TargetPluginInstallState::Installed); + assert_eq!(install.operational_state.enable_state, TargetPluginEnableState::Disabled); + assert_eq!(install.operational_state.runtime_state, TargetPluginRuntimeState::Offline); + + let disable = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Disable, + &example.installation, + &gate, + ) + .expect("verified external disable action should plan"); + assert_eq!(disable.operational_state.enable_state, TargetPluginEnableState::Disabled); + assert_eq!(disable.operational_state.runtime_state, TargetPluginRuntimeState::Offline); + + let current = TargetPluginRevision { + version: "2.0.0".to_string(), + digest_sha256: Some("new-digest".to_string()), + source: "external".to_string(), + installed_at: Some("2026-05-13T12:05:00Z".to_string()), + artifact_id: Some("sidecar-linux-amd64-v2".to_string()), + }; + let previous = TargetPluginRevision { + version: "1.9.0".to_string(), + digest_sha256: Some("old-digest".to_string()), + source: "external".to_string(), + installed_at: Some("2026-05-13T11:55:00Z".to_string()), + artifact_id: Some("sidecar-linux-amd64-v1".to_string()), + }; + let rollback = plan_external_target_plugin_action( + &example.manifest, + TargetPluginExternalAction::Rollback, + &TargetPluginInstallation { + install_state: TargetPluginInstallState::Installed, + current_revision: Some(current.clone()), + previous_revision: Some(previous.clone()), + validation_error: None, + }, + &gate, + ) + .expect("verified external rollback action should plan"); + + assert_eq!(rollback.installation.current_revision, Some(previous)); + assert_eq!(rollback.installation.previous_revision, Some(current)); + assert_eq!(rollback.operational_state.enable_state, TargetPluginEnableState::Disabled); + } + #[test] fn validate_external_installation_accepts_allowed_https_artifact() { let manifest = TargetPluginManifest { diff --git a/crates/targets/src/lib.rs b/crates/targets/src/lib.rs index b9a646c77..1a7a77521 100644 --- a/crates/targets/src/lib.rs +++ b/crates/targets/src/lib.rs @@ -44,9 +44,11 @@ pub use config::{ normalize_legacy_target_instances_from_env, normalize_target_plugin_instances, normalize_target_plugin_instances_from_env, }; pub use control_plane::{ - TargetPluginEnableState, TargetPluginInstallState, TargetPluginInstallation, TargetPluginOperationalState, - TargetPluginRevision, TargetPluginRuntimeState, builtin_target_plugin_installation, builtin_target_plugin_operational_state, - external_target_plugin_installation, rollback_target_plugin_installation, runtime_state_from_status_label, + TargetPluginEnableState, TargetPluginExternalAction, TargetPluginExternalActionDecision, TargetPluginExternalActionError, + TargetPluginExternalFlowGate, TargetPluginExternalFlowGateStatus, TargetPluginInstallState, TargetPluginInstallation, + TargetPluginOperationalState, TargetPluginRevision, TargetPluginRuntimeState, builtin_target_plugin_installation, + builtin_target_plugin_operational_state, external_target_plugin_installation, plan_external_target_plugin_action, + rollback_target_plugin_installation, runtime_state_from_status_label, }; pub use domain::TargetDomain; pub use error::{StoreError, TargetError}; @@ -70,7 +72,7 @@ pub use runtime::{ OpsDiagnosticsRegistryError, }, s3_hooks::{S3HookContext, S3HookDecision, S3HookRegistration, S3HookRegistry, S3HookRegistryError}, - sidecar::{SidecarPluginRuntime, SidecarRuntimePolicy, SidecarRuntimeSafetyChecks}, + sidecar::{SidecarPluginRuntime, SidecarRuntimePolicy, SidecarRuntimePolicyError, SidecarRuntimeSafetyChecks}, sidecar_protocol::{SIDECAR_RUNTIME_PROTOCOL_VERSION, SidecarHandshake, SidecarPluginCapability}, start_replay_worker, }; diff --git a/crates/targets/src/runtime/sidecar.rs b/crates/targets/src/runtime/sidecar.rs index 3a99e8605..c347be21b 100644 --- a/crates/targets/src/runtime/sidecar.rs +++ b/crates/targets/src/runtime/sidecar.rs @@ -16,6 +16,7 @@ use crate::TargetDomain; use crate::runtime::sidecar_protocol::SidecarHandshake; use serde::{Deserialize, Serialize}; use std::time::Duration; +use thiserror::Error; const DEFAULT_FAILURE_THRESHOLD: usize = 3; @@ -60,6 +61,10 @@ impl SidecarRuntimePolicy { pub fn failure_threshold(&self) -> usize { self.failure_threshold.max(1) } + + pub fn validate_activation(&self, safety_checks: &SidecarRuntimeSafetyChecks) -> Result<(), SidecarRuntimePolicyError> { + validate_runtime_policy(self, safety_checks) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -79,6 +84,21 @@ impl SidecarRuntimeSafetyChecks { } } +#[derive(Debug, Error, PartialEq, Eq)] +pub enum SidecarRuntimePolicyError { + #[error("external sidecar runtime is disabled by policy")] + ExternalSidecarDisabled, + + #[error("sidecar runtime requires sandbox isolation")] + SandboxRequired, + + #[error("sidecar runtime requires verified provenance")] + ProvenanceRequired, + + #[error("sidecar runtime queue depth {queue_depth} exceeds policy bound {max_queue_depth}")] + QueueDepthExceeded { queue_depth: usize, max_queue_depth: usize }, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct SidecarPluginRuntime { @@ -132,7 +152,7 @@ impl SidecarPluginRuntime { self.handshake.plugin_id, required_domain )); } - validate_runtime_policy(policy, safety_checks)?; + policy.validate_activation(safety_checks).map_err(|err| err.to_string())?; self.healthy = true; self.degraded_to_builtin = false; @@ -188,21 +208,24 @@ impl SidecarPluginRuntime { } } -fn validate_runtime_policy(policy: &SidecarRuntimePolicy, safety_checks: &SidecarRuntimeSafetyChecks) -> Result<(), String> { +fn validate_runtime_policy( + policy: &SidecarRuntimePolicy, + safety_checks: &SidecarRuntimeSafetyChecks, +) -> Result<(), SidecarRuntimePolicyError> { if !policy.allow_external_sidecars { - return Err("external sidecar runtime is disabled by policy".to_string()); + return Err(SidecarRuntimePolicyError::ExternalSidecarDisabled); } if policy.require_sandbox && !safety_checks.sandboxed { - return Err("sidecar runtime requires sandbox isolation".to_string()); + return Err(SidecarRuntimePolicyError::SandboxRequired); } if policy.require_provenance && !safety_checks.provenance_verified { - return Err("sidecar runtime requires verified provenance".to_string()); + return Err(SidecarRuntimePolicyError::ProvenanceRequired); } if safety_checks.queue_depth > policy.max_queue_depth { - return Err(format!( - "sidecar runtime queue depth {} exceeds policy bound {}", - safety_checks.queue_depth, policy.max_queue_depth - )); + return Err(SidecarRuntimePolicyError::QueueDepthExceeded { + queue_depth: safety_checks.queue_depth, + max_queue_depth: policy.max_queue_depth, + }); } Ok(()) } diff --git a/rustfs/src/admin/handlers/extensions.rs b/rustfs/src/admin/handlers/extensions.rs index 0cb932d57..c42725cc5 100644 --- a/rustfs/src/admin/handlers/extensions.rs +++ b/rustfs/src/admin/handlers/extensions.rs @@ -28,7 +28,10 @@ use hyper::Method; use matchit::Params; use rustfs_extension_schema::{ExtensionKind, ExtensionSchema}; use rustfs_policy::policy::action::{Action, AdminAction}; -use rustfs_targets::{builtin_extension_schemas, catalog::example_external_webhook_plugin, target_marketplace_extension_schema}; +use rustfs_targets::{ + TargetPluginExternalFlowGate, TargetPluginExternalFlowGateStatus, builtin_extension_schemas, + catalog::example_external_webhook_plugin, target_marketplace_extension_schema, +}; use s3s::header::CONTENT_TYPE; use s3s::{Body, S3Request, S3Response, S3Result, s3_error}; use serde::Serialize; @@ -52,6 +55,7 @@ pub fn register_extension_route(r: &mut S3Router) -> std::io::Re #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub(crate) struct ExtensionCatalogResponse { pub extensions: Vec, + pub external_plugin_flow: TargetPluginExternalFlowGateStatus, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -89,7 +93,10 @@ fn build_extension_catalog_response() -> ExtensionCatalogResponse { extensions.push(target_marketplace_extension_schema(&example.manifest)); extensions.sort_by(|a, b| a.extension_id.cmp(&b.extension_id)); - ExtensionCatalogResponse { extensions } + ExtensionCatalogResponse { + extensions, + external_plugin_flow: TargetPluginExternalFlowGate::default().status(), + } } fn map_extension_instance(instance: PluginInstanceEntry) -> ExtensionInstanceEntry { @@ -280,6 +287,13 @@ mod tests { assert_eq!(diagnostics.kind, ExtensionKind::OpsDiagnostics); assert_eq!(diagnostics.runtime.boundary, ExtensionRuntimeBoundary::Builtin); + assert!(!response.external_plugin_flow.enabled); + assert!(response.external_plugin_flow.install_requires_signature); + assert!(response.external_plugin_flow.install_requires_provenance); + assert!(response.external_plugin_flow.runtime_requires_sandbox); + assert!(response.external_plugin_flow.runtime_requires_provenance); + assert!(!response.external_plugin_flow.circuit_breaker_closed); + assert!(validate_extension_schemas(&response.extensions).is_ok()); }