diff --git a/Cargo.lock b/Cargo.lock index 4be7e801e..dfbd8cdf8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11651,27 +11651,36 @@ dependencies = [ name = "temps-external-plugins" version = "0.1.0-beta.56" dependencies = [ + "anyhow", "async-trait", "axum", + "base64 0.23.1", "chrono", + "ed25519-dalek", "futures", + "hex", "http-body-util", "hyper", "hyper-util", "libc", + "reqwest 0.12.28", "sea-orm", "serde", "serde_json", + "sha2 0.11.0", "tempfile", "temps-auth", "temps-config", "temps-core", "temps-entities", "temps-presets", + "thiserror 2.0.20", "tokio", "tokio-tungstenite 0.30.0", "tower", "tracing", + "tracing-subscriber", + "url", "utoipa", "uuid", ] diff --git a/crates/temps-cli/src/commands/serve/console.rs b/crates/temps-cli/src/commands/serve/console.rs index 1b3995363..fa515b006 100644 --- a/crates/temps-cli/src/commands/serve/console.rs +++ b/crates/temps-cli/src/commands/serve/console.rs @@ -1257,6 +1257,9 @@ pub struct ConsoleApiParams { /// connection handling. The watcher's writes reach the route table through /// the `route_table_changes` NOTIFY path, not through this handle. pub traefik_discovery: Arc, + /// Authenticated external-plugin registry configuration resolved from the + /// paired `temps serve` bootstrap options. + pub external_plugin_registry: temps_external_plugins::catalog::RegistryConfig, } /// Build a ClickHouse-backed metrics store from the server config, or `None` @@ -2179,6 +2182,7 @@ pub async fn start_console_api(params: ConsoleApiParams) -> anyhow::Result<()> { update_status, self_updater, traefik_discovery, + external_plugin_registry, } = params; // Count panics for the anonymous `error_summary` telemetry event. Only @@ -2666,7 +2670,8 @@ pub async fn start_console_api(params: ConsoleApiParams) -> anyhow::Result<()> { // through the proxy, so a plugin that has to hand out a URL to something // outside the request (a sandboxed agent, a webhook receiver) cannot // construct one without being told the address the proxy listens on. - .with_proxy_address(&config.address); + .with_proxy_address(&config.address) + .with_registry(external_plugin_registry); let external_plugins_plugin = Box::new(temps_external_plugins::ExternalPluginsPlugin::new( external_plugin_config, )); diff --git a/crates/temps-cli/src/commands/serve/mod.rs b/crates/temps-cli/src/commands/serve/mod.rs index 0eabbaaae..c20eb4ee1 100644 --- a/crates/temps-cli/src/commands/serve/mod.rs +++ b/crates/temps-cli/src/commands/serve/mod.rs @@ -133,6 +133,16 @@ pub struct ServeCommand { /// sibling proxy has a fixed address to forward console traffic to. #[arg(long, value_enum, default_value_t = ServeRole::All, env = "TEMPS_ROLE")] pub role: ServeRole, + + /// Key ID expected on signed external-plugin registry documents. + /// Must be configured together with --plugin-registry-public-key. + #[arg(long, env = "TEMPS_PLUGIN_REGISTRY_KEY_ID")] + pub plugin_registry_key_id: Option, + + /// Hex-encoded 32-byte Ed25519 public key used to authenticate the + /// external-plugin registry. Must be paired with --plugin-registry-key-id. + #[arg(long, env = "TEMPS_PLUGIN_REGISTRY_PUBLIC_KEY")] + pub plugin_registry_public_key: Option, } impl ServeCommand { @@ -207,6 +217,12 @@ impl ServeCommand { ); } + let external_plugin_registry = + temps_external_plugins::catalog::registry_config_from_anchor( + self.plugin_registry_key_id.as_deref(), + self.plugin_registry_public_key.as_deref(), + )?; + let serve_config = Arc::new(temps_config::ServerConfig::new( self.address.clone(), self.database_url.clone(), @@ -738,6 +754,7 @@ impl ServeCommand { update_status, self_updater, traefik_discovery: traefik_discovery_handle, + external_plugin_registry, }; if self.role == ServeRole::Console { diff --git a/crates/temps-core/src/sensitive_action.rs b/crates/temps-core/src/sensitive_action.rs index 3d4a2768e..29e6c1f83 100644 --- a/crates/temps-core/src/sensitive_action.rs +++ b/crates/temps-core/src/sensitive_action.rs @@ -16,6 +16,9 @@ use thiserror::Error; #[derive(Debug, Clone, PartialEq, Eq)] pub enum SensitiveAction { CreateApiKey, + InstallExternalPlugin { + name: String, + }, RotateApiKey { api_key_id: i32, }, @@ -86,6 +89,7 @@ impl SensitiveAction { pub fn as_str(&self) -> &'static str { match self { Self::CreateApiKey => "create_api_key", + Self::InstallExternalPlugin { .. } => "install_external_plugin", Self::RotateApiKey { .. } => "rotate_api_key", Self::DeleteEnvironment { .. } => "delete_environment", Self::DrainNode { .. } => "drain_node", @@ -172,6 +176,13 @@ mod tests { #[test] fn action_identifiers_are_stable_and_resource_independent() { assert_eq!(SensitiveAction::CreateApiKey.as_str(), "create_api_key"); + assert_eq!( + SensitiveAction::InstallExternalPlugin { + name: "example".to_string(), + } + .as_str(), + "install_external_plugin" + ); assert_eq!( SensitiveAction::RotateClusterCa.as_str(), "rotate_cluster_ca" diff --git a/crates/temps-external-plugins/Cargo.toml b/crates/temps-external-plugins/Cargo.toml index 78109167e..42a689aea 100644 --- a/crates/temps-external-plugins/Cargo.toml +++ b/crates/temps-external-plugins/Cargo.toml @@ -18,6 +18,14 @@ temps-entities = { path = "../temps-entities" } temps-presets = { path = "../temps-presets" } sea-orm = { workspace = true } async-trait = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +ed25519-dalek = "2.2.0" +hex = { workspace = true } +reqwest = { workspace = true } +sha2 = { workspace = true } +thiserror = { workspace = true } +url = { workspace = true } axum = { workspace = true } chrono = { workspace = true } futures = { workspace = true } @@ -38,3 +46,4 @@ libc = { workspace = true } [dev-dependencies] tempfile = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/temps-external-plugins/src/catalog.rs b/crates/temps-external-plugins/src/catalog.rs new file mode 100644 index 000000000..d1e7134fc --- /dev/null +++ b/crates/temps-external-plugins/src/catalog.rs @@ -0,0 +1,670 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Authenticated external-plugin registry documents. + +use std::collections::{BTreeMap, BTreeSet}; +use std::time::Duration; + +use base64::Engine as _; +use chrono::{DateTime, Duration as ChronoDuration, Utc}; +use ed25519_dalek::{Signature, Verifier as _, VerifyingKey}; +use futures::StreamExt as _; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use url::Url; +use utoipa::ToSchema; + +pub const REGISTRY_URL: &str = "https://registry.temps.sh/api/plugins"; +pub const MAX_REGISTRY_BYTES: u64 = 1024 * 1024; +const REGISTRY_TIMEOUT: Duration = Duration::from_secs(10); +const MAX_REGISTRY_VALIDITY: ChronoDuration = ChronoDuration::days(30); + +#[derive(Debug, Clone)] +pub struct RegistryConfig { + pub url: String, + pub trust_anchors: BTreeMap, + pub allowed_artifact_hosts: BTreeSet, + pub allow_http: bool, +} + +impl Default for RegistryConfig { + fn default() -> Self { + Self { + url: REGISTRY_URL.to_string(), + // Deliberately empty until the registry owner publishes the + // production Ed25519 public key. An absent key is an error, never + // an invitation to accept an unsigned document. + trust_anchors: BTreeMap::new(), + allowed_artifact_hosts: BTreeSet::from(["registry.temps.sh".to_string()]), + allow_http: false, + } + } +} + +impl RegistryConfig { + pub fn with_trust_anchor(mut self, key_id: impl Into, key: [u8; 32]) -> Self { + self.trust_anchors.insert(key_id.into(), key); + self + } + + pub fn with_artifact_host(mut self, host: impl Into) -> Self { + self.allowed_artifact_hosts.insert(host.into()); + self + } + + #[cfg(test)] + pub(crate) fn local(url: String, key_id: &str, key: [u8; 32]) -> Self { + let host = Url::parse(&url) + .ok() + .and_then(|parsed| parsed.host_str().map(ToOwned::to_owned)) + .unwrap_or_else(|| "127.0.0.1".to_string()); + Self { + url, + trust_anchors: BTreeMap::from([(key_id.to_string(), key)]), + allowed_artifact_hosts: BTreeSet::from([host]), + allow_http: true, + } + } +} + +#[derive(Debug, Error)] +pub enum RegistryTrustConfigError { + #[error("External-plugin registry trust configuration is incomplete: {provided} is set but {missing} is missing")] + Incomplete { + provided: &'static str, + missing: &'static str, + }, + #[error("External-plugin registry key ID must not be empty and may contain only ASCII letters, digits, '.', '_', or '-'")] + InvalidKeyId, + #[error("External-plugin registry public key for key ID '{key_id}' is not valid hexadecimal: {reason}")] + InvalidPublicKeyHex { key_id: String, reason: String }, + #[error("External-plugin registry public key for key ID '{key_id}' decoded to {actual} bytes; Ed25519 public keys must be exactly 32 bytes")] + InvalidPublicKeyLength { key_id: String, actual: usize }, +} + +/// Build the production registry configuration from the paired bootstrap +/// values accepted by `temps serve`. Neither value alone grants any trust; +/// absent values keep the catalogue visible but fail closed on fetch/startup. +pub fn registry_config_from_anchor( + key_id: Option<&str>, + public_key_hex: Option<&str>, +) -> Result { + let (key_id, public_key_hex) = match (key_id, public_key_hex) { + (None, None) => return Ok(RegistryConfig::default()), + (Some(_), None) => { + return Err(RegistryTrustConfigError::Incomplete { + provided: "registry key ID", + missing: "registry public key", + }); + } + (None, Some(_)) => { + return Err(RegistryTrustConfigError::Incomplete { + provided: "registry public key", + missing: "registry key ID", + }); + } + (Some(key_id), Some(public_key_hex)) => (key_id.trim(), public_key_hex.trim()), + }; + if key_id.is_empty() + || key_id.len() > 128 + || !key_id + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')) + { + return Err(RegistryTrustConfigError::InvalidKeyId); + } + let decoded = hex::decode(public_key_hex).map_err(|error| { + RegistryTrustConfigError::InvalidPublicKeyHex { + key_id: key_id.to_string(), + reason: error.to_string(), + } + })?; + let actual = decoded.len(); + let key: [u8; 32] = + decoded + .try_into() + .map_err(|_| RegistryTrustConfigError::InvalidPublicKeyLength { + key_id: key_id.to_string(), + actual, + })?; + Ok(RegistryConfig::default().with_trust_anchor(key_id, key)) +} + +/// The outer envelope signs the decoded bytes in `payload`. Encoding the +/// payload instead of reserializing a JSON object avoids ambiguous map order, +/// whitespace, and number representations. +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct RegistryEnvelope { + pub key_id: String, + /// Standard-base64 encoded JSON [`RegistryDocument`]. + pub payload: String, + /// Standard-base64 encoded 64-byte Ed25519 signature over payload bytes. + pub signature: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RegistryDocument { + pub schema_version: u32, + /// Monotonic publisher revision. Persisted rollback protection can build + /// on this value without changing the signed wire format. + pub revision: u64, + pub issued_at: DateTime, + pub expires_at: DateTime, + pub plugins: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct RegistryPlugin { + pub name: String, + pub title: String, + pub summary: String, + pub description: String, + pub author: String, + pub category: String, + #[serde(default)] + pub keywords: Vec, + #[serde(default)] + pub logo_url: Option, + #[serde(default)] + pub repository: Option, + #[serde(default)] + pub docs_url: Option, + pub version: String, + pub platforms: BTreeMap, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct PlatformRelease { + pub url: String, + pub sha256: String, +} + +#[derive(Debug, Clone)] +pub struct VerifiedRegistry { + pub envelope: RegistryEnvelope, + pub document: RegistryDocument, +} + +#[derive(Debug, Error)] +pub enum CatalogError { + #[error( + "Plugin registry trust is not configured: no Ed25519 public keys are trusted for {url}" + )] + TrustNotConfigured { url: String }, + #[error("Plugin registry document from {url} uses untrusted key id '{key_id}'")] + UntrustedKey { url: String, key_id: String }, + #[error("Plugin registry document from {url} contains invalid base64 in {field}: {reason}")] + InvalidEncoding { + url: String, + field: &'static str, + reason: String, + }, + #[error("Plugin registry signature from {url} using key '{key_id}' is invalid")] + InvalidSignature { url: String, key_id: String }, + #[error("Plugin registry document from {url} has unsupported schema version {version}")] + UnsupportedSchema { url: String, version: u32 }, + #[error("Plugin registry document from {url} has invalid validity metadata: {reason}")] + InvalidValidity { url: String, reason: String }, + #[error("Failed to parse signed plugin registry payload from {url}: {reason}")] + Parse { url: String, reason: String }, + #[error("Refusing plugin registry URL with unsafe transport or host: {url}")] + UnsafeUrl { url: String }, + #[error("Failed to create HTTP client for plugin registry {url}: {reason}")] + Client { url: String, reason: String }, + #[error("Failed to fetch plugin registry from {url}: {reason}")] + Fetch { url: String, reason: String }, + #[error("Plugin registry {url} returned HTTP {status}")] + Status { url: String, status: u16 }, + #[error("Plugin registry response from {url} exceeded the {limit}-byte limit")] + TooLarge { url: String, limit: u64 }, +} + +#[derive(Clone)] +pub struct RegistryClient { + config: RegistryConfig, + client: reqwest::Client, +} + +impl RegistryClient { + pub fn new(config: RegistryConfig) -> Result { + validate_url(&config.url, &config, true)?; + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .timeout(REGISTRY_TIMEOUT) + .build() + .map_err(|error| CatalogError::Client { + url: config.url.clone(), + reason: error.to_string(), + })?; + Ok(Self { config, client }) + } + + pub fn config(&self) -> &RegistryConfig { + &self.config + } + + pub async fn fetch(&self) -> Result { + if self.config.trust_anchors.is_empty() { + return Err(CatalogError::TrustNotConfigured { + url: self.config.url.clone(), + }); + } + let response = self + .client + .get(&self.config.url) + .header("User-Agent", "temps-plugin-installer") + .send() + .await + .map_err(|error| CatalogError::Fetch { + url: self.config.url.clone(), + reason: error.to_string(), + })?; + if response.status().is_redirection() { + return Err(CatalogError::Status { + url: self.config.url.clone(), + status: response.status().as_u16(), + }); + } + if !response.status().is_success() { + return Err(CatalogError::Status { + url: self.config.url.clone(), + status: response.status().as_u16(), + }); + } + let body = read_body_capped(response, &self.config.url, MAX_REGISTRY_BYTES).await?; + let envelope: RegistryEnvelope = + serde_json::from_slice(&body).map_err(|error| CatalogError::Parse { + url: self.config.url.clone(), + reason: error.to_string(), + })?; + let verified = verify_envelope(envelope, &self.config.trust_anchors, &self.config.url)?; + validate_freshness(&verified.document, &self.config.url, Utc::now())?; + Ok(verified) + } +} + +pub fn verify_envelope( + envelope: RegistryEnvelope, + anchors: &BTreeMap, + source: &str, +) -> Result { + if anchors.is_empty() { + return Err(CatalogError::TrustNotConfigured { + url: source.to_string(), + }); + } + let key_bytes = anchors + .get(&envelope.key_id) + .ok_or_else(|| CatalogError::UntrustedKey { + url: source.to_string(), + key_id: envelope.key_id.clone(), + })?; + let payload = base64::engine::general_purpose::STANDARD + .decode(&envelope.payload) + .map_err(|error| CatalogError::InvalidEncoding { + url: source.to_string(), + field: "payload", + reason: error.to_string(), + })?; + let signature_bytes = base64::engine::general_purpose::STANDARD + .decode(&envelope.signature) + .map_err(|error| CatalogError::InvalidEncoding { + url: source.to_string(), + field: "signature", + reason: error.to_string(), + })?; + let signature = + Signature::from_slice(&signature_bytes).map_err(|error| CatalogError::InvalidEncoding { + url: source.to_string(), + field: "signature", + reason: error.to_string(), + })?; + let key = VerifyingKey::from_bytes(key_bytes).map_err(|error| CatalogError::Parse { + url: source.to_string(), + reason: format!("invalid trust anchor '{}': {error}", envelope.key_id), + })?; + key.verify(&payload, &signature) + .map_err(|_| CatalogError::InvalidSignature { + url: source.to_string(), + key_id: envelope.key_id.clone(), + })?; + let document: RegistryDocument = + serde_json::from_slice(&payload).map_err(|error| CatalogError::Parse { + url: source.to_string(), + reason: error.to_string(), + })?; + if document.schema_version != 1 { + return Err(CatalogError::UnsupportedSchema { + url: source.to_string(), + version: document.schema_version, + }); + } + if document.revision == 0 { + return Err(CatalogError::InvalidValidity { + url: source.to_string(), + reason: "revision must be greater than zero".to_string(), + }); + } + if document.expires_at <= document.issued_at { + return Err(CatalogError::InvalidValidity { + url: source.to_string(), + reason: "expires_at must be later than issued_at".to_string(), + }); + } + if document.expires_at - document.issued_at > MAX_REGISTRY_VALIDITY { + return Err(CatalogError::InvalidValidity { + url: source.to_string(), + reason: "catalogue validity may not exceed 30 days".to_string(), + }); + } + Ok(VerifiedRegistry { envelope, document }) +} + +fn validate_freshness( + document: &RegistryDocument, + source: &str, + now: DateTime, +) -> Result<(), CatalogError> { + if document.issued_at > now + ChronoDuration::minutes(5) { + return Err(CatalogError::InvalidValidity { + url: source.to_string(), + reason: "issued_at is more than five minutes in the future".to_string(), + }); + } + if document.expires_at <= now { + return Err(CatalogError::InvalidValidity { + url: source.to_string(), + reason: format!("catalogue expired at {}", document.expires_at), + }); + } + Ok(()) +} + +pub(crate) fn validate_url( + value: &str, + config: &RegistryConfig, + registry: bool, +) -> Result { + let parsed = Url::parse(value).map_err(|_| CatalogError::UnsafeUrl { + url: value.to_string(), + })?; + let scheme_ok = parsed.scheme() == "https" || (config.allow_http && parsed.scheme() == "http"); + let host = parsed.host_str(); + let host_ok = if registry { + Url::parse(&config.url) + .ok() + .and_then(|url| url.host_str().map(ToOwned::to_owned)) + .as_deref() + == host + } else { + host.is_some_and(|host| config.allowed_artifact_hosts.contains(host)) + }; + if !scheme_ok || !host_ok || parsed.username() != "" || parsed.password().is_some() { + return Err(CatalogError::UnsafeUrl { + url: value.to_string(), + }); + } + Ok(parsed) +} + +async fn read_body_capped( + response: reqwest::Response, + url: &str, + limit: u64, +) -> Result, CatalogError> { + if response + .content_length() + .is_some_and(|length| length > limit) + { + return Err(CatalogError::TooLarge { + url: url.to_string(), + limit, + }); + } + let mut body = Vec::new(); + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|error| CatalogError::Fetch { + url: url.to_string(), + reason: error.to_string(), + })?; + if body.len() as u64 + chunk.len() as u64 > limit { + return Err(CatalogError::TooLarge { + url: url.to_string(), + limit, + }); + } + body.extend_from_slice(&chunk); + } + Ok(body) +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signer as _, SigningKey}; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + + #[test] + fn test_registry_config_from_anchor_without_values_returns_unconfigured_defaults() { + // Arrange / Act + let config = registry_config_from_anchor(None, None).expect("empty configuration is valid"); + + // Assert + assert_eq!(config.url, REGISTRY_URL); + assert!(config.trust_anchors.is_empty()); + assert!(!config.allow_http); + assert_eq!( + config.allowed_artifact_hosts, + BTreeSet::from(["registry.temps.sh".to_string()]) + ); + } + + #[test] + fn test_registry_config_from_anchor_with_pair_configures_trimmed_anchor() { + // Arrange + let expected_key = [0xabu8; 32]; + let encoded_key = hex::encode(expected_key); + + // Act + let config = registry_config_from_anchor( + Some(" production-key_1 "), + Some(&format!(" {encoded_key} ")), + ) + .expect("a complete valid trust anchor must be accepted"); + + // Assert + assert_eq!( + config.trust_anchors.get("production-key_1"), + Some(&expected_key) + ); + assert_eq!(config.trust_anchors.len(), 1); + assert_eq!(config.url, REGISTRY_URL); + assert!(!config.allow_http); + } + + #[test] + fn test_registry_config_from_anchor_with_half_pair_returns_incomplete_error() { + // Arrange / Act / Assert + assert!(matches!( + registry_config_from_anchor(Some("key-1"), None), + Err(RegistryTrustConfigError::Incomplete { + provided: "registry key ID", + missing: "registry public key" + }) + )); + assert!(matches!( + registry_config_from_anchor(None, Some(&hex::encode([7u8; 32]))), + Err(RegistryTrustConfigError::Incomplete { + provided: "registry public key", + missing: "registry key ID" + }) + )); + } + + #[test] + fn test_registry_config_from_anchor_with_malformed_key_returns_precise_error() { + // Arrange / Act / Assert + assert!(matches!( + registry_config_from_anchor(Some("key-1"), Some("not-hex")), + Err(RegistryTrustConfigError::InvalidPublicKeyHex { ref key_id, .. }) + if key_id == "key-1" + )); + assert!(matches!( + registry_config_from_anchor(Some("key-1"), Some("abcd")), + Err(RegistryTrustConfigError::InvalidPublicKeyLength { + ref key_id, + actual: 2 + }) if key_id == "key-1" + )); + } + + #[test] + fn test_registry_config_from_anchor_with_invalid_key_id_is_rejected() { + // Arrange + let key = hex::encode([7u8; 32]); + let too_long = "a".repeat(129); + + // Act / Assert + for key_id in ["", "contains space", "path/key", "keyé", &too_long] { + assert!( + matches!( + registry_config_from_anchor(Some(key_id), Some(&key)), + Err(RegistryTrustConfigError::InvalidKeyId) + ), + "invalid key ID was accepted: {key_id:?}" + ); + } + } + + async fn oversized_registry_url() -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind registry test server"); + let address = listener.local_addr().expect("registry test address"); + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept registry request"); + let mut request = [0u8; 1024]; + let _ = stream.read(&mut request).await.expect("read request"); + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + MAX_REGISTRY_BYTES + 1 + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write response"); + }); + format!("http://{address}/api/plugins") + } + + fn signed_envelope(key_id: &str, signing_key: &SigningKey) -> RegistryEnvelope { + let payload = serde_json::to_vec(&RegistryDocument { + schema_version: 1, + revision: 1, + issued_at: Utc::now() - ChronoDuration::minutes(1), + expires_at: Utc::now() + ChronoDuration::hours(1), + plugins: Vec::new(), + }) + .expect("serialize registry"); + RegistryEnvelope { + key_id: key_id.to_string(), + payload: base64::engine::general_purpose::STANDARD.encode(&payload), + signature: base64::engine::general_purpose::STANDARD + .encode(signing_key.sign(&payload).to_bytes()), + } + } + + #[test] + fn accepts_valid_signature() { + let signing = SigningKey::from_bytes(&[7; 32]); + let anchors = + BTreeMap::from([("test-key".to_string(), signing.verifying_key().to_bytes())]); + let verified = verify_envelope(signed_envelope("test-key", &signing), &anchors, "test") + .expect("signature should verify"); + assert_eq!(verified.document.schema_version, 1); + } + + #[test] + fn node_crypto_cross_ecosystem_test_vector_verifies() { + // Generated from a 32-byte test seed containing 0x07 using Node's + // built-in crypto Ed25519 implementation. This literal vector keeps + // the registry signer and Rust verifier aligned on standard base64 + // and on signing the decoded JSON payload bytes. + let public_key: [u8; 32] = + hex::decode("ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c") + .expect("test public key hex") + .try_into() + .expect("32-byte test public key"); + let envelope = RegistryEnvelope { + key_id: "node-test".to_string(), + payload: "eyJzY2hlbWFfdmVyc2lvbiI6MSwicmV2aXNpb24iOjEsImlzc3VlZF9hdCI6IjIwMjYtMDktMDFUMDA6MDA6MDBaIiwiZXhwaXJlc19hdCI6IjIwMjYtMDktMzBUMDA6MDA6MDBaIiwicGx1Z2lucyI6W119".to_string(), + signature: "wS4olkCwC6M9ytN05byXhfWi1MHKtEqryxOufhHTykeei5XpWfwiRCNc222xnkSXjNj7Db9qmdWMlvL3AD5hCQ==".to_string(), + }; + let verified = verify_envelope( + envelope, + &BTreeMap::from([("node-test".to_string(), public_key)]), + "node-test-vector", + ) + .expect("Node signature must verify in Rust"); + assert_eq!(verified.document.schema_version, 1); + assert!(verified.document.plugins.is_empty()); + } + + #[test] + fn rejects_invalid_signature() { + let signing = SigningKey::from_bytes(&[7; 32]); + let other = SigningKey::from_bytes(&[8; 32]); + let anchors = BTreeMap::from([("test-key".to_string(), other.verifying_key().to_bytes())]); + assert!(matches!( + verify_envelope(signed_envelope("test-key", &signing), &anchors, "test"), + Err(CatalogError::InvalidSignature { .. }) + )); + } + + #[test] + fn rejects_untrusted_key() { + let signing = SigningKey::from_bytes(&[7; 32]); + let anchors = BTreeMap::from([("other".to_string(), signing.verifying_key().to_bytes())]); + assert!(matches!( + verify_envelope(signed_envelope("test-key", &signing), &anchors, "test"), + Err(CatalogError::UntrustedKey { .. }) + )); + } + + #[test] + fn production_policy_rejects_http_and_foreign_hosts() { + let config = RegistryConfig::default(); + assert!(validate_url("http://registry.temps.sh/api/plugins", &config, true).is_err()); + assert!(validate_url("https://example.com/plugin", &config, false).is_err()); + } + + #[test] + fn expired_live_catalogue_is_rejected() { + let now = Utc::now(); + let document = RegistryDocument { + schema_version: 1, + revision: 2, + issued_at: now - ChronoDuration::hours(2), + expires_at: now - ChronoDuration::hours(1), + plugins: Vec::new(), + }; + assert!(matches!( + validate_freshness(&document, "test", now), + Err(CatalogError::InvalidValidity { .. }) + )); + } + + #[tokio::test] + async fn registry_body_size_is_bounded() { + let signing = SigningKey::from_bytes(&[7; 32]); + let url = oversized_registry_url().await; + let config = RegistryConfig::local(url, "test-key", signing.verifying_key().to_bytes()); + let client = RegistryClient::new(config).expect("registry client"); + assert!(matches!( + client.fetch().await, + Err(CatalogError::TooLarge { .. }) + )); + } +} diff --git a/crates/temps-external-plugins/src/handler.rs b/crates/temps-external-plugins/src/handler.rs index d129a0276..3cfc0e1c7 100644 --- a/crates/temps-external-plugins/src/handler.rs +++ b/crates/temps-external-plugins/src/handler.rs @@ -5,22 +5,129 @@ use std::sync::Arc; -use axum::extract::State; +use axum::extract::rejection::JsonRejection; +use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::routing::{get, post}; -use axum::{Json, Router}; -use serde::Serialize; +use axum::{Extension, Json, Router}; +use serde::{Deserialize, Serialize}; use temps_auth::{permission_guard, RequireAuth}; use temps_core::external_plugin::{NavEntry, NavSection, PluginManifest, UiManifest, UiRoute}; use temps_core::problemdetails::Problem; use utoipa::{OpenApi as OpenApiTrait, ToSchema}; +use crate::service::ExternalPluginsError; use crate::service::ExternalPluginsService; +#[derive(Debug, Clone, Serialize)] +struct ExternalPluginWriteAudit { + context: temps_core::audit::AuditContext, + operation: String, + plugin_name: Option, + version: Option, + platform: Option, + sha256: Option, + signer_key_id: Option, + registry_source: Option, + failure: Option, +} + +impl temps_core::audit::AuditOperation for ExternalPluginWriteAudit { + fn operation_type(&self) -> String { + self.operation.clone() + } + + fn user_id(&self) -> Option { + Some(self.context.user_id) + } + + fn ip_address(&self) -> Option { + self.context.ip_address.clone() + } + + fn user_agent(&self) -> &str { + &self.context.user_agent + } + + fn serialize(&self) -> anyhow::Result { + serde_json::to_string(self).map_err(|error| { + anyhow::anyhow!("failed to serialize external-plugin audit event: {error}") + }) + } +} + /// Handler state for the external plugins API. #[derive(Clone)] pub struct ExternalPluginsAppState { pub service: Arc, + pub audit_service: Arc, + pub sensitive_action_authorizer: Arc, +} + +fn audit_context( + auth: &temps_auth::AuthContext, + metadata: &temps_core::RequestMetadata, +) -> temps_core::audit::AuditContext { + temps_core::audit::AuditContext { + user_id: auth.user_id(), + ip_address: Some(metadata.ip_address.clone()), + user_agent: metadata.user_agent.clone(), + } +} + +async fn record_audit( + state: &ExternalPluginsAppState, + operation: &dyn temps_core::audit::AuditOperation, +) { + if state + .audit_service + .create_audit_log(operation) + .await + .is_err() + { + tracing::error!( + operation = %operation.operation_type(), + "Failed to record external-plugin write audit" + ); + } +} + +async fn record_required_audit( + state: &ExternalPluginsAppState, + operation: &dyn temps_core::audit::AuditOperation, +) -> Result<(), Problem> { + state + .audit_service + .create_audit_log(operation) + .await + .map_err(|_error| { + tracing::error!( + operation = %operation.operation_type(), + "Required external-plugin security audit could not be recorded" + ); + temps_core::problemdetails::new(StatusCode::SERVICE_UNAVAILABLE) + .with_title("Plugin Installation Audit Unavailable") + .with_detail( + "Plugin installation cannot continue until the security audit log is available", + ) + }) +} + +fn install_request_problem(rejection: JsonRejection) -> Problem { + let status = rejection.status(); + let detail = match status { + StatusCode::PAYLOAD_TOO_LARGE => "The plugin install request body is too large", + StatusCode::UNSUPPORTED_MEDIA_TYPE => { + "The plugin install request must use the application/json content type" + } + StatusCode::UNPROCESSABLE_ENTITY => { + "The plugin install request does not match the required JSON schema" + } + _ => "The plugin install request body is not valid JSON", + }; + temps_core::problemdetails::new(status) + .with_title("Invalid Plugin Install Request") + .with_detail(detail) } /// List all running external plugins and their manifests. @@ -52,10 +159,19 @@ pub struct ReloadResponse { pub loaded: usize, /// Names of loaded plugins pub plugins: Vec, + /// Activated installs that could not be verified or started. + pub failures: Vec, /// Human-readable status message pub message: String, } +#[derive(Debug, Serialize, ToSchema)] +pub struct ReloadFailureResponse { + #[serde(skip_serializing_if = "Option::is_none")] + pub plugin: Option, + pub reason: String, +} + /// Reload all external plugins. /// /// Stops all running plugin processes, re-scans the plugins directory, @@ -68,7 +184,9 @@ pub struct ReloadResponse { post, path = "/x/plugins/reload", responses( - (status = 200, description = "Plugins reloaded successfully", body = ReloadResponse), + (status = 200, description = "All plugins reloaded successfully", body = ReloadResponse), + (status = 207, description = "Some plugins reloaded and some failed", body = ReloadResponse), + (status = 502, description = "No activated plugin could be reloaded", body = ReloadResponse), (status = 401, description = "Unauthorized"), (status = 403, description = "Insufficient permissions"), ), @@ -77,35 +195,448 @@ pub struct ReloadResponse { async fn reload_plugins( RequireAuth(auth): RequireAuth, State(state): State, + Extension(metadata): Extension, ) -> Result<(StatusCode, Json), Problem> { permission_guard!(auth, SystemAdmin); tracing::info!("Admin triggered plugin reload"); - let manifests = state.service.reload_plugins().await; - let names: Vec = manifests.iter().map(|m| m.name.clone()).collect(); + let result = state + .service + .reload_plugins() + .await + .map_err(|error| service_problem(&error))?; + let names: Vec = result.manifests.iter().map(|m| m.name.clone()).collect(); let count = names.len(); + let failures: Vec = result + .failures + .iter() + .map(|failure| ReloadFailureResponse { + plugin: failure.plugin.clone(), + reason: failure.reason.clone(), + }) + .collect(); + let status = if failures.is_empty() { + StatusCode::OK + } else if count == 0 { + StatusCode::BAD_GATEWAY + } else { + StatusCode::MULTI_STATUS + }; + let operation = if failures.is_empty() { + "EXTERNAL_PLUGINS_RELOADED" + } else if count == 0 { + "EXTERNAL_PLUGINS_RELOAD_FAILED" + } else { + "EXTERNAL_PLUGINS_RELOAD_PARTIAL" + }; + let failure_detail = (!failures.is_empty()).then(|| { + failures + .iter() + .map(|failure| failure.reason.as_str()) + .collect::>() + .join("; ") + }); + + record_audit( + &state, + &ExternalPluginWriteAudit { + context: audit_context(&auth, &metadata), + operation: operation.to_string(), + plugin_name: None, + version: None, + platform: None, + sha256: None, + signer_key_id: None, + registry_source: None, + failure: failure_detail, + }, + ) + .await; Ok(( - StatusCode::OK, + status, Json(ReloadResponse { loaded: count, plugins: names, - message: format!("Reload complete. {} plugin(s) loaded.", count), + message: if failures.is_empty() { + format!("Reload complete. {count} plugin(s) loaded.") + } else { + format!( + "Reload complete with failures. {count} plugin(s) loaded; {} failed.", + failures.len() + ) + }, + failures, }), )) } +#[derive(Debug, Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct InstallPluginRequest { + /// Validated registry name only. URLs, paths, versions, and hashes are not + /// accepted from HTTP callers. + pub name: String, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct InstallPluginResponse { + pub name: String, + pub version: String, + pub platform: String, + pub sha256: String, + pub message: String, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct PluginCatalogResponse { + pub available: bool, + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + pub plugins: Vec, +} + +#[derive(Debug, Serialize, ToSchema)] +pub struct PluginStatusResponse { + pub configured: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, + pub setup_path: String, +} + +fn service_problem(error: &ExternalPluginsError) -> Problem { + use crate::catalog::CatalogError; + use crate::install::InstallError; + let (status, title) = match error { + ExternalPluginsError::Install(InstallError::UnsafePluginName { .. }) + | ExternalPluginsError::Install(InstallError::UnsafeVersion { .. }) + | ExternalPluginsError::Install(InstallError::UnsupportedPlatform { .. }) + | ExternalPluginsError::Install(InstallError::NoRelease { .. }) + | ExternalPluginsError::NotInRegistry { .. } + | ExternalPluginsError::DuplicateRegistryEntry { .. } => { + (StatusCode::BAD_REQUEST, "Plugin Cannot Be Installed") + } + ExternalPluginsError::Catalog(CatalogError::TrustNotConfigured { .. }) => ( + StatusCode::SERVICE_UNAVAILABLE, + "Plugin Registry Trust Is Not Configured", + ), + ExternalPluginsError::Catalog(CatalogError::UntrustedKey { .. }) => ( + StatusCode::BAD_GATEWAY, + "Plugin Registry Authentication Failed", + ), + ExternalPluginsError::Catalog(_) => ( + StatusCode::BAD_GATEWAY, + "Plugin Registry Verification Failed", + ), + ExternalPluginsError::Install(InstallError::InvalidDigest { .. }) + | ExternalPluginsError::Install(InstallError::UnsafeArtifactUrl { .. }) + | ExternalPluginsError::Install(InstallError::Client { .. }) + | ExternalPluginsError::Install(InstallError::Download { .. }) + | ExternalPluginsError::Install(InstallError::DownloadStatus { .. }) + | ExternalPluginsError::Install(InstallError::TooLarge { .. }) + | ExternalPluginsError::Install(InstallError::DigestMismatch { .. }) => ( + StatusCode::BAD_GATEWAY, + "Plugin Artifact Verification Failed", + ), + ExternalPluginsError::Install(InstallError::RegistryRollback { .. }) => { + (StatusCode::CONFLICT, "Plugin Registry Rollback Refused") + } + ExternalPluginsError::Install(InstallError::Io { .. }) + | ExternalPluginsError::Install(InstallError::MissingActiveRecord { .. }) + | ExternalPluginsError::Install(InstallError::InvalidReceipt { .. }) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Plugin Installation Failed", + ), + ExternalPluginsError::CandidateRejected { .. } => ( + StatusCode::BAD_GATEWAY, + "Plugin Startup Verification Failed", + ), + ExternalPluginsError::ShuttingDown => ( + StatusCode::SERVICE_UNAVAILABLE, + "Plugin Service Is Shutting Down", + ), + }; + temps_core::problemdetails::new(status) + .with_title(title) + .with_detail(public_error_detail(error)) +} + +/// Render an operator-facing error without exposing local paths, transport +/// internals, or plugin-controlled diagnostics. Full typed errors remain in +/// server logs at their origin. +fn public_error_detail(error: &ExternalPluginsError) -> String { + use crate::catalog::CatalogError; + use crate::install::InstallError; + + match error { + ExternalPluginsError::Install( + error @ (InstallError::UnsafePluginName { .. } + | InstallError::UnsafeVersion { .. } + | InstallError::UnsupportedPlatform { .. } + | InstallError::NoRelease { .. } + | InstallError::InvalidDigest { .. } + | InstallError::RegistryRollback { .. }), + ) => error.to_string(), + ExternalPluginsError::NotInRegistry { .. } + | ExternalPluginsError::DuplicateRegistryEntry { .. } + | ExternalPluginsError::ShuttingDown => error.to_string(), + ExternalPluginsError::Catalog(CatalogError::TrustNotConfigured { .. }) => { + "Configure a trusted plugin-registry key ID and Ed25519 public key before using the registry" + .to_string() + } + ExternalPluginsError::Catalog(CatalogError::UntrustedKey { key_id, .. }) => { + format!("The plugin registry used untrusted signing key ID '{key_id}'") + } + ExternalPluginsError::Catalog(_) => { + "The signed plugin registry response could not be authenticated".to_string() + } + ExternalPluginsError::Install(InstallError::DigestMismatch { + plugin, version, .. + }) => format!( + "Downloaded artifact for plugin '{plugin}' v{version} did not match its signed digest" + ), + ExternalPluginsError::Install( + InstallError::UnsafeArtifactUrl { plugin, .. } + | InstallError::Download { plugin, .. }, + ) => format!("Plugin '{plugin}' could not be downloaded securely"), + ExternalPluginsError::Install( + InstallError::Client { .. } + | InstallError::DownloadStatus { .. } + | InstallError::TooLarge { .. }, + ) => "The plugin artifact could not be downloaded securely".to_string(), + ExternalPluginsError::Install( + InstallError::Io { plugin, .. } + | InstallError::MissingActiveRecord { plugin, .. } + | InstallError::InvalidReceipt { plugin, .. }, + ) => format!("Plugin '{plugin}' could not be installed or verified locally"), + ExternalPluginsError::CandidateRejected { name, version, .. } => { + format!("Plugin '{name}' v{version} did not pass startup verification") + } + } +} + +#[utoipa::path( + tag = "External Plugins", + get, + path = "/x/plugins/catalog", + responses( + (status = 200, description = "Signed plugin catalogue, or an unavailable state when registry trust is not configured", body = PluginCatalogResponse), + (status = 401, description = "Unauthorized", body = temps_core::ProblemDetails), + (status = 403, description = "Insufficient permissions", body = temps_core::ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn list_plugin_catalog( + RequireAuth(auth): RequireAuth, + State(state): State, +) -> Result, Problem> { + permission_guard!(auth, SystemAdmin); + let source = state.service.manager().config().registry.url.clone(); + match state.service.catalog().await { + Ok(registry) => Ok(Json(PluginCatalogResponse { + available: true, + source, + reason: None, + plugins: registry.document.plugins, + })), + Err(error) => Ok(Json(PluginCatalogResponse { + available: false, + source, + reason: Some(error.to_string()), + plugins: Vec::new(), + })), + } +} + +#[utoipa::path( + tag = "External Plugins", + post, + path = "/x/plugins/install", + request_body = InstallPluginRequest, + responses( + (status = 200, description = "Plugin verified, installed, and started", body = InstallPluginResponse), + (status = 400, description = "Invalid plugin name or registry release", body = temps_core::ProblemDetails), + (status = 401, description = "Unauthorized", body = temps_core::ProblemDetails), + (status = 403, description = "Insufficient permissions", body = temps_core::ProblemDetails), + (status = 409, description = "Registry rollback refused", body = temps_core::ProblemDetails), + (status = 413, description = "Request body exceeds the configured limit", body = temps_core::ProblemDetails), + (status = 415, description = "Request content type is not application/json", body = temps_core::ProblemDetails), + (status = 422, description = "Request JSON does not match the install schema", body = temps_core::ProblemDetails), + (status = 428, description = "Recent sensitive-action verification required", body = temps_core::ProblemDetails), + (status = 500, description = "Local plugin installation failed", body = temps_core::ProblemDetails), + (status = 502, description = "Registry, artifact, or plugin startup verification failed", body = temps_core::ProblemDetails), + (status = 503, description = "Registry trust, plugin service, or security audit unavailable", body = temps_core::ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn install_plugin( + RequireAuth(auth): RequireAuth, + State(state): State, + Extension(metadata): Extension, + request: Result, JsonRejection>, +) -> Result, Problem> { + let Json(request) = request.map_err(install_request_problem)?; + permission_guard!(auth, SystemAdmin); + temps_auth::require_sensitive_action( + state.sensitive_action_authorizer.as_ref(), + &auth, + temps_core::SensitiveAction::InstallExternalPlugin { + name: request.name.clone(), + }, + ) + .await?; + let context = audit_context(&auth, &metadata); + record_audit( + &state, + &ExternalPluginWriteAudit { + context: context.clone(), + operation: "EXTERNAL_PLUGIN_INSTALL_REQUESTED".to_string(), + plugin_name: Some(request.name.clone()), + version: None, + platform: None, + sha256: None, + signer_key_id: None, + registry_source: Some(state.service.manager().config().registry.url.clone()), + failure: None, + }, + ) + .await; + let selected = match state.service.select_plugin(&request.name).await { + Ok(selected) => selected, + Err(error) => { + record_audit( + &state, + &ExternalPluginWriteAudit { + context, + operation: "EXTERNAL_PLUGIN_INSTALL_FAILED".to_string(), + plugin_name: Some(request.name), + version: None, + platform: None, + sha256: None, + signer_key_id: None, + registry_source: Some(state.service.manager().config().registry.url.clone()), + failure: Some(public_error_detail(&error)), + }, + ) + .await; + return Err(service_problem(&error)); + } + }; + let identity = selected.identity.clone(); + record_required_audit( + &state, + &ExternalPluginWriteAudit { + context: context.clone(), + operation: "EXTERNAL_PLUGIN_RELEASE_SELECTED".to_string(), + plugin_name: Some(identity.name.clone()), + version: Some(identity.version.clone()), + platform: Some(identity.platform.clone()), + sha256: Some(identity.sha256.clone()), + signer_key_id: Some(identity.signer_key_id.clone()), + registry_source: Some(identity.registry_source.clone()), + failure: None, + }, + ) + .await?; + let outcome = match state.service.install_selected(selected).await { + Ok(outcome) => outcome, + Err(error) => { + record_audit( + &state, + &ExternalPluginWriteAudit { + context, + operation: "EXTERNAL_PLUGIN_INSTALL_FAILED".to_string(), + plugin_name: Some(identity.name), + version: Some(identity.version), + platform: Some(identity.platform), + sha256: Some(identity.sha256), + signer_key_id: Some(identity.signer_key_id), + registry_source: Some(identity.registry_source), + failure: Some(public_error_detail(&error)), + }, + ) + .await; + return Err(service_problem(&error)); + } + }; + record_audit( + &state, + &ExternalPluginWriteAudit { + context, + operation: "EXTERNAL_PLUGIN_INSTALLED".to_string(), + plugin_name: Some(outcome.name.clone()), + version: Some(outcome.version.clone()), + platform: Some(outcome.platform.clone()), + sha256: Some(outcome.sha256.clone()), + signer_key_id: Some(outcome.signer_key_id.clone()), + registry_source: Some(outcome.registry_source.clone()), + failure: None, + }, + ) + .await; + Ok(Json(InstallPluginResponse { + name: outcome.name.clone(), + version: outcome.version, + platform: outcome.platform, + sha256: outcome.sha256, + message: format!( + "Plugin '{}' was verified, installed, and started", + outcome.name + ), + })) +} + +#[utoipa::path( + tag = "External Plugins", + get, + path = "/x/plugins/{name}/status", + params(("name" = String, Path)), + responses( + (status = 200, description = "Verified active plugin status", body = PluginStatusResponse), + (status = 400, description = "Invalid plugin name", body = temps_core::ProblemDetails), + (status = 401, description = "Unauthorized", body = temps_core::ProblemDetails), + (status = 403, description = "Insufficient permissions", body = temps_core::ProblemDetails), + ), + security(("bearer_auth" = [])) +)] +async fn get_plugin_status( + RequireAuth(auth): RequireAuth, + State(state): State, + Path(name): Path, +) -> Result, Problem> { + permission_guard!(auth, SystemAdmin); + crate::install::validate_plugin_name(&name) + .map_err(|error| service_problem(&ExternalPluginsError::Install(error)))?; + let configured = state.service.manager().is_running(&name).await; + Ok(Json(PluginStatusResponse { + configured, + reason: (!configured) + .then(|| format!("Plugin '{name}' is not running from a verified active installation")), + setup_path: "/settings/plugins".to_string(), + })) +} + /// Build the router for external plugin management endpoints. pub fn configure_routes() -> Router { Router::new() .route("/x/plugins", get(list_external_plugins)) .route("/x/plugins/reload", post(reload_plugins)) + .route("/x/plugins/catalog", get(list_plugin_catalog)) + .route("/x/plugins/install", post(install_plugin)) + .route("/x/plugins/{name}/status", get(get_plugin_status)) } #[derive(OpenApiTrait)] #[openapi( - paths(list_external_plugins, reload_plugins), + paths( + list_external_plugins, + reload_plugins, + list_plugin_catalog, + install_plugin, + get_plugin_status, + ), components( schemas( PluginManifest, @@ -114,6 +645,15 @@ pub fn configure_routes() -> Router { UiManifest, UiRoute, ReloadResponse, + ReloadFailureResponse, + crate::catalog::RegistryEnvelope, + crate::catalog::RegistryPlugin, + crate::catalog::PlatformRelease, + InstallPluginRequest, + InstallPluginResponse, + PluginCatalogResponse, + PluginStatusResponse, + temps_core::ProblemDetails, ) ), tags( @@ -126,13 +666,140 @@ pub struct ExternalPluginsApiDoc; mod tests { use super::*; - use chrono::Utc; + use std::collections::BTreeMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Mutex; + + use axum::body::Body; + use axum::http::{header::CONTENT_TYPE, Request}; + use base64::Engine as _; + use chrono::{Duration as ChronoDuration, Utc}; + use ed25519_dalek::{Signer as _, SigningKey}; use temps_auth::context::AuthContext; use temps_auth::permissions::Role; use temps_entities::users; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tower::ServiceExt; + use tracing::instrument::WithSubscriber; + use tracing_subscriber::{layer::SubscriberExt, Layer}; use crate::manager::ExternalPluginConfig; + struct NoopAuditLogger; + + struct RejectingAuditLogger; + + #[derive(Clone, Default)] + struct CapturedAuditEvents(Arc>>); + + struct EventFieldVisitor<'a>(&'a mut String); + + impl tracing::field::Visit for EventFieldVisitor<'_> { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + use std::fmt::Write as _; + + let _ = write!(self.0, " {}={value:?}", field.name()); + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + use std::fmt::Write as _; + + let _ = write!(self.0, " {}={value}", field.name()); + } + } + + impl Layer for CapturedAuditEvents + where + S: tracing::Subscriber, + { + fn on_event( + &self, + event: &tracing::Event<'_>, + _context: tracing_subscriber::layer::Context<'_, S>, + ) { + let mut fields = String::new(); + event.record(&mut EventFieldVisitor(&mut fields)); + self.0 + .lock() + .expect("captured audit-event lock") + .push(fields); + } + } + + struct AllowSensitiveActions; + struct RequireSensitiveVerification; + + #[async_trait::async_trait] + impl temps_core::SensitiveActionAuthorizer for AllowSensitiveActions { + async fn authorize( + &self, + _action: &temps_core::SensitiveAction, + _principal: &temps_core::SensitiveActionPrincipal, + ) -> Result< + temps_core::SensitiveActionDecision, + temps_core::SensitiveActionAuthorizationError, + > { + Ok(temps_core::SensitiveActionDecision::Allow) + } + } + + #[async_trait::async_trait] + impl temps_core::SensitiveActionAuthorizer for RequireSensitiveVerification { + async fn authorize( + &self, + _action: &temps_core::SensitiveAction, + _principal: &temps_core::SensitiveActionPrincipal, + ) -> Result< + temps_core::SensitiveActionDecision, + temps_core::SensitiveActionAuthorizationError, + > { + Ok(temps_core::SensitiveActionDecision::RequireVerification { + mfa_setup_required: false, + }) + } + } + + #[async_trait::async_trait] + impl temps_core::AuditLogger for NoopAuditLogger { + async fn create_audit_log( + &self, + _operation: &dyn temps_core::audit::AuditOperation, + ) -> anyhow::Result<()> { + Ok(()) + } + } + + #[async_trait::async_trait] + impl temps_core::AuditLogger for RejectingAuditLogger { + async fn create_audit_log( + &self, + _operation: &dyn temps_core::audit::AuditOperation, + ) -> anyhow::Result<()> { + Err(anyhow::anyhow!( + "audit backend unavailable: private-audit-detail-must-not-leak" + )) + } + } + + #[derive(Default)] + struct RecordingAuditLogger { + operations: std::sync::Mutex>, + } + + #[async_trait::async_trait] + impl temps_core::AuditLogger for RecordingAuditLogger { + async fn create_audit_log( + &self, + operation: &dyn temps_core::audit::AuditOperation, + ) -> anyhow::Result<()> { + self.operations + .lock() + .expect("audit operation lock") + .push(operation.operation_type()); + Ok(()) + } + } + fn mock_db() -> Arc { Arc::new(sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection()) } @@ -144,9 +811,132 @@ mod tests { ); ExternalPluginsAppState { service: Arc::new(ExternalPluginsService::new_empty(config, None, mock_db())), + audit_service: Arc::new(NoopAuditLogger), + sensitive_action_authorizer: Arc::new(AllowSensitiveActions), } } + fn test_state_with_audit( + audit_service: Arc, + ) -> ExternalPluginsAppState { + let mut state = test_state(); + state.audit_service = audit_service; + state + } + + async fn test_state_with_signed_registry( + audit_service: Arc, + ) -> ( + tempfile::TempDir, + ExternalPluginsAppState, + Arc, + tokio::task::JoinHandle<()>, + ) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind signed registry fixture"); + let address = listener.local_addr().expect("signed registry address"); + let signing_key = SigningKey::from_bytes(&[19; 32]); + let key_id = "handler-audit-test"; + let document = crate::catalog::RegistryDocument { + schema_version: 1, + revision: 1, + issued_at: Utc::now() - ChronoDuration::minutes(1), + expires_at: Utc::now() + ChronoDuration::hours(1), + plugins: vec![crate::catalog::RegistryPlugin { + name: "safe-plugin".to_string(), + title: "Safe plugin".to_string(), + summary: "Audit boundary fixture".to_string(), + description: "Must never reach the artifact download".to_string(), + author: "Temps".to_string(), + category: "Testing".to_string(), + keywords: Vec::new(), + logo_url: None, + repository: None, + docs_url: None, + version: "1.0.0".to_string(), + platforms: BTreeMap::from([( + crate::install::platform_target().expect("supported test platform"), + crate::catalog::PlatformRelease { + url: format!("http://{address}/artifact"), + sha256: "00".repeat(32), + }, + )]), + }], + }; + let payload = serde_json::to_vec(&document).expect("serialize registry fixture"); + let envelope = crate::catalog::RegistryEnvelope { + key_id: key_id.to_string(), + payload: base64::engine::general_purpose::STANDARD.encode(&payload), + signature: base64::engine::general_purpose::STANDARD + .encode(signing_key.sign(&payload).to_bytes()), + }; + let registry_body = serde_json::to_vec(&envelope).expect("serialize registry envelope"); + let requests = Arc::new(AtomicUsize::new(0)); + let server_requests = requests.clone(); + let server = tokio::spawn(async move { + while let Ok(Ok((mut stream, _))) = + tokio::time::timeout(std::time::Duration::from_secs(1), listener.accept()).await + { + let mut request = [0u8; 2048]; + let read = stream + .read(&mut request) + .await + .expect("read fixture request"); + let request = String::from_utf8_lossy(&request[..read]); + server_requests.fetch_add(1, Ordering::SeqCst); + let body = if request.starts_with("GET /api/plugins ") { + registry_body.as_slice() + } else { + b"artifact-must-not-be-requested" + }; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write fixture response headers"); + stream + .write_all(body) + .await + .expect("write fixture response body"); + } + }); + + let temp = tempfile::tempdir().expect("plugin handler tempdir"); + let mut config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + config.registry = crate::catalog::RegistryConfig::local( + format!("http://{address}/api/plugins"), + key_id, + signing_key.verifying_key().to_bytes(), + ); + let state = ExternalPluginsAppState { + service: Arc::new(ExternalPluginsService::new_empty(config, None, mock_db())), + audit_service, + sensitive_action_authorizer: Arc::new(AllowSensitiveActions), + }; + (temp, state, requests, server) + } + + fn metadata() -> Extension { + Extension(temps_core::RequestMetadata { + ip_address: "192.0.2.1".to_string(), + user_agent: "external-plugin-test".to_string(), + headers: Default::default(), + visitor_id_cookie: None, + session_id_cookie: None, + base_url: "http://localhost".to_string(), + scheme: "http".to_string(), + host: "localhost".to_string(), + is_secure: false, + }) + } + fn test_user(id: i32) -> users::Model { let now = Utc::now(); users::Model { @@ -172,7 +962,7 @@ mod tests { } fn user_auth(role: Role) -> RequireAuth { - RequireAuth(AuthContext::new_session(test_user(1), role)) + RequireAuth(AuthContext::new_persisted_session(test_user(1), role, 1)) } // Regression tests for the unauthenticated-access finding: `reload_plugins` @@ -184,7 +974,7 @@ mod tests { #[tokio::test] async fn reload_plugins_rejects_non_admin() { let state = test_state(); - let err = reload_plugins(user_auth(Role::User), State(state)) + let err = reload_plugins(user_auth(Role::User), State(state), metadata()) .await .expect_err("a plain User role must not be able to reload plugins"); assert_eq!(err.status_code, StatusCode::FORBIDDEN); @@ -192,11 +982,132 @@ mod tests { #[tokio::test] async fn reload_plugins_allows_platform_admin() { + // Arrange + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + let audit = Arc::new(RecordingAuditLogger::default()); + let state = ExternalPluginsAppState { + service: Arc::new(ExternalPluginsService::new_empty(config, None, mock_db())), + audit_service: audit.clone(), + sensitive_action_authorizer: Arc::new(AllowSensitiveActions), + }; + + // Act + let (status, response) = + reload_plugins(user_auth(Role::PlatformAdmin), State(state), metadata()) + .await + .expect("a PlatformAdmin must be able to reload plugins"); + + // Assert + assert_eq!(status, StatusCode::OK); + assert_eq!(response.loaded, 0); + assert!(response.failures.is_empty()); + assert_eq!( + *audit.operations.lock().expect("audit operation lock"), + vec!["EXTERNAL_PLUGINS_RELOADED".to_string()] + ); + } + + #[tokio::test] + async fn test_reload_plugins_all_failed_returns_bad_gateway_and_failure_audit() { + // Arrange + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + std::fs::create_dir_all(config.plugins_dir.join("broken-plugin")) + .expect("broken active plugin directory"); + let audit = Arc::new(RecordingAuditLogger::default()); + let state = ExternalPluginsAppState { + service: Arc::new(ExternalPluginsService::new_empty(config, None, mock_db())), + audit_service: audit.clone(), + sensitive_action_authorizer: Arc::new(AllowSensitiveActions), + }; + + // Act + let (status, response) = + reload_plugins(user_auth(Role::PlatformAdmin), State(state), metadata()) + .await + .expect("reload reports individual failures in a typed response"); + + // Assert + assert_eq!(status, StatusCode::BAD_GATEWAY); + assert_eq!(response.loaded, 0); + assert_eq!(response.failures.len(), 1); + assert_eq!( + response.failures[0].reason, + "Activated plugin installation failed verification" + ); + assert_eq!( + *audit.operations.lock().expect("audit operation lock"), + vec!["EXTERNAL_PLUGINS_RELOAD_FAILED".to_string()] + ); + } + + #[tokio::test] + async fn test_list_plugin_catalog_non_admin_returns_forbidden() { + // Arrange let state = test_state(); - let (status, _) = reload_plugins(user_auth(Role::PlatformAdmin), State(state)) + + // Act + let error = list_plugin_catalog(user_auth(Role::User), State(state)) .await - .expect("a PlatformAdmin must be able to reload plugins"); - assert_eq!(status, StatusCode::OK); + .expect_err("catalogue access must require system administration"); + + // Assert + assert_eq!(error.status_code, StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn test_install_plugin_non_admin_returns_forbidden_without_audit() { + // Arrange + let audit = Arc::new(RecordingAuditLogger::default()); + let state = test_state_with_audit(audit.clone()); + + // Act + let error = install_plugin( + user_auth(Role::User), + State(state), + metadata(), + Ok(Json(InstallPluginRequest { + name: "safe-plugin".to_string(), + })), + ) + .await + .expect_err("install must require system administration"); + + // Assert + assert_eq!(error.status_code, StatusCode::FORBIDDEN); + assert!( + audit + .operations + .lock() + .expect("audit operation lock") + .is_empty(), + "a rejected caller must not create an install audit entry" + ); + } + + #[tokio::test] + async fn test_get_plugin_status_non_admin_returns_forbidden() { + // Arrange + let state = test_state(); + + // Act + let error = get_plugin_status( + user_auth(Role::User), + State(state), + Path("safe-plugin".to_string()), + ) + .await + .expect_err("status reveals host plugin state and must require an administrator"); + + // Assert + assert_eq!(error.status_code, StatusCode::FORBIDDEN); } #[tokio::test] @@ -211,6 +1122,182 @@ mod tests { assert!(manifests.is_empty()); } + #[tokio::test] + async fn failed_install_records_attempt_and_failure() { + let audit = Arc::new(RecordingAuditLogger::default()); + let state = test_state_with_audit(audit.clone()); + let error = install_plugin( + user_auth(Role::PlatformAdmin), + State(state), + metadata(), + Ok(Json(InstallPluginRequest { + name: "../../escape".to_string(), + })), + ) + .await + .expect_err("unsafe plugin name must fail"); + assert_eq!(error.status_code, StatusCode::BAD_REQUEST); + assert_eq!( + *audit.operations.lock().expect("audit operation lock"), + vec![ + "EXTERNAL_PLUGIN_INSTALL_REQUESTED".to_string(), + "EXTERNAL_PLUGIN_INSTALL_FAILED".to_string(), + ] + ); + } + + #[tokio::test] + async fn install_requires_sensitive_action_verification_before_audit_or_execution() { + let audit = Arc::new(RecordingAuditLogger::default()); + let mut state = test_state_with_audit(audit.clone()); + state.sensitive_action_authorizer = Arc::new(RequireSensitiveVerification); + let error = install_plugin( + user_auth(Role::PlatformAdmin), + State(state), + metadata(), + Ok(Json(InstallPluginRequest { + name: "safe".to_string(), + })), + ) + .await + .expect_err("step-up verification must be required"); + assert_eq!(error.status_code, StatusCode::PRECONDITION_REQUIRED); + assert!( + audit + .operations + .lock() + .expect("audit operation lock") + .is_empty(), + "install audit must not claim an attempt before authorization succeeds" + ); + } + + #[tokio::test] + async fn install_stops_before_artifact_download_when_release_audit_fails() { + // Arrange: the first request authenticates the signed registry. Any + // second request would be the artifact download and therefore native + // code crossing the pre-execution audit boundary. + let (_temp, state, requests, server) = + test_state_with_signed_registry(Arc::new(RejectingAuditLogger)).await; + + // Act + let error = install_plugin( + user_auth(Role::PlatformAdmin), + State(state), + metadata(), + Ok(Json(InstallPluginRequest { + name: "safe-plugin".to_string(), + })), + ) + .await + .expect_err("a failed release audit must stop installation"); + + // Assert + assert_eq!(error.status_code, StatusCode::SERVICE_UNAVAILABLE); + assert_eq!( + error.body.get("title").and_then(serde_json::Value::as_str), + Some("Plugin Installation Audit Unavailable") + ); + let serialized = serde_json::to_string(&error.body).expect("serialize public problem"); + assert!(!serialized.contains("private-audit-detail")); + assert_eq!( + requests.load(Ordering::SeqCst), + 1, + "the handler may fetch the signed registry but must not request the artifact" + ); + server.abort(); + } + + #[tokio::test] + async fn failed_audit_logs_do_not_expose_backend_error_details() { + let state = test_state_with_audit(Arc::new(RejectingAuditLogger)); + let captured = CapturedAuditEvents::default(); + let subscriber = tracing_subscriber::registry().with(captured.clone()); + let operation = ExternalPluginWriteAudit { + context: audit_context(&user_auth(Role::PlatformAdmin).0, &metadata().0), + operation: "EXTERNAL_PLUGIN_INSTALL_REQUESTED".to_string(), + plugin_name: Some("safe-plugin".to_string()), + version: None, + platform: None, + sha256: None, + signer_key_id: None, + registry_source: None, + failure: None, + }; + + record_audit(&state, &operation) + .with_subscriber(subscriber) + .await; + + let logs = captured + .0 + .lock() + .expect("captured audit-event lock") + .join("\n"); + assert!(logs.contains("EXTERNAL_PLUGIN_INSTALL_REQUESTED")); + assert!(!logs.contains("private-audit-detail")); + } + + #[tokio::test] + async fn malformed_install_bodies_return_documented_problem_details() { + let app = configure_routes() + .with_state(test_state()) + .layer(Extension(metadata().0)) + .layer(Extension(user_auth(Role::PlatformAdmin).0)); + let oversized_body = format!(r#"{{"name":"{}"}}"#, "a".repeat(2 * 1024 * 1024)); + + for (body, content_type, expected_status) in [ + ( + "{".to_string(), + Some("application/json"), + StatusCode::BAD_REQUEST, + ), + ( + r#"{"name":"safe-plugin"}"#.to_string(), + None, + StatusCode::UNSUPPORTED_MEDIA_TYPE, + ), + ( + r#"{"name":42}"#.to_string(), + Some("application/json"), + StatusCode::UNPROCESSABLE_ENTITY, + ), + ( + oversized_body, + Some("application/json"), + StatusCode::PAYLOAD_TOO_LARGE, + ), + ] { + let mut request = Request::builder().method("POST").uri("/x/plugins/install"); + if let Some(content_type) = content_type { + request = request.header(CONTENT_TYPE, content_type); + } + let response = app + .clone() + .oneshot(request.body(Body::from(body)).expect("install request")) + .await + .expect("install response"); + + assert_eq!(response.status(), expected_status); + assert_eq!( + response + .headers() + .get(CONTENT_TYPE) + .and_then(|value| value.to_str().ok()), + Some("application/problem+json") + ); + let body = http_body_util::BodyExt::collect(response.into_body()) + .await + .expect("collect problem body") + .to_bytes(); + let problem: serde_json::Value = serde_json::from_slice(&body).expect("problem JSON"); + assert_eq!( + problem.get("title").and_then(serde_json::Value::as_str), + Some("Invalid Plugin Install Request") + ); + } + } + #[test] fn test_openapi_spec_has_plugins_path() { let spec = ExternalPluginsApiDoc::openapi(); @@ -247,6 +1334,141 @@ mod tests { ); } + #[test] + fn openapi_spec_has_catalog_install_and_status_paths() { + let spec = ExternalPluginsApiDoc::openapi(); + for path in [ + "/x/plugins/catalog", + "/x/plugins/install", + "/x/plugins/{name}/status", + ] { + assert!(spec.paths.paths.contains_key(path), "missing {path}"); + } + } + + #[test] + fn openapi_spec_documents_plugin_management_errors() { + let spec = serde_json::to_value(ExternalPluginsApiDoc::openapi()) + .expect("serialize external plugin OpenAPI document"); + let paths = spec + .get("paths") + .and_then(serde_json::Value::as_object) + .expect("OpenAPI paths"); + + for (path, method, expected_statuses) in [ + ("/x/plugins/catalog", "get", &["200", "401", "403"][..]), + ( + "/x/plugins/install", + "post", + &[ + "200", "400", "401", "403", "409", "413", "415", "422", "428", "500", "502", + "503", + ][..], + ), + ( + "/x/plugins/{name}/status", + "get", + &["200", "400", "401", "403"][..], + ), + ] { + let responses = paths + .get(path) + .and_then(|item| item.get(method)) + .and_then(|operation| operation.get("responses")) + .and_then(serde_json::Value::as_object) + .unwrap_or_else(|| panic!("missing responses for {method} {path}")); + for status in expected_statuses { + assert!( + responses.contains_key(*status), + "missing {status} response for {method} {path}" + ); + } + } + } + + #[test] + fn install_request_rejects_remote_control_fields() { + for body in [ + r#"{"name":"safe","url":"https://example.com/plugin"}"#, + r#"{"name":"safe","path":"../../plugin"}"#, + r#"{"name":"safe","sha256":"00"}"#, + r#"{"name":"safe","version":"1.0.0"}"#, + ] { + assert!(serde_json::from_str::(body).is_err()); + } + assert!(serde_json::from_str::(r#"{"name":"safe"}"#).is_ok()); + } + + #[test] + fn test_service_problem_untrusted_registry_key_returns_authentication_bad_gateway() { + // Arrange + let error = ExternalPluginsError::Catalog(crate::catalog::CatalogError::UntrustedKey { + url: "https://registry.temps.sh/api/plugins".to_string(), + key_id: "rotated-without-anchor".to_string(), + }); + + // Act + let problem = service_problem(&error); + + // Assert + assert_eq!(problem.status_code, StatusCode::BAD_GATEWAY); + assert_eq!( + problem + .body + .get("title") + .and_then(serde_json::Value::as_str), + Some("Plugin Registry Authentication Failed") + ); + assert!(problem + .body + .get("detail") + .and_then(serde_json::Value::as_str) + .is_some_and(|detail| detail.contains("rotated-without-anchor"))); + } + + #[test] + fn test_service_problem_and_audit_detail_hide_local_install_paths() { + let secret_path = "/srv/temps/private/plugins/example/receipt.json"; + let sentinel_secret = "postgres://admin:must-not-leak@example.test/temps"; + let error = ExternalPluginsError::Install(crate::install::InstallError::InvalidReceipt { + plugin: "example".to_string(), + path: secret_path.to_string(), + reason: sentinel_secret.to_string(), + }); + + let public_detail = public_error_detail(&error); + let problem = service_problem(&error); + let audit = ExternalPluginWriteAudit { + context: temps_core::audit::AuditContext { + user_id: 1, + ip_address: Some("192.0.2.1".to_string()), + user_agent: "test".to_string(), + }, + operation: "EXTERNAL_PLUGIN_INSTALL_FAILED".to_string(), + plugin_name: Some("example".to_string()), + version: None, + platform: None, + sha256: None, + signer_key_id: None, + registry_source: None, + failure: Some(public_detail.clone()), + }; + let serialized_audit = temps_core::audit::AuditOperation::serialize(&audit) + .expect("safe audit event must serialize"); + + assert!(!public_detail.contains(secret_path)); + assert!(!public_detail.contains(sentinel_secret)); + assert!(!serialized_audit.contains(secret_path)); + assert!(!serialized_audit.contains(sentinel_secret)); + assert_eq!( + problem + .body + .get("detail") + .and_then(serde_json::Value::as_str), + Some(public_detail.as_str()) + ); + } + #[test] fn test_openapi_spec_has_reload_response_schema() { let spec = ExternalPluginsApiDoc::openapi(); @@ -262,6 +1484,7 @@ mod tests { let response = ReloadResponse { loaded: 2, plugins: vec!["seo-analyzer".into(), "monitoring".into()], + failures: Vec::new(), message: "Reload complete. 2 plugin(s) loaded.".into(), }; let json = serde_json::to_value(&response).unwrap(); diff --git a/crates/temps-external-plugins/src/install.rs b/crates/temps-external-plugins/src/install.rs new file mode 100644 index 000000000..798a9c4d4 --- /dev/null +++ b/crates/temps-external-plugins/src/install.rs @@ -0,0 +1,1538 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Bounded direct-binary installation and authenticated activation records. + +use std::io::{Seek as _, SeekFrom}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use futures::StreamExt as _; +use serde::{Deserialize, Serialize}; +use sha2::{Digest as _, Sha256}; +use thiserror::Error; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + +use crate::catalog::{ + validate_url, PlatformRelease, RegistryConfig, RegistryEnvelope, RegistryPlugin, + VerifiedRegistry, +}; + +pub const MAX_BINARY_BYTES: u64 = 256 * 1024 * 1024; +const BINARY_TIMEOUT: Duration = Duration::from_secs(120); +const ACTIVE_FILE: &str = "active.json"; +const RECEIPT_FILE: &str = "receipt.json"; +const BINARY_FILE: &str = "plugin"; +const REGISTRY_STATE_FILE: &str = "registry-state.json"; + +#[derive(Debug, Error)] +pub enum InstallError { + #[error("Plugin registry name '{name}' is unsafe; expected 1-64 lowercase ASCII letters, digits, or internal hyphens")] + UnsafePluginName { name: String }, + #[error("Plugin '{plugin}' has unsafe release version '{version}'")] + UnsafeVersion { plugin: String, version: String }, + #[error("Unsupported platform: {os} {arch}")] + UnsupportedPlatform { os: String, arch: String }, + #[error("Plugin '{plugin}' v{version} has no binary for platform '{platform}'")] + NoRelease { + plugin: String, + version: String, + platform: String, + }, + #[error("Plugin '{plugin}' v{version} has invalid SHA-256 '{digest}': expected exactly 64 hexadecimal characters")] + InvalidDigest { + plugin: String, + version: String, + digest: String, + }, + #[error("Refusing unsafe binary URL for plugin '{plugin}': {url}")] + UnsafeArtifactUrl { plugin: String, url: String }, + #[error("Failed to create plugin download client for {url}: {reason}")] + Client { url: String, reason: String }, + #[error("Failed to download plugin '{plugin}' from {url}: {reason}")] + Download { + plugin: String, + url: String, + reason: String, + }, + #[error("Plugin binary download from {url} returned HTTP {status}")] + DownloadStatus { url: String, status: u16 }, + #[error("Plugin binary download from {url} exceeded the {limit}-byte limit")] + TooLarge { url: String, limit: u64 }, + #[error("SHA-256 mismatch for plugin '{plugin}' v{version}: expected {expected}, downloaded {actual}")] + DigestMismatch { + plugin: String, + version: String, + expected: String, + actual: String, + }, + #[error("Failed to write plugin '{plugin}' installation path {path}: {reason}")] + Io { + plugin: String, + path: String, + reason: String, + }, + #[error("Installed plugin '{plugin}' has no active release record at {path}")] + MissingActiveRecord { plugin: String, path: String }, + #[error("Installed plugin '{plugin}' has an invalid trusted receipt at {path}: {reason}")] + InvalidReceipt { + plugin: String, + path: String, + reason: String, + }, + #[error("Refusing signed plugin registry revision {received}; this instance has already accepted revision {highest}")] + RegistryRollback { received: u64, highest: u64 }, +} + +#[derive(Debug, Clone)] +pub struct InstallCandidate { + pub name: String, + pub version: String, + pub platform: String, + pub sha256: String, + pub binary_path: PathBuf, + plugin_root: PathBuf, + install_directory: String, +} + +#[derive(Debug, Clone)] +pub struct ActiveInstallation { + pub name: String, + pub version: String, + pub sha256: String, + pub binary_path: PathBuf, +} + +/// A regular executable opened without following a leaf symlink and hashed +/// through this exact descriptor. The manager executes this descriptor rather +/// than resolving the path again, closing the verify-to-exec race. +pub(crate) struct VerifiedExecutable { + file: std::fs::File, + pub display_path: PathBuf, +} + +impl VerifiedExecutable { + #[cfg(unix)] + pub(crate) fn command_path(&self, plugin: &str) -> Result { + use std::os::fd::AsRawFd as _; + + let descriptor = self.file.as_raw_fd(); + // SAFETY: fcntl reads and updates flags on a live descriptor owned by + // this value. Clearing CLOEXEC is required so /proc/self/fd or /dev/fd + // still identifies the verified inode in the child at exec time. + let flags = unsafe { libc::fcntl(descriptor, libc::F_GETFD) }; + if flags < 0 + || unsafe { libc::fcntl(descriptor, libc::F_SETFD, flags & !libc::FD_CLOEXEC) } < 0 + { + return Err(io_error( + plugin, + &self.display_path, + std::io::Error::last_os_error(), + )); + } + #[cfg(target_os = "linux")] + let path = PathBuf::from(format!("/proc/self/fd/{descriptor}")); + // macOS does not expose fexecve and rejects executing /dev/fd paths; + // its fallback reopens the owner-read-only path after the descriptor + // walk and hash. Linux executes the exact verified descriptor. + #[cfg(not(target_os = "linux"))] + let path = self.display_path.clone(); + Ok(path) + } + + #[cfg(not(unix))] + pub(crate) fn command_path(&self, _plugin: &str) -> Result { + Ok(self.display_path.clone()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +struct InstallReceipt { + envelope: RegistryEnvelope, + plugin_name: String, + version: String, + platform: String, + sha256: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct ActiveRecord { + version: String, + directory: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct RegistryState { + highest_revision: u64, +} + +#[derive(Clone)] +pub struct PluginInstaller { + registry: RegistryConfig, + client: reqwest::Client, +} + +impl PluginInstaller { + pub fn new(registry: RegistryConfig) -> Result { + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::none()) + .timeout(BINARY_TIMEOUT) + .build() + .map_err(|error| InstallError::Client { + url: registry.url.clone(), + reason: error.to_string(), + })?; + Ok(Self { registry, client }) + } + + pub async fn prepare( + &self, + plugins_dir: &Path, + registry: &VerifiedRegistry, + plugin: &RegistryPlugin, + ) -> Result { + validate_plugin_name(&plugin.name)?; + validate_version(&plugin.name, &plugin.version)?; + let platform = platform_target()?; + let release = plugin + .platforms + .get(&platform) + .ok_or_else(|| InstallError::NoRelease { + plugin: plugin.name.clone(), + version: plugin.version.clone(), + platform: platform.clone(), + })?; + let sha256 = normalize_digest(&plugin.name, &plugin.version, &release.sha256)?; + validate_url(&release.url, &self.registry, false).map_err(|_| { + InstallError::UnsafeArtifactUrl { + plugin: plugin.name.clone(), + url: release.url.clone(), + } + })?; + + ensure_directory(&plugin.name, plugins_dir).await?; + let staging_root = plugins_dir.join(".staging"); + ensure_directory(&plugin.name, &staging_root).await?; + let unique = uuid::Uuid::new_v4().simple().to_string(); + let stage = staging_root.join(format!("{}-{unique}", plugin.name)); + create_unique_directory(&plugin.name, &stage).await?; + let staged_binary = stage.join(BINARY_FILE); + let plugin_root = plugins_dir.join(&plugin.name); + let directory = format!("{}-{unique}", plugin.version); + let version_dir = plugin_root.join(&directory); + let mut moved_to_version_dir = false; + let prepared = async { + self.download_binary(plugin, release, &sha256, &staged_binary) + .await?; + let receipt = InstallReceipt { + envelope: registry.envelope.clone(), + plugin_name: plugin.name.clone(), + version: plugin.version.clone(), + platform: platform.clone(), + sha256: sha256.clone(), + }; + write_json_synced(&plugin.name, &stage.join(RECEIPT_FILE), &receipt).await?; + sync_directory(&plugin.name, &stage).await?; + ensure_directory(&plugin.name, &plugin_root).await?; + // Install directories are immutable and unique. This means even a + // same-version reinstall cannot modify the directory referenced by + // the current active record before its candidate passes startup. + tokio::fs::rename(&stage, &version_dir) + .await + .map_err(|error| io_error(&plugin.name, &version_dir, error))?; + moved_to_version_dir = true; + sync_directory(&plugin.name, &plugin_root).await?; + + Ok(InstallCandidate { + name: plugin.name.clone(), + version: plugin.version.clone(), + platform, + sha256, + binary_path: version_dir.join(BINARY_FILE), + plugin_root, + install_directory: directory, + }) + } + .await; + + if let Err(primary) = prepared { + let cleanup_path = if moved_to_version_dir { + &version_dir + } else { + &stage + }; + if let Err(cleanup_error) = tokio::fs::remove_dir_all(cleanup_path).await { + if cleanup_error.kind() != std::io::ErrorKind::NotFound { + tracing::warn!( + plugin = %plugin.name, + path = %cleanup_path.display(), + error = %cleanup_error, + "Failed to remove incomplete plugin installation" + ); + } + } + return Err(primary); + } + + prepared + } + + /// Persist the signed global registry revision before any artifact from it + /// is downloaded or executed. Equal revisions are safe for multiple + /// installs; a lower revision is a replay/downgrade attempt. + pub async fn accept_registry_revision( + &self, + plugins_dir: &Path, + revision: u64, + ) -> Result<(), InstallError> { + ensure_directory("registry", plugins_dir).await?; + let state_path = plugins_dir.join(REGISTRY_STATE_FILE); + let highest = match tokio::fs::symlink_metadata(&state_path).await { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { + return Err(InstallError::Io { + plugin: "registry".to_string(), + path: state_path.display().to_string(), + reason: "registry state must be a regular file".to_string(), + }); + } + Ok(_) => { + let bytes = read_regular_file_capped("registry", &state_path, 16 * 1024).await?; + serde_json::from_slice::(&bytes) + .map_err(|error| InstallError::Io { + plugin: "registry".to_string(), + path: state_path.display().to_string(), + reason: format!("invalid registry state: {error}"), + })? + .highest_revision + } + Err(error) if error.kind() == std::io::ErrorKind::NotFound => 0, + Err(error) => return Err(io_error("registry", &state_path, error)), + }; + if revision < highest { + return Err(InstallError::RegistryRollback { + received: revision, + highest, + }); + } + if revision == highest { + return Ok(()); + } + write_json_atomically( + "registry", + &state_path, + &RegistryState { + highest_revision: revision, + }, + ) + .await?; + sync_directory("registry", plugins_dir).await + } + + pub async fn activate(&self, candidate: &InstallCandidate) -> Result<(), InstallError> { + write_json_atomically( + &candidate.name, + &candidate.plugin_root.join(ACTIVE_FILE), + &ActiveRecord { + version: candidate.version.clone(), + directory: candidate.install_directory.clone(), + }, + ) + .await?; + // The rename above is the commit point. A directory fsync failure + // means durability is uncertain, not that activation was rolled back; + // reporting an error here would make the caller kill the candidate + // even though active.json already points at it. + if let Err(error) = sync_directory(&candidate.name, &candidate.plugin_root).await { + tracing::warn!( + plugin = %candidate.name, + path = %candidate.plugin_root.display(), + error = %error, + "Plugin activation committed but directory fsync failed" + ); + } + Ok(()) + } + + /// Remove a prepared release that never reached the activation commit + /// point. The exact immutable directory is carried by the candidate, so + /// cleanup never derives a recursive target from caller input. + pub async fn discard(&self, candidate: &InstallCandidate) -> Result<(), InstallError> { + let directory = candidate.plugin_root.join(&candidate.install_directory); + tokio::fs::remove_dir_all(&directory) + .await + .map_err(|error| io_error(&candidate.name, &directory, error))?; + sync_directory(&candidate.name, &candidate.plugin_root).await + } + + async fn download_binary( + &self, + plugin: &RegistryPlugin, + release: &PlatformRelease, + expected: &str, + destination: &Path, + ) -> Result<(), InstallError> { + let response = self + .client + .get(&release.url) + .header("User-Agent", "temps-plugin-installer") + .send() + .await + .map_err(|error| InstallError::Download { + plugin: plugin.name.clone(), + url: release.url.clone(), + reason: error.to_string(), + })?; + if response.status().is_redirection() || !response.status().is_success() { + return Err(InstallError::DownloadStatus { + url: release.url.clone(), + status: response.status().as_u16(), + }); + } + if response + .content_length() + .is_some_and(|length| length > MAX_BINARY_BYTES) + { + return Err(InstallError::TooLarge { + url: release.url.clone(), + limit: MAX_BINARY_BYTES, + }); + } + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(destination) + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + let mut total = 0u64; + let mut hasher = Sha256::new(); + let mut stream = response.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|error| InstallError::Download { + plugin: plugin.name.clone(), + url: release.url.clone(), + reason: error.to_string(), + })?; + total = total.saturating_add(chunk.len() as u64); + if total > MAX_BINARY_BYTES { + return Err(InstallError::TooLarge { + url: release.url.clone(), + limit: MAX_BINARY_BYTES, + }); + } + hasher.update(&chunk); + file.write_all(&chunk) + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + } + file.flush() + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + file.sync_all() + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + file.set_permissions(std::fs::Permissions::from_mode(0o500)) + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + file.sync_all() + .await + .map_err(|error| io_error(&plugin.name, destination, error))?; + } + drop(file); + let actual = hex::encode(hasher.finalize()); + if actual != expected { + return Err(InstallError::DigestMismatch { + plugin: plugin.name.clone(), + version: plugin.version.clone(), + expected: expected.to_string(), + actual, + }); + } + Ok(()) + } +} + +pub async fn discover_active( + plugins_dir: &Path, + registry: &RegistryConfig, +) -> Vec> { + let mut results = Vec::new(); + let mut entries = match tokio::fs::read_dir(plugins_dir).await { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return results, + Err(error) => { + results.push(Err(io_error("registry", plugins_dir, error))); + return results; + } + }; + while let Ok(Some(entry)) = entries.next_entry().await { + let name = match entry.file_name().to_str() { + Some(name) if !name.starts_with('.') => name.to_string(), + _ => continue, + }; + let path = entry.path(); + let metadata = match tokio::fs::symlink_metadata(&path).await { + Ok(metadata) => metadata, + Err(error) => { + results.push(Err(io_error(&name, &path, error))); + continue; + } + }; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + continue; + } + results.push(verify_active(&name, &path, registry).await); + } + results +} + +async fn verify_active( + name: &str, + plugin_root: &Path, + registry: &RegistryConfig, +) -> Result { + validate_plugin_name(name)?; + let active_path = plugin_root.join(ACTIVE_FILE); + let active_bytes = read_regular_file_capped(name, &active_path, 16 * 1024) + .await + .map_err(|_| InstallError::MissingActiveRecord { + plugin: name.to_string(), + path: active_path.display().to_string(), + })?; + let active: ActiveRecord = + serde_json::from_slice(&active_bytes).map_err(|error| InstallError::InvalidReceipt { + plugin: name.to_string(), + path: active_path.display().to_string(), + reason: error.to_string(), + })?; + validate_version(name, &active.version)?; + validate_version(name, &active.directory)?; + let version_dir = plugin_root.join(&active.directory); + let receipt_path = version_dir.join(RECEIPT_FILE); + let receipt_bytes = read_regular_file_capped(name, &receipt_path, 2 * 1024 * 1024) + .await + .map_err(|error| invalid_receipt(name, &receipt_path, error.to_string()))?; + let receipt: InstallReceipt = serde_json::from_slice(&receipt_bytes) + .map_err(|error| invalid_receipt(name, &receipt_path, error.to_string()))?; + let current_platform = platform_target()?; + if receipt.platform != current_platform { + return Err(invalid_receipt( + name, + &receipt_path, + format!( + "receipt targets platform '{}', but this host is '{}'", + receipt.platform, current_platform + ), + )); + } + let verified = + crate::catalog::verify_envelope(receipt.envelope, ®istry.trust_anchors, ®istry.url) + .map_err(|error| invalid_receipt(name, &receipt_path, error.to_string()))?; + let plugin = verified + .document + .plugins + .iter() + .find(|plugin| plugin.name == name && plugin.version == active.version) + .ok_or_else(|| invalid_receipt(name, &receipt_path, "signed plugin/version missing"))?; + let release = plugin + .platforms + .get(&receipt.platform) + .ok_or_else(|| invalid_receipt(name, &receipt_path, "signed platform release missing"))?; + let signed_digest = normalize_digest(name, &active.version, &release.sha256)?; + if receipt.plugin_name != name + || receipt.version != active.version + || receipt.sha256 != signed_digest + { + return Err(invalid_receipt( + name, + &receipt_path, + "receipt fields do not match the signed release", + )); + } + let binary_path = version_dir.join(BINARY_FILE); + let actual = hash_regular_file_capped(name, &binary_path, MAX_BINARY_BYTES) + .await + .map_err(|error| invalid_receipt(name, &receipt_path, error.to_string()))?; + if actual != signed_digest { + return Err(InstallError::DigestMismatch { + plugin: name.to_string(), + version: active.version, + expected: signed_digest, + actual, + }); + } + Ok(ActiveInstallation { + name: name.to_string(), + version: receipt.version, + sha256: signed_digest, + binary_path, + }) +} + +pub(crate) async fn open_verified_executable( + plugin: &str, + plugins_dir: &Path, + path: &Path, + expected_sha256: &str, +) -> Result { + let mut file = open_executable_beneath(plugin, plugins_dir, path)?; + let metadata = file + .metadata() + .await + .map_err(|error| io_error(plugin, path, error))?; + if metadata.len() > MAX_BINARY_BYTES { + return Err(InstallError::TooLarge { + url: path.display().to_string(), + limit: MAX_BINARY_BYTES, + }); + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt as _; + if metadata.nlink() != 1 { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "verified executable must have exactly one hard link".to_string(), + }); + } + if metadata.uid() != unsafe { libc::geteuid() } || metadata.mode() & 0o777 != 0o500 { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "verified executable must be owned by the Temps user with mode 0500" + .to_string(), + }); + } + } + let actual = hash_open_file_capped(plugin, path, &mut file, MAX_BINARY_BYTES).await?; + if actual != expected_sha256 { + return Err(InstallError::DigestMismatch { + plugin: plugin.to_string(), + version: "active".to_string(), + expected: expected_sha256.to_string(), + actual, + }); + } + let mut file = file.into_std().await; + file.seek(SeekFrom::Start(0)) + .map_err(|error| io_error(plugin, path, error))?; + Ok(VerifiedExecutable { + file, + display_path: path.to_path_buf(), + }) +} + +#[cfg(unix)] +fn open_executable_beneath( + plugin: &str, + plugins_dir: &Path, + path: &Path, +) -> Result { + use std::os::fd::{AsRawFd as _, FromRawFd as _, OwnedFd}; + use std::os::unix::ffi::OsStrExt as _; + + let relative = path + .strip_prefix(plugins_dir) + .map_err(|_| InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: format!( + "executable must be beneath plugin root {}", + plugins_dir.display() + ), + })?; + let components: Vec<_> = relative.components().collect(); + if components.is_empty() + || components + .iter() + .any(|component| !matches!(component, std::path::Component::Normal(_))) + { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "executable path contains an unsafe component".to_string(), + }); + } + + let root = std::ffi::CString::new(plugins_dir.as_os_str().as_bytes()).map_err(|_| { + InstallError::Io { + plugin: plugin.to_string(), + path: plugins_dir.display().to_string(), + reason: "plugin root contains a NUL byte".to_string(), + } + })?; + // SAFETY: root is a valid C string and the returned descriptor is moved + // exactly once into OwnedFd. + let root_fd = unsafe { + libc::open( + root.as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if root_fd < 0 { + return Err(io_error( + plugin, + plugins_dir, + std::io::Error::last_os_error(), + )); + } + // SAFETY: root_fd was returned by open and is uniquely owned here. + let mut directory = unsafe { OwnedFd::from_raw_fd(root_fd) }; + + for component in &components[..components.len() - 1] { + let std::path::Component::Normal(component) = component else { + unreachable!("components were validated above") + }; + let component = + std::ffi::CString::new(component.as_bytes()).map_err(|_| InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "executable path contains a NUL byte".to_string(), + })?; + // SAFETY: both descriptors and the C string are live for the call. + let next = unsafe { + libc::openat( + directory.as_raw_fd(), + component.as_ptr(), + libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if next < 0 { + return Err(io_error(plugin, path, std::io::Error::last_os_error())); + } + // SAFETY: next was returned by openat and ownership moves once. + directory = unsafe { OwnedFd::from_raw_fd(next) }; + } + + let std::path::Component::Normal(file_name) = components[components.len() - 1] else { + unreachable!("components were validated above") + }; + let file_name = std::ffi::CString::new(file_name.as_bytes()).map_err(|_| InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "executable filename contains a NUL byte".to_string(), + })?; + // SAFETY: directory and file_name are valid and live for this call. + let file = unsafe { + libc::openat( + directory.as_raw_fd(), + file_name.as_ptr(), + libc::O_RDONLY | libc::O_NOFOLLOW | libc::O_CLOEXEC, + ) + }; + if file < 0 { + return Err(io_error(plugin, path, std::io::Error::last_os_error())); + } + // SAFETY: file was returned by openat and ownership moves into File once. + let file = unsafe { std::fs::File::from_raw_fd(file) }; + Ok(tokio::fs::File::from_std(file)) +} + +#[cfg(not(unix))] +fn open_executable_beneath( + plugin: &str, + plugins_dir: &Path, + path: &Path, +) -> Result { + if !path.starts_with(plugins_dir) { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: format!( + "executable must be beneath plugin root {}", + plugins_dir.display() + ), + }); + } + open_regular_file(plugin, path) +} + +pub fn platform_target_for(os: &str, arch: &str) -> Result { + let target = match (os, arch) { + ("macos", "x86_64") => "darwin-amd64", + ("macos", "aarch64") => "darwin-arm64", + ("linux", "x86_64") => "linux-amd64", + ("linux", "aarch64") => "linux-arm64", + _ => { + return Err(InstallError::UnsupportedPlatform { + os: os.to_string(), + arch: arch.to_string(), + }) + } + }; + Ok(target.to_string()) +} + +pub fn platform_target() -> Result { + platform_target_for(std::env::consts::OS, std::env::consts::ARCH) +} + +pub fn validate_plugin_name(name: &str) -> Result<(), InstallError> { + let valid = !name.is_empty() + && name.len() <= 64 + && !name.starts_with('-') + && !name.ends_with('-') + && name + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'); + if valid { + Ok(()) + } else { + Err(InstallError::UnsafePluginName { + name: name.to_string(), + }) + } +} + +pub(crate) fn validate_version(plugin: &str, version: &str) -> Result<(), InstallError> { + let valid = !version.is_empty() + && version.len() <= 64 + && !version.contains(['/', '\\']) + && version != "." + && version != ".." + && version + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'+')); + if valid { + Ok(()) + } else { + Err(InstallError::UnsafeVersion { + plugin: plugin.to_string(), + version: version.to_string(), + }) + } +} + +pub(crate) fn normalize_digest( + plugin: &str, + version: &str, + digest: &str, +) -> Result { + if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(InstallError::InvalidDigest { + plugin: plugin.to_string(), + version: version.to_string(), + digest: digest.to_string(), + }); + } + Ok(digest.to_ascii_lowercase()) +} + +async fn ensure_directory(plugin: &str, path: &Path) -> Result<(), InstallError> { + let result = match tokio::fs::symlink_metadata(path).await { + Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_dir() => { + Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "path must be a real directory, not a symlink or file".to_string(), + }) + } + Ok(_) => Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + tokio::fs::create_dir(path) + .await + .map_err(|error| io_error(plugin, path, error))?; + let metadata = tokio::fs::symlink_metadata(path) + .await + .map_err(|error| io_error(plugin, path, error))?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "created path is not a real directory".to_string(), + }); + } + Ok(()) + } + Err(error) => Err(io_error(plugin, path, error)), + }; + result?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)) + .await + .map_err(|error| io_error(plugin, path, error))?; + } + Ok(()) +} + +async fn create_unique_directory(plugin: &str, path: &Path) -> Result<(), InstallError> { + tokio::fs::create_dir(path) + .await + .map_err(|error| io_error(plugin, path, error)) +} + +async fn write_json_synced( + plugin: &str, + path: &Path, + value: &T, +) -> Result<(), InstallError> { + let bytes = serde_json::to_vec(value).map_err(|error| InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: format!("failed to encode JSON: {error}"), + })?; + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path) + .await + .map_err(|error| io_error(plugin, path, error))?; + file.write_all(&bytes) + .await + .map_err(|error| io_error(plugin, path, error))?; + file.sync_all() + .await + .map_err(|error| io_error(plugin, path, error)) +} + +async fn write_json_atomically( + plugin: &str, + destination: &Path, + value: &T, +) -> Result<(), InstallError> { + let parent = destination.parent().ok_or_else(|| InstallError::Io { + plugin: plugin.to_string(), + path: destination.display().to_string(), + reason: "destination has no parent".to_string(), + })?; + ensure_directory(plugin, parent).await?; + let temporary = parent.join(format!(".active-{}.tmp", uuid::Uuid::new_v4())); + write_json_synced(plugin, &temporary, value).await?; + tokio::fs::rename(&temporary, destination) + .await + .map_err(|error| io_error(plugin, destination, error)) +} + +async fn sync_directory(plugin: &str, path: &Path) -> Result<(), InstallError> { + let file = tokio::fs::File::open(path) + .await + .map_err(|error| io_error(plugin, path, error))?; + file.sync_all() + .await + .map_err(|error| io_error(plugin, path, error)) +} + +async fn read_regular_file_capped( + plugin: &str, + path: &Path, + limit: u64, +) -> Result, InstallError> { + let mut file = open_regular_file(plugin, path)?; + let metadata = file + .metadata() + .await + .map_err(|error| io_error(plugin, path, error))?; + if metadata.len() > limit { + return Err(InstallError::TooLarge { + url: path.display().to_string(), + limit, + }); + } + let mut bytes = Vec::with_capacity(metadata.len() as usize); + file.read_to_end(&mut bytes) + .await + .map_err(|error| io_error(plugin, path, error))?; + if bytes.len() as u64 > limit { + return Err(InstallError::TooLarge { + url: path.display().to_string(), + limit, + }); + } + Ok(bytes) +} + +async fn hash_regular_file_capped( + plugin: &str, + path: &Path, + limit: u64, +) -> Result { + let mut file = open_regular_file(plugin, path)?; + hash_open_file_capped(plugin, path, &mut file, limit).await +} + +async fn hash_open_file_capped( + plugin: &str, + path: &Path, + file: &mut tokio::fs::File, + limit: u64, +) -> Result { + let metadata = file + .metadata() + .await + .map_err(|error| io_error(plugin, path, error))?; + if metadata.len() > limit { + return Err(InstallError::TooLarge { + url: path.display().to_string(), + limit, + }); + } + let mut total = 0u64; + let mut hasher = Sha256::new(); + let mut buffer = [0u8; 64 * 1024]; + loop { + let read = file + .read(&mut buffer) + .await + .map_err(|error| io_error(plugin, path, error))?; + if read == 0 { + break; + } + total = total.saturating_add(read as u64); + if total > limit { + return Err(InstallError::TooLarge { + url: path.display().to_string(), + limit, + }); + } + hasher.update(&buffer[..read]); + } + Ok(hex::encode(hasher.finalize())) +} + +fn open_regular_file(plugin: &str, path: &Path) -> Result { + #[cfg(unix)] + let file = { + use std::os::unix::fs::OpenOptionsExt as _; + std::fs::OpenOptions::new() + .read(true) + .custom_flags(libc::O_NOFOLLOW) + .open(path) + }; + #[cfg(not(unix))] + let file = std::fs::OpenOptions::new().read(true).open(path); + + let file = file.map_err(|error| io_error(plugin, path, error))?; + let metadata = file + .metadata() + .map_err(|error| io_error(plugin, path, error))?; + if !metadata.is_file() { + return Err(InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: "path must be a regular file, not a symlink".to_string(), + }); + } + Ok(tokio::fs::File::from_std(file)) +} + +fn io_error(plugin: &str, path: &Path, error: std::io::Error) -> InstallError { + InstallError::Io { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: error.to_string(), + } +} + +fn invalid_receipt(plugin: &str, path: &Path, reason: impl Into) -> InstallError { + InstallError::InvalidReceipt { + plugin: plugin.to_string(), + path: path.display().to_string(), + reason: reason.into(), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use base64::Engine as _; + use ed25519_dalek::{Signer as _, SigningKey}; + use std::collections::BTreeMap; + + async fn serve_once(status: &str, headers: &[(&str, String)], body: Vec) -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test server"); + let address = listener.local_addr().expect("test server address"); + let status = status.to_string(); + let headers: Vec<(String, String)> = headers + .iter() + .map(|(name, value)| ((*name).to_string(), value.clone())) + .collect(); + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept request"); + let mut request = [0u8; 2048]; + let _ = stream.read(&mut request).await.expect("read request"); + let mut response = format!("HTTP/1.1 {status}\r\nConnection: close\r\n"); + for (name, value) in headers { + response.push_str(&format!("{name}: {value}\r\n")); + } + if !response.to_ascii_lowercase().contains("content-length:") { + response.push_str(&format!("Content-Length: {}\r\n", body.len())); + } + response.push_str("\r\n"); + stream + .write_all(response.as_bytes()) + .await + .expect("write response headers"); + stream.write_all(&body).await.expect("write response body"); + }); + format!("http://{address}/plugin") + } + + fn signed_registry(plugin: RegistryPlugin) -> (VerifiedRegistry, SigningKey) { + let signing = SigningKey::from_bytes(&[42; 32]); + let document = crate::catalog::RegistryDocument { + schema_version: 1, + revision: 1, + issued_at: chrono::Utc::now() - chrono::Duration::minutes(1), + expires_at: chrono::Utc::now() + chrono::Duration::hours(1), + plugins: vec![plugin], + }; + let payload = serde_json::to_vec(&document).expect("serialize registry"); + let envelope = RegistryEnvelope { + key_id: "test-key".to_string(), + payload: base64::engine::general_purpose::STANDARD.encode(&payload), + signature: base64::engine::general_purpose::STANDARD + .encode(signing.sign(&payload).to_bytes()), + }; + (VerifiedRegistry { envelope, document }, signing) + } + + fn plugin(url: String, bytes: &[u8], version: &str) -> RegistryPlugin { + RegistryPlugin { + name: "test-plugin".to_string(), + title: "Test plugin".to_string(), + summary: "test".to_string(), + description: "test".to_string(), + author: "Temps Contributors".to_string(), + category: "test".to_string(), + keywords: vec!["test".to_string()], + logo_url: None, + repository: None, + docs_url: None, + version: version.to_string(), + platforms: BTreeMap::from([( + platform_target().expect("supported test platform"), + PlatformRelease { + url, + sha256: hex::encode(Sha256::digest(bytes)), + }, + )]), + } + } + + fn installer(url: &str, signing: &SigningKey) -> (PluginInstaller, RegistryConfig) { + let config = RegistryConfig::local( + url.to_string(), + "test-key", + signing.verifying_key().to_bytes(), + ); + ( + PluginInstaller::new(config.clone()).expect("test installer"), + config, + ) + } + + #[test] + fn platform_selection_is_explicit() { + assert_eq!( + platform_target_for("linux", "x86_64").expect("linux"), + "linux-amd64" + ); + assert_eq!( + platform_target_for("macos", "aarch64").expect("mac"), + "darwin-arm64" + ); + assert!(matches!( + platform_target_for("windows", "x86_64"), + Err(InstallError::UnsupportedPlatform { .. }) + )); + } + + #[test] + fn unsafe_names_cannot_escape_plugin_root() { + for name in ["../escape", "a/b", "A", "-leading", "trailing-", ""] { + assert!(validate_plugin_name(name).is_err(), "accepted {name:?}"); + } + assert!(validate_plugin_name("analytics-2").is_ok()); + } + + #[test] + fn digest_accepts_uppercase_and_normalizes() { + let upper = "AB".repeat(32); + assert_eq!( + normalize_digest("p", "1", &upper).expect("digest"), + "ab".repeat(32) + ); + assert!(normalize_digest("p", "1", "abc").is_err()); + } + + #[tokio::test] + async fn registry_revision_is_persisted_and_cannot_roll_back() { + let signing = SigningKey::from_bytes(&[42; 32]); + let (installer, _) = installer("http://127.0.0.1/plugin", &signing); + let temp = tempfile::tempdir().expect("tempdir"); + installer + .accept_registry_revision(temp.path(), 7) + .await + .expect("accept revision"); + installer + .accept_registry_revision(temp.path(), 7) + .await + .expect("equal revision remains valid"); + assert!(matches!( + installer.accept_registry_revision(temp.path(), 6).await, + Err(InstallError::RegistryRollback { + received: 6, + highest: 7 + }) + )); + } + + #[tokio::test] + async fn successful_direct_binary_install_and_discovery() { + let bytes = b"standalone executable bytes"; + let url = serve_once("200 OK", &[], bytes.to_vec()).await; + let (registry, signing) = signed_registry(plugin(url.clone(), bytes, "1.2.3")); + let (installer, config) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let plugins_dir = temp.path().join("plugins"); + let candidate = installer + .prepare(&plugins_dir, ®istry, ®istry.document.plugins[0]) + .await + .expect("prepare binary"); + assert_eq!( + std::fs::read(&candidate.binary_path).expect("binary"), + bytes + ); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + let mode = std::fs::metadata(&candidate.binary_path) + .expect("metadata") + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o500); + } + installer.activate(&candidate).await.expect("activate"); + let active = discover_active(&plugins_dir, &config).await; + assert!(matches!(&active[..], [Ok(found)] if found.version == "1.2.3")); + } + + #[tokio::test] + async fn test_prepare_receipt_write_failure_removes_staging_directory() { + // Arrange: place a receipt in the newly-created staging directory + // while the installer is waiting for the artifact response. This + // forces the post-download create_new receipt write to fail. + let bytes = b"standalone executable bytes".to_vec(); + let temp = tempfile::tempdir().expect("tempdir"); + let plugins_dir = temp.path().join("plugins"); + let staging_root = plugins_dir.join(".staging"); + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind test server"); + let url = format!("http://{}/plugin", listener.local_addr().expect("address")); + let server_staging_root = staging_root.clone(); + let response_body = bytes.clone(); + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept request"); + let mut request = [0u8; 2048]; + let _ = stream.read(&mut request).await.expect("read request"); + let stage = std::fs::read_dir(&server_staging_root) + .expect("staging root") + .next() + .expect("staging child") + .expect("staging entry") + .path(); + std::fs::write(stage.join(RECEIPT_FILE), b"occupied").expect("occupy receipt path"); + let response = format!( + "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: {}\r\n\r\n", + response_body.len() + ); + stream + .write_all(response.as_bytes()) + .await + .expect("write response headers"); + stream + .write_all(&response_body) + .await + .expect("write response body"); + }); + let (registry, signing) = signed_registry(plugin(url.clone(), &bytes, "1.2.3")); + let (installer, _) = installer(&url, &signing); + + // Act + let result = installer + .prepare(&plugins_dir, ®istry, ®istry.document.plugins[0]) + .await; + server.await.expect("test server task"); + + // Assert + assert!(matches!(result, Err(InstallError::Io { .. }))); + let remaining = std::fs::read_dir(&staging_root) + .expect("staging root remains") + .count(); + assert_eq!(remaining, 0, "failed preparation must remove its stage"); + } + + #[tokio::test] + async fn test_discover_active_tampered_receipt_is_rejected() { + // Arrange + let bytes = b"standalone executable bytes"; + let url = serve_once("200 OK", &[], bytes.to_vec()).await; + let (registry, signing) = signed_registry(plugin(url.clone(), bytes, "1.2.3")); + let (installer, config) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let candidate = installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await + .expect("prepare binary"); + installer.activate(&candidate).await.expect("activate"); + let receipt_path = candidate + .plugin_root + .join(&candidate.install_directory) + .join(RECEIPT_FILE); + let mut receipt: serde_json::Value = + serde_json::from_slice(&std::fs::read(&receipt_path).expect("read original receipt")) + .expect("parse receipt"); + receipt["sha256"] = serde_json::Value::String("00".repeat(32)); + std::fs::write( + &receipt_path, + serde_json::to_vec(&receipt).expect("serialize tampered receipt"), + ) + .expect("tamper receipt"); + + // Act + let discovered = discover_active(temp.path(), &config).await; + + // Assert + assert!(matches!( + &discovered[..], + [Err(InstallError::InvalidReceipt { reason, .. })] + if reason.contains("do not match the signed release") + )); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_discover_active_tampered_binary_returns_digest_mismatch() { + use std::os::unix::fs::PermissionsExt as _; + + // Arrange + let bytes = b"standalone executable bytes"; + let url = serve_once("200 OK", &[], bytes.to_vec()).await; + let (registry, signing) = signed_registry(plugin(url.clone(), bytes, "1.2.3")); + let (installer, config) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let candidate = installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await + .expect("prepare binary"); + installer.activate(&candidate).await.expect("activate"); + std::fs::set_permissions( + &candidate.binary_path, + std::fs::Permissions::from_mode(0o700), + ) + .expect("make installed binary writable for tampering"); + std::fs::write(&candidate.binary_path, b"tampered executable") + .expect("tamper installed binary"); + + // Act + let discovered = discover_active(temp.path(), &config).await; + + // Assert + assert!(matches!( + &discovered[..], + [Err(InstallError::DigestMismatch { plugin, version, .. })] + if plugin == "test-plugin" && version == "1.2.3" + )); + } + + #[tokio::test] + async fn sha_mismatch_does_not_create_install() { + let bytes = b"tampered"; + let url = serve_once("200 OK", &[], bytes.to_vec()).await; + let release = plugin(url.clone(), b"expected", "2.0.0"); + let (registry, signing) = signed_registry(release); + let (installer, _) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let result = installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await; + assert!(matches!(result, Err(InstallError::DigestMismatch { .. }))); + } + + #[tokio::test] + async fn advertised_oversized_binary_is_rejected_before_streaming() { + let url = serve_once( + "200 OK", + &[("Content-Length", (MAX_BINARY_BYTES + 1).to_string())], + Vec::new(), + ) + .await; + let (registry, signing) = signed_registry(plugin(url.clone(), b"", "2.0.0")); + let (installer, _) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + assert!(matches!( + installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await, + Err(InstallError::TooLarge { .. }) + )); + } + + #[tokio::test] + async fn redirect_is_not_followed() { + let url = serve_once( + "302 Found", + &[("Location", "http://127.0.0.1:9/attacker".to_string())], + Vec::new(), + ) + .await; + let (registry, signing) = signed_registry(plugin(url.clone(), b"", "2.0.0")); + let (installer, _) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + assert!(matches!( + installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await, + Err(InstallError::DownloadStatus { status: 302, .. }) + )); + } + + #[tokio::test] + async fn same_version_failed_prepare_preserves_active_release() { + let v1 = b"healthy version"; + let url = serve_once("200 OK", &[], v1.to_vec()).await; + let (registry, signing) = signed_registry(plugin(url.clone(), v1, "1.0.0")); + let (installer, config) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let candidate = installer + .prepare(temp.path(), ®istry, ®istry.document.plugins[0]) + .await + .expect("prepare healthy release"); + installer + .activate(&candidate) + .await + .expect("activate healthy release"); + let active_before = + std::fs::read(temp.path().join("test-plugin/active.json")).expect("active record"); + + let bad_url = serve_once("200 OK", &[], b"corrupt".to_vec()).await; + let (bad_registry, _) = signed_registry(plugin(bad_url, v1, "1.0.0")); + assert!(matches!( + installer + .prepare( + temp.path(), + &bad_registry, + &bad_registry.document.plugins[0] + ) + .await, + Err(InstallError::DigestMismatch { .. }) + )); + assert_eq!( + std::fs::read(temp.path().join("test-plugin/active.json")).expect("active record"), + active_before + ); + let active = discover_active(temp.path(), &config).await; + assert!( + matches!(&active[..], [Ok(found)] if std::fs::read(&found.binary_path).expect("binary") == v1) + ); + } + + #[tokio::test] + #[cfg(unix)] + async fn staging_symlink_is_rejected() { + use std::os::unix::fs::symlink; + let bytes = b"binary"; + let url = serve_once("200 OK", &[], bytes.to_vec()).await; + let (registry, signing) = signed_registry(plugin(url.clone(), bytes, "1.0.0")); + let (installer, _) = installer(&url, &signing); + let temp = tempfile::tempdir().expect("tempdir"); + let plugins = temp.path().join("plugins"); + std::fs::create_dir(&plugins).expect("plugins directory"); + symlink(temp.path(), plugins.join(".staging")).expect("staging symlink"); + assert!(matches!( + installer + .prepare(&plugins, ®istry, ®istry.document.plugins[0]) + .await, + Err(InstallError::Io { .. }) + )); + } + + #[tokio::test] + #[cfg(target_os = "linux")] + async fn verified_descriptor_is_executed_even_if_path_is_replaced() { + use std::os::unix::fs::PermissionsExt as _; + + let temp = tempfile::tempdir().expect("tempdir"); + let plugins = temp.path().join("plugins"); + let version = plugins.join("safe/1.0.0-test"); + std::fs::create_dir_all(&version).expect("plugin directories"); + let binary = version.join(BINARY_FILE); + let trusted = b"#!/bin/sh\nprintf trusted"; + std::fs::write(&binary, trusted).expect("trusted binary"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o500)) + .expect("trusted permissions"); + let digest = hex::encode(Sha256::digest(trusted)); + let executable = open_verified_executable("safe", &plugins, &binary, &digest) + .await + .expect("verified executable"); + let command_path = executable.command_path("safe").expect("descriptor path"); + + std::fs::rename(&binary, version.join("replaced")).expect("replace trusted pathname"); + std::fs::write(&binary, b"#!/bin/sh\nprintf attacker").expect("replacement binary"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o500)) + .expect("replacement permissions"); + + let output = std::process::Command::new(command_path) + .output() + .expect("execute verified descriptor"); + assert!(output.status.success()); + assert_eq!(output.stdout, b"trusted"); + drop(executable); + } + + #[tokio::test] + #[cfg(unix)] + async fn executable_hardlinks_are_rejected() { + use std::os::unix::fs::PermissionsExt as _; + + let temp = tempfile::tempdir().expect("tempdir"); + let plugins = temp.path().join("plugins"); + let version = plugins.join("safe/1.0.0-test"); + std::fs::create_dir_all(&version).expect("plugin directories"); + let binary = version.join(BINARY_FILE); + let bytes = b"binary"; + std::fs::write(&binary, bytes).expect("binary"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o500)) + .expect("binary permissions"); + std::fs::hard_link(&binary, version.join("second-link")).expect("hardlink"); + let digest = hex::encode(Sha256::digest(bytes)); + + assert!(matches!( + open_verified_executable("safe", &plugins, &binary, &digest).await, + Err(InstallError::Io { .. }) + )); + } + + #[tokio::test] + #[cfg(unix)] + async fn executable_ancestor_symlinks_are_rejected() { + use std::os::unix::fs::{symlink, PermissionsExt as _}; + + let temp = tempfile::tempdir().expect("tempdir"); + let plugins = temp.path().join("plugins"); + let real = plugins.join("real"); + std::fs::create_dir_all(&real).expect("real directory"); + let binary = real.join(BINARY_FILE); + let bytes = b"binary"; + std::fs::write(&binary, bytes).expect("binary"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o500)) + .expect("binary permissions"); + symlink(&real, plugins.join("alias")).expect("ancestor symlink"); + let digest = hex::encode(Sha256::digest(bytes)); + + assert!(matches!( + open_verified_executable("safe", &plugins, &plugins.join("alias/plugin"), &digest) + .await, + Err(InstallError::Io { .. }) + )); + } +} diff --git a/crates/temps-external-plugins/src/lib.rs b/crates/temps-external-plugins/src/lib.rs index 69e9943c2..866e3552c 100644 --- a/crates/temps-external-plugins/src/lib.rs +++ b/crates/temps-external-plugins/src/lib.rs @@ -12,10 +12,12 @@ //! - Event delivery: forwarding platform events to subscribing plugins //! - API: listing plugin manifests via REST endpoint +pub mod catalog; pub mod channel; pub mod event_listener; pub mod handler; pub mod host_api; +pub mod install; pub mod manager; pub mod plugin; pub mod proxy; diff --git a/crates/temps-external-plugins/src/manager.rs b/crates/temps-external-plugins/src/manager.rs index 697fa0d41..0b686f942 100644 --- a/crates/temps-external-plugins/src/manager.rs +++ b/crates/temps-external-plugins/src/manager.rs @@ -8,13 +8,14 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; use temps_core::external_plugin::{ HandshakeMessage, PluginLaunchConfig, PluginManifest, EXTERNAL_PLUGIN_PROTOCOL_VERSION, }; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; use tokio::process::{Child, Command}; use tokio::sync::RwLock; use tracing::{debug, error, info, warn}; @@ -23,6 +24,7 @@ use sea_orm::DatabaseConnection; use utoipa::openapi::OpenApi; use crate::channel::PluginChannel; +use crate::install::open_verified_executable; use crate::proxy::PluginProxy; /// State of a single external plugin process. @@ -31,6 +33,8 @@ pub struct ExternalPluginProcess { pub manifest: PluginManifest, /// Path to the plugin binary pub binary_path: PathBuf, + /// Signed digest verified against the descriptor used for execution. + pub sha256: String, /// Unix socket path for communication pub socket_path: PathBuf, /// Per-process secret used to bind internal requests to this staged @@ -48,6 +52,25 @@ pub struct ExternalPluginProcess { pub openapi_schema: Option, } +/// A candidate that completed the complete protocol handshake but has not yet +/// replaced the healthy active process. +pub(crate) struct PendingPlugin { + expected_name: String, + process: ExternalPluginProcess, +} + +#[derive(Debug, Clone)] +pub struct PluginLoadFailure { + pub plugin: Option, + pub reason: String, +} + +#[derive(Debug, Clone, Default)] +pub struct PluginReloadResult { + pub manifests: Vec, + pub failures: Vec, +} + impl ExternalPluginProcess { /// Send SIGKILL to the plugin process (non-blocking). /// @@ -117,6 +140,8 @@ pub struct ExternalPluginConfig { pub handshake_timeout: Duration, /// Timeout for health check (default: 5s) pub health_check_timeout: Duration, + /// Signed registry trust and network policy used for installed receipts. + pub registry: crate::catalog::RegistryConfig, } /// Maximum length of a Unix socket path on this platform. @@ -125,6 +150,7 @@ pub struct ExternalPluginConfig { const SUN_PATH_MAX: usize = 104; #[cfg(not(target_os = "macos"))] const SUN_PATH_MAX: usize = 108; +const MAX_HANDSHAKE_FRAME_BYTES: usize = 2 * 1024 * 1024; fn generate_plugin_auth_secret() -> String { uuid::Uuid::new_v4().to_string() @@ -136,6 +162,58 @@ fn legacy_startup_eof_error(binary_name: &str) -> String { ) } +fn scrub_plugin_environment(command: &mut Command) { + command.env_clear(); +} + +fn plugin_stderr_context(observed_bytes: usize) -> String { + if observed_bytes == 0 { + String::new() + } else { + format!( + "\nPlugin emitted diagnostic output ({observed_bytes} bytes withheld to prevent secret disclosure)" + ) + } +} + +async fn read_handshake_frame( + reader: &mut R, + binary_name: &str, + phase: &str, +) -> Result, String> { + let mut bytes = Vec::new(); + loop { + let available = reader + .fill_buf() + .await + .map_err(|error| format!("Failed to read {phase} from {binary_name}: {error}"))?; + if available.is_empty() { + if bytes.is_empty() { + return Ok(None); + } + break; + } + let newline = available.iter().position(|byte| *byte == b'\n'); + let consumed = newline.map_or(available.len(), |index| index + 1); + if bytes.len().saturating_add(consumed) > MAX_HANDSHAKE_FRAME_BYTES { + return Err(format!( + "Plugin {binary_name} {phase} exceeded the {MAX_HANDSHAKE_FRAME_BYTES}-byte limit" + )); + } + bytes.extend_from_slice(&available[..consumed]); + reader.consume(consumed); + if newline.is_some() { + break; + } + } + while matches!(bytes.last(), Some(b'\n' | b'\r')) { + bytes.pop(); + } + String::from_utf8(bytes) + .map(Some) + .map_err(|error| format!("Plugin {binary_name} sent non-UTF-8 {phase}: {error}")) +} + #[cfg(unix)] fn secure_socket_directory(path: &Path) -> Result<(), std::io::Error> { use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; @@ -207,9 +285,17 @@ impl ExternalPluginConfig { database_url, handshake_timeout: Duration::from_secs(30), health_check_timeout: Duration::from_secs(5), + registry: crate::catalog::RegistryConfig::default(), } } + /// Inject an authenticated registry configuration. The default has no + /// trust anchors and therefore refuses registry installs and startup. + pub fn with_registry(mut self, registry: crate::catalog::RegistryConfig) -> Self { + self.registry = registry; + self + } + /// Record where the proxy listens, so plugins can be told their own /// externally-reachable base URL. /// @@ -289,13 +375,23 @@ impl ExternalPluginManager { /// /// Returns the list of successfully started plugin manifests. pub async fn discover_and_start(&self) -> Vec { + self.discover_and_start_report().await.manifests + } + + async fn discover_and_start_report(&self) -> PluginReloadResult { #[cfg(unix)] if let Err(error) = secure_socket_directory(&self.config.sockets_dir) { error!( directory = %self.config.sockets_dir.display(), "Failed to create a secure 0700 plugin socket directory: {error}" ); - return Vec::new(); + return PluginReloadResult { + manifests: Vec::new(), + failures: vec![PluginLoadFailure { + plugin: None, + reason: "Plugin socket directory could not be prepared".to_string(), + }], + }; } #[cfg(not(unix))] if let Err(error) = tokio::fs::create_dir_all(&self.config.sockets_dir).await { @@ -303,7 +399,13 @@ impl ExternalPluginManager { directory = %self.config.sockets_dir.display(), "Failed to create plugin socket directory: {error}" ); - return Vec::new(); + return PluginReloadResult { + manifests: Vec::new(), + failures: vec![PluginLoadFailure { + plugin: None, + reason: "Plugin socket directory could not be prepared".to_string(), + }], + }; } for dir in [ @@ -314,27 +416,31 @@ impl ExternalPluginManager { ] { if let Err(e) = tokio::fs::create_dir_all(dir).await { error!("Failed to create directory {}: {}", dir.display(), e); - return Vec::new(); + return PluginReloadResult { + manifests: Vec::new(), + failures: vec![PluginLoadFailure { + plugin: None, + reason: "A required plugin runtime directory could not be prepared" + .to_string(), + }], + }; } } // Kill any stale plugin processes left over from a previous run // (e.g. if the server was killed without graceful shutdown). self.kill_stale_processes().await; - let binaries = match self.scan_plugins_dir().await { - Ok(bins) => bins, - Err(e) => { - error!("Failed to scan plugins directory: {}", e); - return Vec::new(); - } - }; + let (binaries, mut failures) = self.scan_plugins_dir().await; if binaries.is_empty() { debug!( "No external plugins found in {}", self.config.plugins_dir.display() ); - return Vec::new(); + return PluginReloadResult { + manifests: Vec::new(), + failures, + }; } info!( @@ -345,8 +451,16 @@ impl ExternalPluginManager { let mut manifests = Vec::new(); - for binary_path in binaries { - match self.start_plugin(&binary_path).await { + for installation in binaries { + match self + .start_plugin( + &installation.name, + &installation.version, + &installation.sha256, + &installation.binary_path, + ) + .await + { Ok(manifest) => { info!( plugin = %manifest.name, @@ -357,60 +471,59 @@ impl ExternalPluginManager { } Err(e) => { error!( - binary = %binary_path.display(), + binary = %installation.binary_path.display(), "Failed to start external plugin: {}", e ); + failures.push(PluginLoadFailure { + plugin: Some(installation.name), + reason: "Plugin failed startup verification".to_string(), + }); } } } - manifests + PluginReloadResult { + manifests, + failures, + } } - /// Scan the plugins directory for executable binaries. - async fn scan_plugins_dir(&self) -> Result, std::io::Error> { + /// Discover only activated binaries whose local receipt still verifies + /// against a trusted signed registry document. Flat executable files are + /// intentionally ignored: dropping a file into this directory must never + /// turn it into code executed by the Temps server. + async fn scan_plugins_dir( + &self, + ) -> ( + Vec, + Vec, + ) { let mut binaries = Vec::new(); - let mut entries = tokio::fs::read_dir(&self.config.plugins_dir).await?; - - while let Some(entry) = entries.next_entry().await? { - let path = entry.path(); - - if path.is_dir() { - continue; - } - if path - .file_name() - .and_then(|n| n.to_str()) - .is_some_and(|n| n.starts_with('.')) - { - continue; - } - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Ok(metadata) = path.metadata() { - if metadata.permissions().mode() & 0o111 != 0 { - binaries.push(path); - } + let mut failures = Vec::new(); + for result in + crate::install::discover_active(&self.config.plugins_dir, &self.config.registry).await + { + match result { + Ok(installation) => binaries.push(installation), + Err(error) => { + warn!(error = %error, "Ignoring unverified external plugin install"); + failures.push(PluginLoadFailure { + plugin: None, + reason: "Activated plugin installation failed verification".to_string(), + }); } } - - #[cfg(not(unix))] - { - binaries.push(path); - } } - - binaries.sort(); - Ok(binaries) + binaries.sort_by(|left, right| left.name.cmp(&right.name)); + (binaries, failures) } - /// Kill stale plugin processes left over from a previous run. + /// Remove stale plugin bookkeeping left over from a previous run. /// - /// Reads PID files from the pids directory, checks whether each process - /// is still alive, and kills it if so. All PID files are removed - /// regardless of whether the process was still running. + /// A persisted numeric PID is not process identity: it may have been + /// reused by the operating system. Therefore startup never signals a + /// process based on this file alone; it only removes the stale record and + /// socket pathname. async fn kill_stale_processes(&self) { let mut entries = match tokio::fs::read_dir(&self.config.pids_dir).await { Ok(entries) => entries, @@ -432,58 +545,8 @@ impl ExternalPluginManager { _ => continue, }; - let pid_str = match tokio::fs::read_to_string(&path).await { - Ok(s) => s.trim().to_string(), - Err(e) => { - warn!("Failed to read PID file {}: {}", path.display(), e); - let _ = tokio::fs::remove_file(&path).await; - continue; - } - }; - - let pid: u32 = match pid_str.parse() { - Ok(p) => p, - Err(_) => { - warn!("Invalid PID in file {}: {:?}", path.display(), pid_str); - let _ = tokio::fs::remove_file(&path).await; - continue; - } - }; - let plugin_name = filename.trim_end_matches(".pid"); - // Check if the process is still alive and kill it - #[cfg(unix)] - { - // SAFETY: libc::kill with signal 0 is a standard POSIX - // existence check that does not affect the target process. - let exists = unsafe { libc::kill(pid as i32, 0) } == 0; - - if exists { - info!( - plugin = %plugin_name, - pid = pid, - "Killing stale plugin process from previous run" - ); - // SAFETY: SIGKILL is always safe to send to a known PID. - let ret = unsafe { libc::kill(pid as i32, libc::SIGKILL) }; - if ret != 0 { - let err = std::io::Error::last_os_error(); - warn!( - plugin = %plugin_name, - pid = pid, - "Failed to kill stale process: {}", err - ); - } - } else { - debug!( - plugin = %plugin_name, - pid = pid, - "Stale PID file for already-exited process" - ); - } - } - // Remove stale PID file let _ = tokio::fs::remove_file(&path).await; @@ -503,8 +566,17 @@ impl ExternalPluginManager { } } - /// Start a single plugin binary and complete the handshake. - async fn start_plugin(&self, binary_path: &Path) -> Result { + /// Spawn a single plugin binary and complete the handshake without adding + /// it to the active process table. + async fn spawn_plugin( + &self, + binary_path: &Path, + expected_sha256: &str, + expected_name: &str, + expected_version: &str, + instance_name: &str, + data_name: &str, + ) -> Result { let binary_name = binary_path .file_name() .and_then(|n| n.to_str()) @@ -513,8 +585,8 @@ impl ExternalPluginManager { let socket_path = self .config .sockets_dir - .join(format!("{}.sock", binary_name)); - let plugin_data_dir = self.config.data_dir.join(binary_name); + .join(format!("{}.sock", instance_name)); + let plugin_data_dir = self.config.data_dir.join(data_name); // This authenticates traffic as belonging to the staged child process. // Installed plugins are trusted host code under the current shared-UID // architecture; this is protocol integrity, not a sandbox boundary. @@ -538,10 +610,25 @@ impl ExternalPluginManager { debug!(binary = %binary_name, "Spawning external plugin"); - let pid_file_path = self.config.pids_dir.join(format!("{}.pid", binary_name)); + let pid_file_path = self.config.pids_dir.join(format!("{}.pid", instance_name)); - let mut command = Command::new(binary_path); + let executable = open_verified_executable( + binary_name, + &self.config.plugins_dir, + binary_path, + expected_sha256, + ) + .await + .map_err(|error| error.to_string())?; + let command_path = executable + .command_path(binary_name) + .map_err(|error| error.to_string())?; + let mut command = Command::new(command_path); + scrub_plugin_environment(&mut command); command + // Registry plugins are host code, but they do not need the Temps + // process's secrets. Required non-secret paths and URLs are + // passed explicitly below. .kill_on_drop(true) .arg("--socket-path") .arg(socket_path.to_str().unwrap_or_default()) @@ -559,6 +646,9 @@ impl ExternalPluginManager { .stderr(std::process::Stdio::piped()) .spawn() .map_err(|e| format!("Failed to spawn {}: {}", binary_name, e))?; + // Keep the verified executable descriptor alive until spawn has + // completed and the child has inherited it. + drop(executable); let mut child_stdin = child .stdin @@ -567,7 +657,23 @@ impl ExternalPluginManager { // Write PID file so we can clean up stale processes on restart if let Some(pid) = child.id() { - if let Err(e) = tokio::fs::write(&pid_file_path, pid.to_string()).await { + let pid_write = async { + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&pid_file_path) + .await?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + file.set_permissions(std::fs::Permissions::from_mode(0o600)) + .await?; + } + file.write_all(pid.to_string().as_bytes()).await?; + file.sync_all().await + } + .await; + if let Err(e) = pid_write { warn!( binary = %binary_name, "Failed to write PID file {}: {}", @@ -582,52 +688,40 @@ impl ExternalPluginManager { .take() .ok_or_else(|| format!("No stdout from {}", binary_name))?; - // Capture stderr in a background task so we can surface plugin errors - // even when the handshake fails (the plugin logs to stderr in JSON). - let stderr_lines: Arc>> = - Arc::new(tokio::sync::Mutex::new(Vec::new())); - let stderr_task = if let Some(stderr) = child.stderr.take() { - let lines_ref = stderr_lines.clone(); - let name = binary_name.to_string(); + // Drain stderr so a noisy child cannot block, but never place + // plugin-controlled output in logs or HTTP errors. The process may + // receive database credentials after its identity handshake, so even + // apparently harmless diagnostics must be treated as secret-bearing. + let stderr_bytes = Arc::new(AtomicUsize::new(0)); + let stderr_task = if let Some(mut stderr) = child.stderr.take() { + let observed = stderr_bytes.clone(); Some(tokio::spawn(async move { - let mut reader = BufReader::new(stderr).lines(); - while let Ok(Some(line)) = reader.next_line().await { - debug!(plugin = %name, "[plugin stderr] {}", line); - let mut buf = lines_ref.lock().await; - // Keep last 20 lines to avoid unbounded growth - if buf.len() >= 20 { - buf.remove(0); + let mut buffer = [0u8; 4096]; + loop { + match stderr.read(&mut buffer).await { + Ok(0) | Err(_) => break, + Ok(read) => { + let _ = observed.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |current| Some(current.saturating_add(read)), + ); + } } - buf.push(line); } })) } else { None }; - let mut reader = BufReader::new(stdout).lines(); - - // Helper: collect recent stderr lines into a single string for error context. - let collect_stderr = |lines: &Arc>>| { - let lines = lines.clone(); - async move { - let buf = lines.lock().await; - if buf.is_empty() { - String::new() - } else { - format!("\nPlugin stderr:\n {}", buf.join("\n ")) - } - } - }; + let mut reader = BufReader::new(stdout); // Stage 1: the already-running child identifies its protocol and // declares which host values it needs. Nothing sensitive or privileged // has been passed to it in argv. let manifest = match tokio::time::timeout(self.config.handshake_timeout, async { - let line = reader - .next_line() - .await - .map_err(|e| format!("Failed to read manifest from {}: {}", binary_name, e))? + let line = read_handshake_frame(&mut reader, binary_name, "startup hello") + .await? .ok_or_else(|| legacy_startup_eof_error(binary_name))?; let msg: HandshakeMessage = serde_json::from_str(&line) @@ -657,7 +751,7 @@ impl ExternalPluginManager { Ok(Err(e)) => { // Give stderr a moment to flush tokio::time::sleep(Duration::from_millis(100)).await; - let stderr_context = collect_stderr(&stderr_lines).await; + let stderr_context = plugin_stderr_context(stderr_bytes.load(Ordering::Relaxed)); if let Some(task) = stderr_task { task.abort(); } @@ -665,7 +759,7 @@ impl ExternalPluginManager { } Err(_) => { tokio::time::sleep(Duration::from_millis(100)).await; - let stderr_context = collect_stderr(&stderr_lines).await; + let stderr_context = plugin_stderr_context(stderr_bytes.load(Ordering::Relaxed)); if let Some(task) = stderr_task { task.abort(); } @@ -675,6 +769,21 @@ impl ExternalPluginManager { } }; + // Verify signed identity before disclosing the database URL or host + // data directory through the second handshake frame. + if manifest.name != expected_name || manifest.version != expected_version { + let declared_name = manifest.name.clone(); + let declared_version = manifest.version.clone(); + let _ = child.start_kill(); + if let Some(task) = stderr_task { + task.abort(); + } + return Err(format!( + "Plugin binary {} declares {declared_name} v{declared_version}, but signed installation expects {expected_name} v{expected_version}", + binary_path.display() + )); + } + debug!(plugin = %manifest.name, "Received manifest from plugin"); // Stage 2: send one typed line to this same child. The manifest flags @@ -717,10 +826,8 @@ impl ExternalPluginManager { // Read ready signal (handshake phase 2) let (has_ui, openapi_schema) = match tokio::time::timeout(self.config.handshake_timeout, async { - let line = reader - .next_line() - .await - .map_err(|e| format!("Failed to read ready signal from {}: {}", binary_name, e))? + let line = read_handshake_frame(&mut reader, binary_name, "ready signal") + .await? .ok_or_else(|| { format!( "Plugin {} closed stdout before sending ready signal", @@ -770,7 +877,7 @@ impl ExternalPluginManager { Ok(Ok(v)) => v, Ok(Err(e)) => { tokio::time::sleep(Duration::from_millis(100)).await; - let stderr_context = collect_stderr(&stderr_lines).await; + let stderr_context = plugin_stderr_context(stderr_bytes.load(Ordering::Relaxed)); if let Some(task) = stderr_task { task.abort(); } @@ -778,7 +885,7 @@ impl ExternalPluginManager { } Err(_) => { tokio::time::sleep(Duration::from_millis(100)).await; - let stderr_context = collect_stderr(&stderr_lines).await; + let stderr_context = plugin_stderr_context(stderr_bytes.load(Ordering::Relaxed)); if let Some(task) = stderr_task { task.abort(); } @@ -800,8 +907,6 @@ impl ExternalPluginManager { ); } - let result_manifest = manifest.clone(); - // Open the platform channel (WebSocket to plugin for queries + events). // This is non-fatal: older plugins that don't serve /_temps/channel // will simply not get a channel (they can still use POST /_events). @@ -824,9 +929,10 @@ impl ExternalPluginManager { } }; - let process = ExternalPluginProcess { + Ok(ExternalPluginProcess { manifest, binary_path: binary_path.to_path_buf(), + sha256: expected_sha256.to_string(), socket_path, auth_secret, pid_file_path, @@ -834,14 +940,82 @@ impl ExternalPluginManager { has_ui, channel: Some(channel), openapi_schema, - }; + }) + } - self.plugins + /// Start a verified active install and register it under its signed name. + async fn start_plugin( + &self, + expected_name: &str, + expected_version: &str, + expected_sha256: &str, + binary_path: &Path, + ) -> Result { + let process = self + .spawn_plugin( + binary_path, + expected_sha256, + expected_name, + expected_version, + expected_name, + expected_name, + ) + .await?; + let manifest = process.manifest.clone(); + if let Some(mut replaced) = self + .plugins .write() .await - .insert(result_manifest.name.clone(), process); + .insert(expected_name.to_string(), process) + { + replaced.shutdown().await; + } + Ok(manifest) + } - Ok(result_manifest) + /// Run a candidate through the full startup protocol while leaving the + /// current active process untouched. + pub(crate) async fn prepare_candidate( + &self, + expected_name: &str, + expected_version: &str, + expected_sha256: &str, + binary_path: &Path, + ) -> Result { + let suffix = uuid::Uuid::new_v4().simple().to_string(); + let instance_name = format!("candidate-{}", &suffix[..16]); + let process = self + .spawn_plugin( + binary_path, + expected_sha256, + expected_name, + expected_version, + &instance_name, + expected_name, + ) + .await?; + Ok(PendingPlugin { + expected_name: expected_name.to_string(), + process, + }) + } + + /// Atomically swap the process table entry, then stop the old process. + pub(crate) async fn promote_candidate(&self, pending: PendingPlugin) -> PluginManifest { + let manifest = pending.process.manifest.clone(); + let old = self + .plugins + .write() + .await + .insert(pending.expected_name, pending.process); + if let Some(mut old) = old { + old.shutdown().await; + } + manifest + } + + pub(crate) async fn discard_candidate(&self, mut pending: PendingPlugin) { + pending.process.shutdown().await; } /// Get all running plugin manifests. @@ -957,14 +1131,14 @@ impl ExternalPluginManager { /// and start everything fresh. /// /// Returns the manifests of all successfully started plugins. - pub async fn reload_all(&self) -> Vec { + pub async fn reload_all(&self) -> PluginReloadResult { info!("Reloading all external plugins"); // Phase 1: Shut down all running plugins self.shutdown_all().await; // Phase 2: Re-discover and start - self.discover_and_start().await + self.discover_and_start_report().await } /// Reload a single plugin by name: shut it down (if running), then @@ -973,10 +1147,14 @@ impl ExternalPluginManager { /// Returns the new manifest on success, or an error string on failure. pub async fn reload_plugin(&self, plugin_name: &str) -> Result { // Find the binary path before shutting down - let binary_path = { + let (binary_path, version, sha256) = { let plugins = self.plugins.read().await; match plugins.get(plugin_name) { - Some(process) => process.binary_path.clone(), + Some(process) => ( + process.binary_path.clone(), + process.manifest.version.clone(), + process.sha256.clone(), + ), None => { return Err(format!( "Plugin '{}' is not running; cannot reload", @@ -992,7 +1170,8 @@ impl ExternalPluginManager { self.shutdown_plugin(plugin_name).await; // Phase 2: Re-start - self.start_plugin(&binary_path).await + self.start_plugin(plugin_name, &version, &sha256, &binary_path) + .await } /// Get the config. @@ -1004,12 +1183,44 @@ impl ExternalPluginManager { #[cfg(test)] mod tests { use super::*; + use sha2::{Digest as _, Sha256}; /// Create a mock database connection for tests. fn mock_db() -> Arc { Arc::new(sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection()) } + #[cfg(unix)] + fn sleeping_process(root: &Path, name: &str, version: &str) -> (ExternalPluginProcess, u32) { + let child = tokio::process::Command::new("sleep") + .arg("300") + .spawn() + .expect("spawn fixture process"); + let pid = child.id().expect("fixture process ID"); + ( + ExternalPluginProcess { + manifest: PluginManifest::builder(name, version).build(), + binary_path: root.join(format!("{name}-{version}")), + sha256: "00".repeat(32), + socket_path: root.join(format!("{name}-{version}.sock")), + auth_secret: "fixture-auth".to_string(), + pid_file_path: root.join(format!("{name}-{version}.pid")), + child, + has_ui: false, + channel: None, + openapi_schema: None, + }, + pid, + ) + } + + #[cfg(unix)] + fn process_is_alive(pid: u32) -> bool { + // SAFETY: signal 0 only checks whether this PID is visible; it does + // not signal or otherwise modify the process. + unsafe { libc::kill(pid as i32, 0) == 0 } + } + #[tokio::test] async fn test_manager_creation() { let config = ExternalPluginConfig::new( @@ -1031,14 +1242,209 @@ mod tests { assert!(!second.is_empty()); } + #[tokio::test] + async fn test_read_handshake_frame_partial_eof_returns_complete_frame() { + // Arrange + let mut reader = BufReader::new(&b"{\"type\":\"hello\"}"[..]); + + // Act + let frame = read_handshake_frame(&mut reader, "fixture", "startup hello") + .await + .expect("partial final frame should be readable"); + + // Assert + assert_eq!(frame.as_deref(), Some("{\"type\":\"hello\"}")); + } + + #[tokio::test] + async fn test_read_handshake_frame_crlf_strips_line_terminator() { + // Arrange + let mut reader = BufReader::new(&b"{\"type\":\"ready\"}\r\nsecond\n"[..]); + + // Act + let first = read_handshake_frame(&mut reader, "fixture", "ready signal") + .await + .expect("CRLF frame should be readable"); + let second = read_handshake_frame(&mut reader, "fixture", "next frame") + .await + .expect("reader must retain bytes after the first newline"); + + // Assert + assert_eq!(first.as_deref(), Some("{\"type\":\"ready\"}")); + assert_eq!(second.as_deref(), Some("second")); + } + + #[tokio::test] + async fn test_read_handshake_frame_over_limit_returns_bounded_error() { + // Arrange: the newline itself counts toward the protocol frame limit. + let mut bytes = vec![b'a'; MAX_HANDSHAKE_FRAME_BYTES]; + bytes.push(b'\n'); + let mut reader = BufReader::new(bytes.as_slice()); + + // Act + let error = read_handshake_frame(&mut reader, "oversized-plugin", "startup hello") + .await + .expect_err("an oversized frame must be rejected before allocation grows further"); + + // Assert + assert!(error.contains("oversized-plugin"), "{error}"); + assert!(error.contains("startup hello"), "{error}"); + assert!(error.contains("byte limit"), "{error}"); + } + #[test] - fn legacy_startup_eof_is_actionable_and_keeps_stderr_context() { - let stderr = "\nPlugin stderr:\n error: unexpected argument '--socket-path'"; - let error = format!("{}{}", legacy_startup_eof_error("plugin-v0.0.8"), stderr); + fn legacy_startup_eof_is_actionable_without_exposing_stderr() { + let secret = "postgres://admin:secret@example.test/temps"; + let error = format!( + "{}{}", + legacy_startup_eof_error("plugin-v0.0.8"), + plugin_stderr_context(secret.len()) + ); assert!(error.contains("incompatible or uses a legacy temps-plugin-sdk")); assert!(error.contains("rebuild it with protocol v2")); - assert!(error.contains("unexpected argument '--socket-path'")); + assert!(error.contains("diagnostic output")); + assert!(error.contains("withheld to prevent secret disclosure")); + assert!(!error.contains(secret)); + } + + #[tokio::test] + async fn plugin_command_environment_is_scrubbed() { + let mut command = Command::new("sh"); + command.env("TEMPS_TEST_SECRET", "must-not-leak"); + scrub_plugin_environment(&mut command); + let status = command + .arg("-c") + .arg("test -z \"$TEMPS_TEST_SECRET\"") + .status() + .await + .expect("run environment probe"); + assert!(status.success()); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_prepare_candidate_wrong_identity_receives_no_launch_configuration() { + use std::os::unix::fs::PermissionsExt as _; + + // Arrange: the child identifies itself, then records stdin only if the + // host sends the second (secret-bearing) handshake frame. + let temp = tempfile::tempdir().expect("tempdir"); + let mut config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://admin:must-not-leak@example.test/temps".to_string(), + ); + config.handshake_timeout = Duration::from_secs(2); + std::fs::create_dir_all(&config.plugins_dir).expect("plugins directory"); + let marker = temp.path().join("launch-config-received"); + let manifest = PluginManifest::builder("wrong-plugin", "1.0.0") + .requires_db(true) + .requires_host_data_access(true) + .build(); + let hello = serde_json::to_string(&HandshakeMessage::Hello( + temps_core::external_plugin::PluginHello { + protocol_version: EXTERNAL_PLUGIN_PROTOCOL_VERSION, + manifest: Box::new(manifest), + }, + )) + .expect("serialize hello"); + let script = format!( + "#!/bin/sh\nprintf '%s\\n' '{}'\nif IFS= read -r launch; then printf '%s' \"$launch\" > '{}'; fi\n", + hello.replace('\'', "'\\''"), + marker.display() + ); + let binary = config.plugins_dir.join("wrong-identity-fixture"); + std::fs::write(&binary, script.as_bytes()).expect("fixture script"); + std::fs::set_permissions(&binary, std::fs::Permissions::from_mode(0o500)) + .expect("fixture permissions"); + let digest = hex::encode(Sha256::digest(script.as_bytes())); + let manager = ExternalPluginManager::new(config, mock_db()); + + // Act + let error = match manager + .prepare_candidate("expected-plugin", "1.0.0", &digest, &binary) + .await + { + Ok(_) => panic!("signed and declared identities must match"), + Err(error) => error, + }; + tokio::time::sleep(Duration::from_millis(50)).await; + + // Assert + assert!(error.contains("wrong-plugin"), "{error}"); + assert!(error.contains("expected-plugin"), "{error}"); + assert!(!error.contains("must-not-leak"), "{error}"); + assert!( + !marker.exists(), + "identity rejection must happen before the launch secret is written" + ); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_promote_candidate_swaps_then_stops_previous_process() { + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + let manager = ExternalPluginManager::new(config, mock_db()); + let (old, old_pid) = sleeping_process(temp.path(), "example", "1.0.0"); + let (candidate, candidate_pid) = sleeping_process(temp.path(), "example", "2.0.0"); + manager + .plugins + .write() + .await + .insert("example".to_string(), old); + + let manifest = manager + .promote_candidate(PendingPlugin { + expected_name: "example".to_string(), + process: candidate, + }) + .await; + + assert_eq!(manifest.version, "2.0.0"); + assert!( + !process_is_alive(old_pid), + "previous process must be reaped" + ); + assert!( + process_is_alive(candidate_pid), + "candidate must remain active" + ); + assert_eq!(manager.manifests().await[0].version, "2.0.0"); + + manager.shutdown_all().await; + assert!( + !process_is_alive(candidate_pid), + "active process must be reaped" + ); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_discard_candidate_stops_process_without_activating_it() { + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + let manager = ExternalPluginManager::new(config, mock_db()); + let (candidate, candidate_pid) = sleeping_process(temp.path(), "example", "2.0.0"); + + manager + .discard_candidate(PendingPlugin { + expected_name: "example".to_string(), + process: candidate, + }) + .await; + + assert!( + !process_is_alive(candidate_pid), + "discarded process must be reaped" + ); + assert!(manager.manifests().await.is_empty()); } #[tokio::test] @@ -1068,11 +1474,36 @@ mod tests { assert!(manifests.is_empty()); // Reload — should also be empty with no plugins - let manifests = manager.reload_all().await; - assert!(manifests.is_empty()); + let result = manager.reload_all().await; + assert!(result.manifests.is_empty()); + assert!(result.failures.is_empty()); assert!(manager.manifests().await.is_empty()); } + #[tokio::test] + async fn test_reload_all_invalid_active_install_reports_typed_failure() { + // Arrange + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + std::fs::create_dir_all(config.plugins_dir.join("broken-plugin")) + .expect("broken plugin directory"); + let manager = ExternalPluginManager::new(config, mock_db()); + + // Act + let result = manager.reload_all().await; + + // Assert + assert!(result.manifests.is_empty()); + assert_eq!(result.failures.len(), 1); + assert_eq!( + result.failures[0].reason, + "Activated plugin installation failed verification" + ); + } + #[tokio::test] async fn test_reload_plugin_not_running() { let tmp = tempfile::tempdir().unwrap(); @@ -1271,7 +1702,7 @@ mod tests { #[tokio::test] #[cfg(unix)] - async fn test_kill_stale_processes_kills_real_process() { + async fn stale_pid_cleanup_never_kills_a_reused_process_id() { use tokio::process::Command; let tmp = tempfile::tempdir().unwrap(); @@ -1303,19 +1734,18 @@ mod tests { let alive = unsafe { libc::kill(pid as i32, 0) } == 0; assert!(alive, "Spawned sleep process should be alive"); - // Kill stale processes + // Clean stale bookkeeping. A numeric PID alone is deliberately not + // sufficient authority to signal a process because the OS may have + // reused it. manager.kill_stale_processes().await; - // Reap the zombie child so the kernel removes the process entry. - // Without this, kill(pid, 0) returns success for zombies. - let exit = child.wait().await; - assert!(exit.is_ok(), "Should be able to wait on killed child"); - - // Verify the process is dead let still_alive = unsafe { libc::kill(pid as i32, 0) } == 0; - assert!(!still_alive, "Stale process should have been killed"); + assert!(still_alive, "PID-file cleanup must not kill a process"); // PID file should be cleaned up assert!(!pid_file.exists(), "PID file should be removed"); + + child.start_kill().expect("terminate test process"); + child.wait().await.expect("reap test process"); } } diff --git a/crates/temps-external-plugins/src/plugin.rs b/crates/temps-external-plugins/src/plugin.rs index 8b2394fb1..6e3cb7b9d 100644 --- a/crates/temps-external-plugins/src/plugin.rs +++ b/crates/temps-external-plugins/src/plugin.rs @@ -92,6 +92,9 @@ impl TempsPlugin for ExternalPluginsPlugin { // Register the handler app state let app_state = Arc::new(ExternalPluginsAppState { service: service.clone(), + audit_service: context.require_service::(), + sensitive_action_authorizer: context + .require_service::(), }); // External plugin OpenAPI schemas would normally be merged into @@ -102,7 +105,12 @@ impl TempsPlugin for ExternalPluginsPlugin { // dramatically faster boot, which is the right call when the // common case is "no external plugins installed". { - let mut cache = self.cached_schemas.lock().unwrap(); + let mut cache = self.cached_schemas.lock().map_err(|error| { + PluginError::PluginRegistrationFailed { + plugin_name: self.name().to_string(), + error: format!("failed to lock external-plugin OpenAPI cache: {error}"), + } + })?; *cache = Some(Vec::new()); } diff --git a/crates/temps-external-plugins/src/service.rs b/crates/temps-external-plugins/src/service.rs index 11e69aa68..5da6a7897 100644 --- a/crates/temps-external-plugins/src/service.rs +++ b/crates/temps-external-plugins/src/service.rs @@ -6,16 +6,25 @@ //! Orchestrates plugin lifecycle (discovery, proxy creation, event delivery) //! and provides a clean API consumed by the handler and plugin layers. +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use axum::Router; use temps_core::external_plugin::PluginManifest; use temps_core::JobQueue; +use thiserror::Error; use tokio::sync::RwLock; use tracing::{debug, error, info}; +use crate::catalog::{ + validate_url, CatalogError, RegistryClient, RegistryPlugin, VerifiedRegistry, +}; use crate::event_listener::PluginEventListener; -use crate::manager::{ExternalPluginConfig, ExternalPluginManager}; +use crate::install::{ + normalize_digest, platform_target, validate_plugin_name, validate_version, InstallError, + PluginInstaller, +}; +use crate::manager::{ExternalPluginConfig, ExternalPluginManager, PluginReloadResult}; use crate::proxy; /// Service that manages the external plugin lifecycle and provides data @@ -31,6 +40,57 @@ pub struct ExternalPluginsService { /// Swappable proxy router — rebuilt on reload so new/removed plugins /// are reflected without restarting the server. proxy_router: Arc>, + /// Serializes discovery, reload, and install/promotion lifecycles. + lifecycle: tokio::sync::Mutex<()>, + /// Set before shutdown waits for the lifecycle lock so queued mutations + /// cannot start after shutdown was requested. + closing: AtomicBool, +} + +#[derive(Debug, Error)] +pub enum ExternalPluginsError { + #[error(transparent)] + Catalog(#[from] CatalogError), + #[error(transparent)] + Install(#[from] InstallError), + #[error("Plugin '{name}' is not present in the authenticated registry document")] + NotInRegistry { name: String }, + #[error("Authenticated registry document contains duplicate entries for plugin '{name}'")] + DuplicateRegistryEntry { name: String }, + #[error("Plugin '{name}' v{version} failed protocol identity/ready verification; previous active version was preserved: {reason}")] + CandidateRejected { + name: String, + version: String, + reason: String, + }, + #[error("External plugin service is shutting down and cannot accept lifecycle changes")] + ShuttingDown, +} + +#[derive(Debug, Clone)] +pub struct InstallOutcome { + pub name: String, + pub version: String, + pub platform: String, + pub sha256: String, + pub signer_key_id: String, + pub registry_source: String, +} + +#[derive(Debug, Clone)] +pub struct ReleaseIdentity { + pub name: String, + pub version: String, + pub platform: String, + pub sha256: String, + pub signer_key_id: String, + pub registry_source: String, +} + +pub struct SelectedPlugin { + registry: VerifiedRegistry, + plugin: RegistryPlugin, + pub identity: ReleaseIdentity, } impl ExternalPluginsService { @@ -69,6 +129,8 @@ impl ExternalPluginsService { event_listener: RwLock::new(None), queue, proxy_router: Arc::new(RwLock::new(Router::new())), + lifecycle: tokio::sync::Mutex::new(()), + closing: AtomicBool::new(false), } } @@ -77,6 +139,10 @@ impl ExternalPluginsService { /// freshly-constructed shell from [`new_empty`](Self::new_empty). pub fn start_background_discovery(self: Arc) { tokio::spawn(async move { + let _lifecycle = self.lifecycle.lock().await; + if self.closing.load(Ordering::Acquire) { + return; + } let manifests = self.manager.discover_and_start().await; if !manifests.is_empty() { @@ -150,6 +216,8 @@ impl ExternalPluginsService { event_listener: RwLock::new(event_listener), queue, proxy_router: Arc::new(RwLock::new(proxy_router)), + lifecycle: tokio::sync::Mutex::new(()), + closing: AtomicBool::new(false), } } @@ -181,8 +249,12 @@ impl ExternalPluginsService { /// 4. Rebuilds the proxy router /// 5. Restarts the event listener if needed /// - /// Returns the manifests of all successfully started plugins. - pub async fn reload_plugins(&self) -> Vec { + /// Returns every successful manifest and every verification/start failure. + pub async fn reload_plugins(&self) -> Result { + let _lifecycle = self.lifecycle.lock().await; + if self.closing.load(Ordering::Acquire) { + return Err(ExternalPluginsError::ShuttingDown); + } // Stop event listener { let mut listener = self.event_listener.write().await; @@ -192,7 +264,8 @@ impl ExternalPluginsService { } // Reload all plugins via manager (shutdown + re-discover + re-start) - let new_manifests = self.manager.reload_all().await; + let result = self.manager.reload_all().await; + let new_manifests = &result.manifests; info!( "Reloaded {} external plugin(s): {}", @@ -205,7 +278,7 @@ impl ExternalPluginsService { ); // Rebuild proxy router and swap it in - let new_router = Self::build_proxy_router_from(&self.manager, &new_manifests).await; + let new_router = Self::build_proxy_router_from(&self.manager, new_manifests).await; { let mut router = self.proxy_router.write().await; *router = new_router; @@ -214,8 +287,7 @@ impl ExternalPluginsService { // Restart event listener { let new_listener = - Self::start_event_listener(&self.manager, &new_manifests, self.queue.as_ref()) - .await; + Self::start_event_listener(&self.manager, new_manifests, self.queue.as_ref()).await; let mut listener = self.event_listener.write().await; *listener = new_listener; } @@ -226,11 +298,152 @@ impl ExternalPluginsService { *manifests = new_manifests.clone(); } - new_manifests + Ok(result) + } + + /// Fetch and authenticate the complete remote catalogue. + pub async fn catalog(&self) -> Result { + let client = RegistryClient::new(self.manager.config().registry.clone())?; + Ok(client.fetch().await?) + } + + /// Resolve the exact signed release identity without downloading or + /// executing it. Handlers use this boundary to durably audit which bytes + /// are about to run before candidate execution starts. + pub async fn select_plugin(&self, name: &str) -> Result { + validate_plugin_name(name)?; + if self.closing.load(Ordering::Acquire) { + return Err(ExternalPluginsError::ShuttingDown); + } + let registry = self.catalog().await?; + let mut matches = registry + .document + .plugins + .iter() + .filter(|plugin| plugin.name == name); + let plugin = matches + .next() + .ok_or_else(|| ExternalPluginsError::NotInRegistry { + name: name.to_string(), + })?; + if matches.next().is_some() { + return Err(ExternalPluginsError::DuplicateRegistryEntry { + name: name.to_string(), + }); + } + let plugin = plugin.clone(); + validate_version(&plugin.name, &plugin.version)?; + let platform = platform_target()?; + let release = plugin + .platforms + .get(&platform) + .ok_or_else(|| InstallError::NoRelease { + plugin: plugin.name.clone(), + version: plugin.version.clone(), + platform: platform.clone(), + })?; + let sha256 = normalize_digest(&plugin.name, &plugin.version, &release.sha256)?; + validate_url(&release.url, &self.manager.config().registry, false).map_err(|_| { + InstallError::UnsafeArtifactUrl { + plugin: plugin.name.clone(), + url: release.url.clone(), + } + })?; + let identity = ReleaseIdentity { + name: plugin.name.clone(), + version: plugin.version.clone(), + platform, + sha256, + signer_key_id: registry.envelope.key_id.clone(), + registry_source: self.manager.config().registry.url.clone(), + }; + Ok(SelectedPlugin { + registry, + plugin, + identity, + }) + } + + /// Install and activate one preselected signed release without disrupting + /// its healthy process until the candidate completes its handshake. + pub async fn install_selected( + &self, + selected: SelectedPlugin, + ) -> Result { + let _lifecycle = self.lifecycle.lock().await; + if self.closing.load(Ordering::Acquire) { + return Err(ExternalPluginsError::ShuttingDown); + } + + let installer = PluginInstaller::new(self.manager.config().registry.clone())?; + installer + .accept_registry_revision( + &self.manager.config().plugins_dir, + selected.registry.document.revision, + ) + .await?; + let candidate = installer + .prepare( + &self.manager.config().plugins_dir, + &selected.registry, + &selected.plugin, + ) + .await?; + let pending = match self + .manager + .prepare_candidate( + &candidate.name, + &candidate.version, + &candidate.sha256, + &candidate.binary_path, + ) + .await + { + Ok(pending) => pending, + Err(reason) => { + if let Err(cleanup_error) = installer.discard(&candidate).await { + tracing::warn!( + plugin = %candidate.name, + error = %cleanup_error, + "Failed to remove rejected plugin candidate" + ); + } + return Err(ExternalPluginsError::CandidateRejected { + name: candidate.name.clone(), + version: candidate.version.clone(), + reason, + }); + } + }; + + if let Err(error) = installer.activate(&candidate).await { + self.manager.discard_candidate(pending).await; + if let Err(cleanup_error) = installer.discard(&candidate).await { + tracing::warn!( + plugin = %candidate.name, + error = %cleanup_error, + "Failed to remove uncommitted plugin candidate" + ); + } + return Err(error.into()); + } + self.manager.promote_candidate(pending).await; + self.refresh_runtime_surfaces().await; + + Ok(InstallOutcome { + name: candidate.name, + version: candidate.version, + platform: candidate.platform, + sha256: candidate.sha256, + signer_key_id: selected.identity.signer_key_id, + registry_source: selected.identity.registry_source, + }) } /// Shut down all external plugins gracefully. pub async fn shutdown_all(&self) { + self.closing.store(true, Ordering::Release); + let _lifecycle = self.lifecycle.lock().await; let mut listener = self.event_listener.write().await; if let Some(l) = listener.take() { l.stop().await; @@ -270,6 +483,22 @@ impl ExternalPluginsService { router } + async fn refresh_runtime_surfaces(&self) { + { + let mut listener = self.event_listener.write().await; + if let Some(listener) = listener.take() { + listener.stop().await; + } + } + let manifests = self.manager.manifests().await; + let router = Self::build_proxy_router_from(&self.manager, &manifests).await; + let listener = + Self::start_event_listener(&self.manager, &manifests, self.queue.as_ref()).await; + *self.proxy_router.write().await = router; + *self.event_listener.write().await = listener; + *self.manifests.write().await = manifests; + } + /// Start event listener if any plugins subscribe to events. async fn start_event_listener( manager: &Arc, @@ -304,3 +533,420 @@ impl ExternalPluginsService { } } } + +#[cfg(test)] +mod tests { + use super::*; + use base64::Engine as _; + use ed25519_dalek::{Signer as _, SigningKey}; + use sha2::{Digest as _, Sha256}; + use std::collections::BTreeMap; + use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + + async fn serve_artifact_once(body: Vec) -> String { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("bind artifact fixture"); + let address = listener.local_addr().expect("artifact fixture address"); + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.expect("accept artifact request"); + let mut request = [0u8; 2048]; + let _ = stream + .read(&mut request) + .await + .expect("read artifact request"); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + body.len() + ); + stream + .write_all(headers.as_bytes()) + .await + .expect("write artifact headers"); + stream.write_all(&body).await.expect("write artifact body"); + }); + format!("http://{address}/plugin") + } + + #[cfg(unix)] + fn protocol_v2_fixture(name: &str, version: &str) -> Vec { + let manifest = PluginManifest::builder(name, version).build(); + let hello = serde_json::to_string(&temps_core::external_plugin::HandshakeMessage::Hello( + temps_core::external_plugin::PluginHello { + protocol_version: temps_core::external_plugin::EXTERNAL_PLUGIN_PROTOCOL_VERSION, + manifest: Box::new(manifest), + }, + )) + .expect("serialize fixture hello"); + let ready = serde_json::to_string(&temps_core::external_plugin::HandshakeMessage::Ready( + temps_core::external_plugin::PluginReady { + ready: true, + has_ui: false, + protocol_version: temps_core::external_plugin::EXTERNAL_PLUGIN_PROTOCOL_VERSION, + openapi: None, + }, + )) + .expect("serialize fixture ready"); + let hello_literal = serde_json::to_string(&hello).expect("quote fixture hello"); + let ready_literal = serde_json::to_string(&ready).expect("quote fixture ready"); + format!( + r#"#!/usr/bin/python3 +import base64 +import hashlib +import json +import socket +import sys + +HELLO = {hello_literal} +READY = {ready_literal} +args = dict(zip(sys.argv[1::2], sys.argv[2::2])) +print(HELLO, flush=True) +launch = json.loads(sys.stdin.readline()) +server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) +server.bind(args["--socket-path"]) +server.listen(1) +print(READY, flush=True) +connection, _ = server.accept() +request = b"" +while b"\r\n\r\n" not in request: + request += connection.recv(4096) +lines = request.decode("latin1").split("\r\n") +headers = {{}} +for line in lines[1:]: + if ":" in line: + key, value = line.split(":", 1) + headers[key.lower()] = value.strip() +if headers.get("x-temps-auth-signature") != launch["auth_secret"]: + sys.exit(7) +websocket_key = headers["sec-websocket-key"] +accept = base64.b64encode(hashlib.sha1((websocket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode()).digest()).decode() +connection.sendall(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " + accept + "\r\n\r\n").encode()) +try: + while connection.recv(4096): + pass +except Exception: + pass +"# + ) + .into_bytes() + } + + fn selected_plugin( + url: String, + bytes: &[u8], + name: &str, + version: &str, + revision: u64, + signing: &SigningKey, + ) -> SelectedPlugin { + let platform = platform_target().expect("supported test platform"); + let sha256 = hex::encode(Sha256::digest(bytes)); + let plugin = RegistryPlugin { + name: name.to_string(), + title: "Fixture plugin".to_string(), + summary: "fixture".to_string(), + description: "fixture".to_string(), + author: "Temps Contributors".to_string(), + category: "test".to_string(), + keywords: vec!["test".to_string()], + logo_url: None, + repository: None, + docs_url: None, + version: version.to_string(), + platforms: BTreeMap::from([( + platform.clone(), + crate::catalog::PlatformRelease { + url, + sha256: sha256.clone(), + }, + )]), + }; + let document = crate::catalog::RegistryDocument { + schema_version: 1, + revision, + issued_at: chrono::Utc::now() - chrono::Duration::minutes(1), + expires_at: chrono::Utc::now() + chrono::Duration::hours(1), + plugins: vec![plugin.clone()], + }; + let payload = serde_json::to_vec(&document).expect("serialize fixture registry"); + let envelope = crate::catalog::RegistryEnvelope { + key_id: "fixture-key".to_string(), + payload: base64::engine::general_purpose::STANDARD.encode(&payload), + signature: base64::engine::general_purpose::STANDARD + .encode(signing.sign(&payload).to_bytes()), + }; + SelectedPlugin { + registry: VerifiedRegistry { envelope, document }, + plugin, + identity: ReleaseIdentity { + name: name.to_string(), + version: version.to_string(), + platform, + sha256, + signer_key_id: "fixture-key".to_string(), + registry_source: "http://127.0.0.1/fixture".to_string(), + }, + } + } + fn service() -> ExternalPluginsService { + let database = Arc::new( + sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection(), + ); + ExternalPluginsService::new_empty( + ExternalPluginConfig::new( + std::env::temp_dir().join("temps-external-plugin-service-tests"), + "postgres://localhost/test".to_string(), + ), + None, + database, + ) + } + + #[tokio::test] + async fn catalog_fails_closed_without_production_trust_anchor() { + let error = service() + .catalog() + .await + .expect_err("unsigned registry access must be unavailable"); + assert!(matches!( + error, + ExternalPluginsError::Catalog(CatalogError::TrustNotConfigured { .. }) + )); + } + + #[tokio::test] + async fn install_rejects_path_traversal_before_network_access() { + let error = match service().select_plugin("../../escape").await { + Ok(_) => panic!("path traversal must be rejected"), + Err(error) => error, + }; + assert!(matches!( + error, + ExternalPluginsError::Install(InstallError::UnsafePluginName { .. }) + )); + } + + #[tokio::test] + async fn shutdown_rejects_later_installs_before_registry_access() { + let service = service(); + service.shutdown_all().await; + let error = match service.select_plugin("safe").await { + Ok(_) => panic!("closed service must reject installs"), + Err(error) => error, + }; + assert!(matches!(error, ExternalPluginsError::ShuttingDown)); + } + + #[tokio::test] + #[cfg(unix)] + async fn protocol_v2_install_activates_and_promotes_verified_binary() { + if !std::path::Path::new("/usr/bin/python3").exists() { + eprintln!("skipping protocol fixture: /usr/bin/python3 is unavailable"); + return; + } + let temp = tempfile::tempdir().expect("tempdir"); + let signing = SigningKey::from_bytes(&[61; 32]); + let binary = protocol_v2_fixture("fixture-plugin", "1.0.0"); + let url = serve_artifact_once(binary.clone()).await; + let registry = crate::catalog::RegistryConfig::local( + url.clone(), + "fixture-key", + signing.verifying_key().to_bytes(), + ); + let mut config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ) + .with_registry(registry); + config.sockets_dir = temp.path().join("sockets"); + std::fs::create_dir_all(&config.sockets_dir).expect("fixture socket directory"); + let service = ExternalPluginsService::new_empty( + config.clone(), + None, + Arc::new( + sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection(), + ), + ); + + let outcome = service + .install_selected(selected_plugin( + url, + &binary, + "fixture-plugin", + "1.0.0", + 1, + &signing, + )) + .await + .expect("protocol-v2 fixture must install"); + + assert_eq!(outcome.name, "fixture-plugin"); + assert_eq!(service.manifests().await[0].version, "1.0.0"); + assert!(config + .plugins_dir + .join("fixture-plugin/active.json") + .is_file()); + service.shutdown_all().await; + } + + #[tokio::test] + #[cfg(unix)] + async fn rejected_upgrade_preserves_active_record_and_running_process() { + if !std::path::Path::new("/usr/bin/python3").exists() { + eprintln!("skipping protocol fixture: /usr/bin/python3 is unavailable"); + return; + } + let temp = tempfile::tempdir().expect("tempdir"); + let signing = SigningKey::from_bytes(&[62; 32]); + let first_binary = protocol_v2_fixture("fixture-plugin", "1.0.0"); + let first_url = serve_artifact_once(first_binary.clone()).await; + let registry = crate::catalog::RegistryConfig::local( + first_url.clone(), + "fixture-key", + signing.verifying_key().to_bytes(), + ); + let mut config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ) + .with_registry(registry); + config.sockets_dir = temp.path().join("sockets"); + std::fs::create_dir_all(&config.sockets_dir).expect("fixture socket directory"); + let service = ExternalPluginsService::new_empty( + config.clone(), + None, + Arc::new( + sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection(), + ), + ); + service + .install_selected(selected_plugin( + first_url, + &first_binary, + "fixture-plugin", + "1.0.0", + 1, + &signing, + )) + .await + .expect("initial plugin must install"); + let active_path = config.plugins_dir.join("fixture-plugin/active.json"); + let active_before = std::fs::read(&active_path).expect("initial active record"); + + let rejected_binary = protocol_v2_fixture("different-plugin", "2.0.0"); + let rejected_url = serve_artifact_once(rejected_binary.clone()).await; + let error = service + .install_selected(selected_plugin( + rejected_url, + &rejected_binary, + "fixture-plugin", + "2.0.0", + 2, + &signing, + )) + .await + .expect_err("signed and declared identities must match"); + + assert!(matches!( + error, + ExternalPluginsError::CandidateRejected { .. } + )); + assert_eq!( + std::fs::read(&active_path).expect("preserved active record"), + active_before + ); + assert_eq!(service.manifests().await[0].version, "1.0.0"); + let install_directories = std::fs::read_dir(config.plugins_dir.join("fixture-plugin")) + .expect("plugin root") + .filter_map(Result::ok) + .filter(|entry| entry.file_type().is_ok_and(|kind| kind.is_dir())) + .count(); + assert_eq!(install_directories, 1, "rejected candidate must be removed"); + service.shutdown_all().await; + } + + #[tokio::test] + async fn test_install_selected_queued_during_shutdown_is_rejected_without_writes() { + // Arrange: hold the lifecycle lock so shutdown and install both queue, + // then wait until shutdown has atomically closed admission. + let temp = tempfile::tempdir().expect("tempdir"); + let config = ExternalPluginConfig::new( + temp.path().to_path_buf(), + "postgres://localhost/test".to_string(), + ); + let plugins_dir = config.plugins_dir.clone(); + let database = Arc::new( + sea_orm::MockDatabase::new(sea_orm::DatabaseBackend::Postgres).into_connection(), + ); + let service = Arc::new(ExternalPluginsService::new_empty(config, None, database)); + let lifecycle = service.lifecycle.lock().await; + let shutdown_service = service.clone(); + let shutdown = tokio::spawn(async move { shutdown_service.shutdown_all().await }); + while !service.closing.load(Ordering::Acquire) { + tokio::task::yield_now().await; + } + let platform = platform_target().expect("supported test platform"); + let plugin = RegistryPlugin { + name: "safe-plugin".to_string(), + title: "Safe plugin".to_string(), + summary: "test".to_string(), + description: "test".to_string(), + author: "Temps Contributors".to_string(), + category: "test".to_string(), + keywords: vec!["test".to_string()], + logo_url: None, + repository: None, + docs_url: None, + version: "1.0.0".to_string(), + platforms: BTreeMap::from([( + platform.clone(), + crate::catalog::PlatformRelease { + url: "https://registry.temps.sh/safe-plugin".to_string(), + sha256: "00".repeat(32), + }, + )]), + }; + let selected = SelectedPlugin { + registry: VerifiedRegistry { + envelope: crate::catalog::RegistryEnvelope { + key_id: "test-key".to_string(), + payload: String::new(), + signature: String::new(), + }, + document: crate::catalog::RegistryDocument { + schema_version: 1, + revision: 1, + issued_at: chrono::Utc::now(), + expires_at: chrono::Utc::now() + chrono::Duration::hours(1), + plugins: vec![plugin.clone()], + }, + }, + identity: ReleaseIdentity { + name: plugin.name.clone(), + version: plugin.version.clone(), + platform, + sha256: "00".repeat(32), + signer_key_id: "test-key".to_string(), + registry_source: "https://registry.temps.sh/api/plugins".to_string(), + }, + plugin, + }; + let install_service = service.clone(); + let install = tokio::spawn(async move { install_service.install_selected(selected).await }); + + // Act + drop(lifecycle); + shutdown.await.expect("shutdown task"); + let error = install + .await + .expect("install task") + .expect_err("shutdown must reject queued installation"); + + // Assert + assert!(matches!(error, ExternalPluginsError::ShuttingDown)); + assert!( + !plugins_dir.exists(), + "a rejected queued install must not create plugin state" + ); + } +} diff --git a/web/bun.lock b/web/bun.lock index f6a8d59b6..c0a593d6c 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -32,7 +32,7 @@ "@tailwindcss/postcss": "^4.3.3", "@tanstack/react-query": "^5.102.8", "@tanstack/react-virtual": "^3.14.10", - "@temps-sdk/console-kit": "file:./packages/console-kit", + "@temps-sdk/console-kit": "workspace:*", "@temps-sdk/react-analytics": "^0.0.4", "@types/three": "^0.185.4", "ansi-to-html": "^0.7.2", @@ -548,8 +548,6 @@ "@temps-sdk/analytics-core": ["@temps-sdk/analytics-core@0.0.2", "", { "dependencies": { "@rrweb/packer": "^2.0.0-alpha.17", "rrweb": "^2.0.0-alpha.17", "web-vitals": "^4.2.4" } }, "sha512-7lpfpNLVzmHDJtKVuaX11e3BJoKkjeFabssg7kJuK3iOfZxp+tpLCxY4V7z3H5DN5VHGLi9dAFj4FxSlv5azbw=="], - "@temps-sdk/console-kit": ["@temps-sdk/console-kit@file:packages/console-kit", { "devDependencies": { "@types/react": "^19.1.12", "react": "^19.1.1", "typescript": "^5.9.2" }, "peerDependencies": { "react": "^19.0.0" } }], - "@temps-sdk/console-kit": ["@temps-sdk/console-kit@workspace:packages/console-kit"], "@temps-sdk/ds": ["@temps-sdk/ds@workspace:packages/ds"], diff --git a/web/package.json b/web/package.json index 91d07317d..78e42dfec 100644 --- a/web/package.json +++ b/web/package.json @@ -62,7 +62,7 @@ "@tailwindcss/postcss": "^4.3.3", "@tanstack/react-query": "^5.102.8", "@tanstack/react-virtual": "^3.14.10", - "@temps-sdk/console-kit": "file:./packages/console-kit", + "@temps-sdk/console-kit": "workspace:*", "@temps-sdk/react-analytics": "^0.0.4", "@types/three": "^0.185.4", "ansi-to-html": "^0.7.2", diff --git a/web/src/api/client/index.ts b/web/src/api/client/index.ts index 2b90635f7..0bd42d4d0 100644 --- a/web/src/api/client/index.ts +++ b/web/src/api/client/index.ts @@ -3,5 +3,32 @@ // This file is auto-generated by @hey-api/openapi-ts +export { getPluginStatus, installPlugin, listPluginCatalog } from './sdk.gen'; +export type { + GetPluginStatusData, + GetPluginStatusError, + GetPluginStatusErrors, + GetPluginStatusResponse, + GetPluginStatusResponses, + InstallPluginData, + InstallPluginError, + InstallPluginErrors, + InstallPluginRequest, + InstallPluginResponse, + InstallPluginResponse2, + InstallPluginResponses, + ListPluginCatalogData, + ListPluginCatalogError, + ListPluginCatalogErrors, + ListPluginCatalogResponse, + ListPluginCatalogResponses, + PlatformRelease, + PluginCatalogResponse, + PluginStatusResponse, + RegistryEnvelope, + RegistryPlugin, + ReloadFailureResponse, +} from './types.gen'; + export { acknowledgeAlarm, acknowledgeSystemAlarm, activateAiProvider, activateApiKey, activateConnection, activateProvider, addClusterMember, addContext, addEnvironmentDomain, addEvents, addManagedDomain, addProviderModel, addSessionReplayEvents, addTeamMember, adminDrainNode, adminDrainStatus, adminGetNode, adminListNodeContainers, adminListNodes, adminRemoveNode, adminUndrainNode, aggregateApiTraffic, applyHostnameMode, archiveApplication, archiveConversation, archiveFlag, archiveUserConversation, assignRole, attachScheduleServices, authorizeEmailDomainProject, blobCopy, blobDelete, blobDisable, blobDownload, blobEnable, blobHead, blobList, blobPut, blobStatus, blobUpdate, cancel, cancelBackup, cancelBulkActivationJob, cancelDeployment, cancelDomainOrder, cancelPgUpgrade, cancelRun, cancelScheduleRun, changePasswordSelf, changeProjectSource, changeRequiredPassword, chatCompletions, checkAnalyticsHasEvents, checkCommitExists, checkDomainStatus, checkExplorerSupport, checkForUpdate, checkIpBlocked, checkProviderDeletionSafety, chunkUploadOptions, cleanupExpiredBackups, clearPreviewPassword, cliDeviceApprove, cliDeviceDeny, cliDeviceLookup, cliDevicePoll, cliDeviceStart, cliLogout, clusterDnsStatus, cmd, cmdKill, cmdLogs, confirmPendingAction, confirmUserPendingAction, containerMetricsGetHistory, controlApplicationWorkspace, createAgent, createAlert, createAlertRule, createAnalyticsIngestKey, createApiKey, createApplication, createApplicationConversation, createApplicationPreviewLink, createApplicationProject, createBackupSchedule, createBitbucketProvider, createBulkActivationJob, createCloudflareProvider, createConversation, createCustomDomain, createDashboard, createDeploymentToken, createDnsProvider, createDomain, createDsn, createEmailDomain, createEmailProvider, createEnvironment, createEnvironmentVariable, createFacet, createFlag, createFunnel, createGenericProvider, createGiteaPatProvider, createGithubPatProvider, createGitlabOauthProvider, createGitlabPatProvider, createGitProvider, createGlobalConversation, createGlobalMcp, createGlobalSkill, createGlobalWorkspacePreviewLink, createIncident, createIpAccessControl, createMcp, createMonitor, createNotificationEmailProvider, createNotificationProvider, createNotificationRoute, createOidcProvider, createOidcRoleMapping, createOrRecreateOrder, createPlan, createPr, createProject, createProjectFromTemplate, createProjectRelease, createProjectSecret, createProviderKey, createRelease, createRoute, createS3Source, createSandbox, createService, createSkill, createSlackProvider, createSnapshot, createTeam, createThreadArtifact, createUser, createWebhook, createWebhookProvider, deactivateApiKey, deactivateConnection, deactivateProvider, deauthorizeDiscoveredRouteCert, deleteAgent, deleteAlert, deleteAlertRule, deleteApiKey, deleteBackup, deleteBackupSchedule, deleteConnection, deleteCustomDomain, deleteDashboard, deleteDeploymentToken, deleteDnsProvider, deleteDomain, deleteEmailDomain, deleteEmailProvider, deleteEnvironment, deleteEnvironmentDomain, deleteEnvironmentVariable, deleteExternalImage, deleteFacet, deleteFunnel, deleteGitProvider, deleteGlobalMcp, deleteGlobalSkill, deleteIpAccessControl, deleteMcp, deleteMonitor, deleteNotificationProvider, deleteNotificationRoute, deleteOidcProvider, deleteOidcRoleMapping, deletePreferences, deleteProject, deleteProjectSecret, deleteProviderKey, deleteProviderModel, deleteProviderSafely, deleteReleaseSourceFiles, deleteReleaseSourceMaps, deleteRoute, deleteS3Source, deleteScan, deleteSecret, deleteService, deleteSessionReplay, deleteSkill, deleteSnapshot, deleteSourceMap, deleteStaticBundle, deleteTeam, deleteUser, deleteWebhook, deployApplicationWorkspaceProject, deployFromImage, deployFromImageUpload, deployFromStatic, deployFromUploadedSource, deploymentMetricsGetLatest, deploymentMetricsGetRange, deploymentMetricsToggle, destroySandbox, detachScheduleService, detectPublicEnvExample, detectPublicPresets, disableBackupSchedule, disableMfa, disconnectCloud, discoverWorkloads, domain, downloadApplicationWorkspaceFile, downloadGlobalSkillArchive, downloadGlobalWorkspaceFile, downloadObject, downloadSkillArchive, emailStatus, embeddings, enableBackupSchedule, enrichVisitor, enrollCloud, estimateBulkActivation, exec, execDetached, executeDeploymentOperation, executeImport, extendTimeout, externalServiceEnablePgStatStatements, externalServiceMetricsByDatabase, externalServiceMetricsCreateAlertRule, externalServiceMetricsDeleteAlertRule, externalServiceMetricsGetAlertRules, externalServiceMetricsGetLatest, externalServiceMetricsGetRange, externalServiceMetricsStatus, externalServiceMetricsToggle, externalServiceMetricsUpdateAlertRule, externalServiceResetPgStatStatements, finalizeOrder, finalizeProjectRelease, findConversation, generateJoinToken, generatePresetDockerfile, getAccessInfo, getActiveVisitors, getActivityGraph, getAdminGate, getAgent, getAggregatedBuckets, getAiAgentBreakdown, getAiAgentPages, getAiAgentTimeline, getAiDataAccess, getAiPageBreakdown, getAiProviderStatus, getAiStatusBreakdown, getAlert, getAlertRule, getAllRepositoriesByName, getAnalyticsActiveVisitors, getAnalyticsEventsCount, getAnalyticsSessionEvents, getAnalyticsVisitorSessions, getApiCallers, getApiKey, getApiKeyPermissions, getApiRoutes, getApiSummary, getApiTimeseries, getApiTrafficProxyLogAccess, getApplication, getApplicationWorkspace, getApplicationWorkspaceChanges, getApplicationWorkspaceDiff, getApplicationWorkspaceDirectory, getApplicationWorkspaceFile, getAuditLog, getBackup, getBackupSchedule, getBranchesByRepositoryId, getBucketedIncidents, getBucketedStatus, getBulkActivationJob, getChallengeToken, getChatReadiness, getCliStatus, getCloudAiCapability, getCloudBackfillStatus, getCloudCapability, getCloudStatus, getCloudTelemetryStatus, getClusterHealth, getClusterMember, getCmd, getContainerDetail, getContainerEnvironmentVariable, getContainerLogs, getContainerLogsById, getContainerMetrics, getConversation, getConversationDetail, getConversations, getCronById, getCronExecutions, getCrossProjectTraceSiblings, getCurrentBulkActivationJob, getCurrentMonitorStatus, getCurrentUser, getCustomDomain, getDashboard, getDashboardProjectsAnalytics, getDelivery, getDeployment, getDeploymentContainerLogContent, getDeploymentJobLogs, getDeploymentJobs, getDeploymentOperations, getDeploymentOperationStatus, getDeploymentToken, getDiskStatus, getDnsChanges, getDnsProvider, getDomain, getDomainByHost, getDomainById, getDomainByName, getDomainDnsRecords, getDomainOrder, getEmail, getEmailEvents, getEmailLinks, getEmailProvider, getEmailStats, getEmailTracking, getEmailTrackingStatus, getEntityInfo, getEnvironment, getEnvironmentCrons, getEnvironmentDomains, getEnvironments, getEnvironmentVariables, getEnvironmentVariableValue, getErrorDashboardStats, getErrorEvent, getErrorGroup, getErrorStats, getErrorTimeSeries, getEventDetail, getEventEntries, getEventsCount, getEventsTimeline, getEventTypeBreakdown, getEventVisitors, getExternalImage, getExternalServiceBackupCapability, getFailureReportPreview, getFeatureMaturity, getFile, getFlag, getFlagSnapshot, getFunnelMetrics, getGenaiTrace, getGeneralStats, getGitProvider, getGlobalAiWorkspace, getGlobalEvents, getGlobalEventStats, getGlobalMcp, getGlobalSandboxStatus, getGlobalSkill, getGlobalWorkspaceChanges, getGlobalWorkspaceDiff, getGlobalWorkspaceDirectory, getGlobalWorkspaceFile, getGroupedPageMetrics, getHealth, getHourlyVisits, getHttpChallengeDebug, getImportStatus, getIncident, getIncidentUpdates, getIngestErrors, getIpAccessControl, getIpGeolocation, getJoinTokenStatus, getLastDeployment, getLatestDeploymentMedia, getLatestScan, getLatestScansPerEnvironment, getLiveVisitorsList, getLogContext, getMcp, getMetricsOverTime, getMonitor, getNotificationProvider, getNotificationRoute, getOnDemandCertStatus, getOrCreateDsn, getPageFlow, getPageHourlySessions, getPagePathDetail, getPagePaths, getPagePathsSparklines, getPagePathVisitors, getPendingAction, getPerformanceMetrics, getPgUpgrade, getPgUpgradeLogs, getPipelineHistory, getPipelineStats, getPlatformInfo, getPostgresWalHealth, getPreferences, getPreviewGatewayLogs, getPreviewGatewaySettings, getPreviewGatewayStatus, getPricing, getPrivateIp, getProject, getProjectAlarmsSummary, getProjectBySlug, getProjectCloudTelemetry, getProjectDeployments, getProjects, getProjectServiceEnvironmentVariables, getProjectServiceTemplate, getProjectSessionReplays, getProjectsHealth, getProjectsMonitorHealth, getProjectStatistics, getProjectTemplate, getPropertyBreakdown, getPropertyTimeline, getProviderConnections, getProviderKey, getProviderMetadata, getProvidersMetadata, getProxyLogById, getProxyLogByRequestId, getProxyLogs, getPublicBranches, getPublicComposePreview, getPublicComposeServices, getPublicIp, getPublicRepository, getQueryContainerInfo, getQuota, getRecentActivity, getRemoteExternalImage, getRepositoryBranches, getRepositoryById, getRepositoryByName, getRepositoryComposePreview, getRepositoryComposeServicesLive, getRepositoryEnvExampleLive, getRepositoryPresetByName, getRepositoryPresetLive, getRepositoryTags, getResolvedEnvironmentVariables, getResolvedEnvironmentVariableValue, getRestoreCapabilities, getRestoreRun, getRoute, getRun, getRunWithLogs, getS3Credentials, getS3Source, getSandbox, getSandboxStatus, getScan, getScanByDeployment, getScanVulnerabilities, getService, getServiceBySlug, getServiceEnvironmentVariable, getServiceEnvironmentVariables, getServiceHealthStatus, getServicePreviewEnvironmentVariableNames, getServicePreviewEnvironmentVariablesMasked, getServiceRuntime, getServiceStats, getServiceTypeParameters, getServiceTypes, getSessionDetails, getSessionEvents, getSessionLogs, getSessionReplay, getSessionReplayEvents, getSettings, getSkill, getSlowQueries, getSnapshot, getStaticBundle, getStatusOverview, getSystemAlarmsSummary, getTagsByRepositoryId, getTeam, getTimeBucketStats, getTodayStats, getTrace, getTraefikDiscoveryStatus, getUnifiedTrace, getUniqueCounts, getUniqueEvents, getUpdateCapability, getUpdateStatus, getUptimeHistory, getUsageByProvider, getUsageRecent, getUsageSummary, getUsageTimeseries, getUsageTopModels, getUserConversation, getUserConversationAttachment, getUserPendingAction, getVisibleCustomDomainByHostname, getVisitorByGuid, getVisitorById, getVisitorDetails, getVisitorFacets, getVisitorInfo, getVisitorJourney, getVisitors, getVisitorSessions, getVisitorStats, getWebhook, getWorkspaceFileLimits, grantProjectAccess, handleGitProviderOauthCallback, hasAnalyticsEvents, hasErrorGroups, hasPerformanceMetrics, hasTraces, importApplicationWorkspaceGit, importEmailDomain, importExternalService, importTraefikAcmeJson, ingestLogs, ingestLogsByPath, ingestMetrics, ingestMetricsByPath, ingestSentryEnvelope, ingestSentryEvent, ingestTraces, ingestTracesByPath, ingestTunneledEnvelope, initSessionReplay, inspectDropArchive, issueRuntimeCredentials, jobLogs, jobStatus, killJob, kvDel, kvDisable, kvEnable, kvExpire, kvGet, kvIncr, kvKeys, kvSet, kvStatus, kvTtl, kvUpdate, latestRunForSource, linkApplicationProject, linkCustomDomainToCertificate, linkServiceToProject, listAgentRuns, listAgents, listAiProviders, listAlertRules, listAlerts, listAllConversations, listAllRuns, listAnalyticsIngestKeys, listApiKeys, listApplicationConversations, listApplications, listAuditLogs, listAvailableContainers, listBackupAlerts, listBackupChildren, listBackupSchedules, listBackupsForSchedule, listCommitsByRepositoryId, listConnections, listContainerHistory, listContainers, listContainersAtPath, listConversations, listCustomDomainsForProject, listDashboards, listDeliveries, listDeploymentContainerLogs, listDeploymentTokens, listDiscoverableDomains, listDnsProviders, listDomains, listDsns, listEmailDomainProjects, listEmailDomains, listEmailProviders, listEmails, listEnrollmentTokens, listEntities, listErrorEvents, listErrorGroups, listEvents, listEventTypes, listExternalImages, listExternalPlugins, listExternalServiceBackups, listFacets, listFlags, listFunnels, listGitProviders, listGlobalMcps, listGlobalSkills, listIncidents, listInsights, listIpAccessControl, listJobs, listKnownAiAgents, listManagedDomains, listManagedEnvironmentVariables, listMcps, listMetricLabelKeys, listMetricLabelValues, listMetricNames, listModels, listMonitors, listNotificationProviders, listNotificationRoutes, listOidcProviders, listOidcProviderUsers, listOidcRoleMappings, listOnDemandCerts, listOrders, listPeers, listPendingActions, listPgUpgrades, listPresets, listProjectAccess, listProjectAlarms, listProjectScans, listProjectSecrets, listProjectServices, listProjectTemplates, listProjectTemplateTags, listProviderKeys, listProviderZones, listPublicProviders, listReleaseFiles, listReleases, listRemoteExternalImages, listRenewalAttempts, listRepositoriesByConnection, listRepositoriesByProvider, listRestoreRunsForService, listRootContainers, listRoutes, listS3Sources, listSandboxes, listScheduleRunJobs, listScheduleRuns, listScheduleServices, listSecrets, listServiceHealthStatuses, listServiceProjects, listServices, listServiceSchedules, listSkills, listSnapshots, listSourceBackups, listSourceFiles, listSourceMaps, listSources, listStaticBundles, listSyncedRepositories, listSystemAlarms, listTeamMembers, listTeamProjects, listTeams, listThreadArtifacts, listTraefikDiscoveredRoutes, listUserPendingActions, listUsers, listWebhooks, login, logout, lookupDnsARecords, mintEnrollmentToken, mkdir, nodeHeartbeat, nodeMetricsGetAlertRules, nodeMetricsGetRange, nodeMetricsUpdateAlertRule, observabilityFullEvent, observabilityListEvents, oidcCallback, type Options, patchAdminGate, patchPreviewGatewaySettings, pauseDeployment, pauseSandbox, planRestore, postDnsAck, previewAlert, previewFunnelMetrics, previewHostnameMode, promoteClusterMember, promoteDeployment, provisionDomain, purgeProjectLogs, pushExternalImage, queryData, queryGenaiTraces, queryLogs, queryMetrics, querySpanStats, queryTraces, queryTraceSummaries, readEntityRows, readFile, reAnalyze, reassignProjectCustomDomain, reconcileCloudBackupSource, recordConsoleEvent, recordEventMetrics, recordFlagExposure, recordSpeedMetrics, refreshAiProviderStatus, refreshProviderModels, refreshRouteTable, regenerateDsn, registerExternalImage, registerNode, reinstallGitlabWebhook, rejectPendingAction, rejectUserPendingAction, reloadPlugins, removeClusterMember, removeManagedDomain, removeRole, removeTeamMember, renameConversation, renameUserConversation, renewDomain, repointContinuousArchiveSource, requestDiscoveredRouteCert, requestPasswordReset, resetPassword, resizeSandbox, resolveAlarm, resolvePermission, resolveSystemAlarm, resolveUserPermission, restartContainer, restartPreviewGateway, restartSandbox, restoreApplication, restoreFlag, restoreUser, restoreUserConversation, resumeDeployment, resumeSandbox, retryCluster, retryDelivery, retryFacetBackfill, retryPgUpgrade, retryRun, revealGlobalMcpConfig, revealMcpConfig, revealNotificationProviderConfig, revealServiceEnvironmentVariables, revealServiceParameter, revenueCreateIntegration, revenueDeleteIntegration, revenueGlobalEvents, revenueImportInvoicesCsv, revenueImportSubscriptionsCsv, revenueListIntegrations, revenueListProviders, revenueMetricsCustomers, revenueMetricsGlobalMrr, revenueMetricsGlobalSummary, revenueMetricsMrr, revenueMetricsSummary, revenueRecentEvents, revenueRotateToken, revenueUpdateConfig, revenueUpdateSecret, revokeAnalyticsIngestKey, revokeDsn, revokeEmailDomainProject, revokeEnrollmentToken, revokeJoinToken, revokeProjectAccess, rollbackPgUpgrade, rollbackToDeployment, rootfsGc, rootfsReport, rotateAnalyticsIngestKey, rotateApiKey, rotateClusterCa, rotateDeploymentToken, runBackupForSource, runConnectionHealthCheck, runExternalServiceBackup, runScheduleNow, sandboxCreatePreviewLink, saveAgentToken, saveAiProviderCredential, searchLogs, sendEmail, sendFailureReport, sendProjectAiMessage, sendUserMessage, setAiDataAccess, setAlternateSources, setApplicationPrimaryProject, setDefaultS3Source, setFlagEnvironment, setPreviewPassword, setTraefikDiscoveredRouteEnabled, setupDns, setupDnsChallenge, setupEmailTracking, setupMfa, silenceAlarm, silenceSystemAlarm, sleepEnvironment, smokeTestAgent, sourceSandbox, startAnalysis, startContainer, startFix, startGitProviderOauth, startOidcLoginBySlug, startPgUpgrade, startRestore, startService, startUpdate, statPath, stopContainer, stopSandbox, stopService, stopTurn, stopUserTurn, storageSummary, streamContainerMetrics, streamEvents, streamRunEvents, syncRepositories, tailDeploymentJobLogs, tailLogs, teardownDeployment, teardownEnvironment, terminal, testNotificationProvider, testOidcProvider, testProvider, testProviderConnection, testProviderKeyById, testProviderKeyInline, testS3ConnectionPreview, testS3SourceConnection, trackClick, trackOpen, triggerAgent, triggerProjectPipeline, triggerScan, triggerServiceHealthCheck, triggerWeeklyDigest, unlinkApplicationProject, unlinkServiceFromProject, updateAgent, updateAiProvider, updateAiProviderPreference, updateAiSummaryPreference, updateAlert, updateAlertRule, updateAnalyticsIngestKey, updateApiKey, updateApplicationWorkspace, updateAutomaticDeploy, updateBackupSchedule, updateCloudFeatures, updateCloudflareProvider, updateConnectionToken, updateCustomDomain, updateDashboard, updateDeploymentToken, updateEmailProvider, updateEnvironmentSettings, updateEnvironmentSubdomain, updateEnvironmentVariable, updateErrorGroup, updateFlag, updateFunnel, updateGitProviderCredentials, updateGitSettings, updateGlobalMcp, updateGlobalSkill, updateIncidentStatus, updateIpAccessControl, updateManagedDomain, updateMcp, updateNotificationEmailProvider, updateNotificationProvider, updateNotificationRoute, updateOidcProvider, updatePermissionMode, updatePreferences, updateProject, updateProjectCloudTelemetry, updateProjectDeploymentConfig, updateProjectSecret, updateProjectSettings, updateProvider, updateProviderKey, updateProviderModel, updateRoute, updateS3Source, updateSelf, updateService, updateServiceResources, updateServiceTemplateRuntime, updateSessionDuration, updateSettings, updateSkill, updateSlackProvider, updateSpeedMetrics, updateTeam, updateTeamMemberRole, updateUser, updateUserPermissionMode, updateWebhook, updateWebhookProvider, upgradePreviewGateway, upgradeProjectServiceTemplate, upgradeService, uploadApplicationWorkspaceFiles, uploadGlobalSkill, uploadGlobalWorkspaceFiles, uploadReleaseFile, uploadSkill, uploadSourceFile, uploadSourceMap, uploadStaticBundle, uploadUserConversationAttachment, upsertSecret, validateConnection, validateEmail, verifyAndEnableMfa, verifyDomain, verifyEmail, verifyManagedDomain, verifyMfaChallenge, verifyStepUp, wakeEnvironment, webhookTrigger, workflowDryRun, writeApplicationWorkspaceFiles, writeFile, writeFiles } from './sdk.gen'; export type { AcknowledgeAlarmData, AcknowledgeAlarmErrors, AcknowledgeAlarmResponses, AcknowledgeSystemAlarmData, AcknowledgeSystemAlarmErrors, AcknowledgeSystemAlarmResponses, AcmeOrderResponse, ActivateAiProviderData, ActivateAiProviderErrors, ActivateAiProviderResponse, ActivateAiProviderResponses, ActivateApiKeyData, ActivateApiKeyErrors, ActivateApiKeyResponse, ActivateApiKeyResponses, ActivateConnectionData, ActivateConnectionErrors, ActivateConnectionResponses, ActivateProviderData, ActivateProviderErrors, ActivateProviderResponse, ActivateProviderResponses, ActiveVisitor, ActiveVisitorCountResponse, ActiveVisitorsQuery, ActiveVisitorsResponse, ActivityDay, ActivityEvent, ActivityGraphQuery, ActivityGraphResponse, AddClusterMemberData, AddClusterMemberErrors, AddClusterMemberRequest, AddClusterMemberResponse, AddClusterMemberResponses, AddContextData, AddContextErrors, AddContextRequest, AddContextResponses, AddEnvironmentDomainData, AddEnvironmentDomainErrors, AddEnvironmentDomainRequest, AddEnvironmentDomainResponse, AddEnvironmentDomainResponses, AddEventsData, AddEventsError, AddEventsErrors, AddEventsRequest, AddEventsResponse, AddEventsResponse2, AddEventsResponses, AddManagedDomainApiRequest, AddManagedDomainData, AddManagedDomainErrors, AddManagedDomainResponse, AddManagedDomainResponses, AddProviderModelData, AddProviderModelError, AddProviderModelErrors, AddProviderModelRequest, AddProviderModelResponse, AddProviderModelResponses, AddSessionReplayEventsData, AddSessionReplayEventsError, AddSessionReplayEventsErrors, AddSessionReplayEventsResponse, AddSessionReplayEventsResponses, AddTeamMemberData, AddTeamMemberErrors, AddTeamMemberResponse, AddTeamMemberResponses, AdminDrainNodeData, AdminDrainNodeErrors, AdminDrainNodeResponse, AdminDrainNodeResponses, AdminDrainStatusData, AdminDrainStatusErrors, AdminDrainStatusResponse, AdminDrainStatusResponses, AdminGateResponse, AdminGateSource, AdminGetNodeData, AdminGetNodeErrors, AdminGetNodeResponse, AdminGetNodeResponses, AdminListNodeContainersData, AdminListNodeContainersErrors, AdminListNodeContainersResponse, AdminListNodeContainersResponses, AdminListNodesData, AdminListNodesErrors, AdminListNodesResponse, AdminListNodesResponses, AdminRemoveNodeData, AdminRemoveNodeErrors, AdminRemoveNodeResponse, AdminRemoveNodeResponses, AdminUndrainNodeData, AdminUndrainNodeErrors, AdminUndrainNodeResponse, AdminUndrainNodeResponses, AgentConfigResponse, AgentRunLogResponse, AgentRunResponse, AgentRunWithLogsResponse, AgentSandboxSettings, AgentSandboxSettingsMasked, AggregateApiTrafficData, AggregateApiTrafficError, AggregateApiTrafficErrors, AggregateApiTrafficResponse, AggregateApiTrafficResponses, AggregatedBucketItem, AggregatedBucketsQuery, AggregatedBucketsResponse, AggregationLevel, AggregationTemporality, AiAgentBreakdownResponse, AiAgentBreakdownRow, AiAgentDescriptor, AiAgentPageRow, AiAgentPagesResponse, AiAgentTimelineResponse, AiAgentTimelineRow, AiChatLimitsSettings, AiCliStatusDto, AiConfigSettings, AiDataAccessResponse, AiModelOptionDto, AiPageBreakdownResponse, AiPageBreakdownRow, AiProviderStatusResponse, AiSelectOptionDto, AiStatusBreakdownResponse, AiStatusBreakdownRow, AiSummaryPreferenceDto, AiWorkspaceFileLimitsSettings, AlarmListResponse, AlarmResponse, AlarmSummaryResponse, AlertRuleResponse, AllocEntry, AnalyticsIngestKey, AnalyticsSessionEventsResponse, AnnotatedSpan, AnomalyAlgorithm, AnomalyParams, AnomalyPreviewPointResponse, AnomalyPreviewRequest, AnomalyPreviewResponse, ApiCallerEntry, ApiCallersResponse, ApiKeyListResponse, ApiKeyResponse, ApiRouteEntry, ApiRoutesResponse, ApiTimeseriesPoint, ApiTimeseriesResponse, ApiTrafficSummary, ApiTrafficSummaryResponse, ApplicationPreviewLinkResponse, ApplicationProjectDeploymentResponse, ApplicationProjectEnvironmentResponse, ApplicationProjectResponse, ApplicationResponse, ApplicationWorkspaceChangesResponse, ApplicationWorkspaceDiffResponse, ApplicationWorkspaceDirectoryEntryResponse, ApplicationWorkspaceDirectoryResponse, ApplicationWorkspaceFileContentResponse, ApplicationWorkspaceFileResponse, ApplicationWorkspaceFileWrite, ApplicationWorkspaceResponse, ApplyHostnameModeData, ApplyHostnameModeErrors, ApplyHostnameModeRequest, ApplyHostnameModeResponse, ApplyHostnameModeResponses, AppSettings, AppSettingsResponse, ArchiveApplicationData, ArchiveApplicationErrors, ArchiveApplicationResponse, ArchiveApplicationResponses, ArchiveConversationData, ArchiveConversationErrors, ArchiveConversationResponse, ArchiveConversationResponses, ArchiveFlagData, ArchiveFlagErrors, ArchiveFlagResponse, ArchiveFlagResponse2, ArchiveFlagResponses, ArchiveMode, ArchiveUserConversationData, ArchiveUserConversationErrors, ArchiveUserConversationResponse, ArchiveUserConversationResponses, AssignRoleData, AssignRoleErrors, AssignRoleRequest, AssignRoleResponses, AttachScheduleServicesData, AttachScheduleServicesError, AttachScheduleServicesErrors, AttachScheduleServicesRequest, AttachScheduleServicesResponse, AttachScheduleServicesResponse2, AttachScheduleServicesResponses, AuditLogIpInfo, AuditLogResponse, AuditLogUserInfo, AuthFlavorDto, AuthorizedEmailDomainProjectResponse, AuthorizeEmailDomainProjectData, AuthorizeEmailDomainProjectErrors, AuthorizeEmailDomainProjectResponse, AuthorizeEmailDomainProjectResponses, AuthResponse, AuthStatusResponse, AuthTokenResponse, AutofixerRunResponse, AutofixerRunWithLogsResponse, AutofixRunConfig, AutoWatchParams, AvailableAiProviderDto, AvailableContainerInfo, AvailablePermissions, BackupAlertListResponse, BackupAlertResponse, BackupResponse, BackupScheduleResponse, BitbucketAuthInput, BlobCopyData, BlobCopyError, BlobCopyErrors, BlobCopyResponse, BlobCopyResponses, BlobDeleteData, BlobDeleteError, BlobDeleteErrors, BlobDeleteResponse, BlobDeleteResponses, BlobDisableData, BlobDisableErrors, BlobDisableResponse, BlobDisableResponses, BlobDownloadData, BlobDownloadError, BlobDownloadErrors, BlobDownloadResponses, BlobEnableData, BlobEnableErrors, BlobEnableResponse, BlobEnableResponses, BlobHeadData, BlobHeadError, BlobHeadErrors, BlobHeadResponses, BlobListData, BlobListError, BlobListErrors, BlobListResponse, BlobListResponses, BlobPutData, BlobPutError, BlobPutErrors, BlobPutResponse, BlobPutResponses, BlobResponse, BlobStatusData, BlobStatusErrors, BlobStatusResponse, BlobStatusResponse2, BlobStatusResponses, BlobUpdateData, BlobUpdateErrors, BlobUpdateResponse, BlobUpdateResponses, BranchInfo, BranchListResponse, BrowserCount, BrowsersQuery, BuildConfiguration, BuildLimitsSettings, BulkActivationEstimateResponse, BulkActivationEtaState, BulkActivationJobProjectResponse, BulkActivationJobResponse, BulkActivationProjectEstimateResponse, BulkJobProjectStatus, BulkJobStatus, BulkJobTrigger, CancelBackupData, CancelBackupError, CancelBackupErrors, CancelBackupResponse, CancelBackupResponse2, CancelBackupResponses, CancelBulkActivationJobData, CancelBulkActivationJobError, CancelBulkActivationJobErrors, CancelBulkActivationJobResponse, CancelBulkActivationJobResponses, CancelData, CancelDeploymentData, CancelDeploymentErrors, CancelDeploymentResponse, CancelDeploymentResponses, CancelDomainOrderData, CancelDomainOrderErrors, CancelDomainOrderResponse, CancelDomainOrderResponses, CancelErrors, CancelPgUpgradeData, CancelPgUpgradeError, CancelPgUpgradeErrors, CancelPgUpgradeResponse, CancelPgUpgradeResponses, CancelResponses, CancelRunData, CancelRunErrors, CancelRunResponse, CancelRunResponses, CancelScheduleRunData, CancelScheduleRunError, CancelScheduleRunErrors, CancelScheduleRunResponse, CancelScheduleRunResponses, CertStatusResponse, ChallengeConfig, ChallengeError, ChallengeValidationStatus, ChangePasswordRequest, ChangePasswordSelfData, ChangePasswordSelfErrors, ChangePasswordSelfResponse, ChangePasswordSelfResponses, ChangeProjectSourceData, ChangeProjectSourceErrors, ChangeProjectSourceRequest, ChangeProjectSourceResponse, ChangeProjectSourceResponses, ChangeRequiredPasswordData, ChangeRequiredPasswordErrors, ChangeRequiredPasswordResponse, ChangeRequiredPasswordResponses, ChatAttachmentReference, ChatAttachmentResponse, ChatAttachmentUpload, ChatCompletionChoice, ChatCompletionRequest, ChatCompletionResponse, ChatCompletionsData, ChatCompletionsError, ChatCompletionsErrors, ChatCompletionsResponse, ChatCompletionsResponses, ChatMessage, ChatReadinessResponse, CheckAnalyticsHasEventsData, CheckAnalyticsHasEventsErrors, CheckAnalyticsHasEventsResponse, CheckAnalyticsHasEventsResponses, CheckCommitExistsData, CheckCommitExistsErrors, CheckCommitExistsResponse, CheckCommitExistsResponses, CheckDomainStatusData, CheckDomainStatusErrors, CheckDomainStatusResponse, CheckDomainStatusResponses, CheckExplorerSupportData, CheckExplorerSupportErrors, CheckExplorerSupportResponse, CheckExplorerSupportResponses, CheckForUpdateData, CheckForUpdateError, CheckForUpdateErrors, CheckForUpdateResponse, CheckForUpdateResponses, CheckIpBlockedData, CheckIpBlockedError, CheckIpBlockedErrors, CheckIpBlockedResponses, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyErrors, CheckProviderDeletionSafetyResponse, CheckProviderDeletionSafetyResponses, ChildBackupEntryResponse, ChildBackupListResponse, ChunkUploadOptionsData, ChunkUploadOptionsResponse, ChunkUploadOptionsResponses, CleanupExpiredBackupsData, CleanupExpiredBackupsError, CleanupExpiredBackupsErrors, CleanupExpiredBackupsRequest, CleanupExpiredBackupsResponse, CleanupExpiredBackupsResponses, ClearPreviewPasswordData, ClearPreviewPasswordErrors, ClearPreviewPasswordResponse, ClearPreviewPasswordResponses, CliDeviceApproveData, CliDeviceApproveErrors, CliDeviceApproveRequest, CliDeviceApproveResponse, CliDeviceApproveResponse2, CliDeviceApproveResponses, CliDeviceDenyData, CliDeviceDenyErrors, CliDeviceDenyResponse, CliDeviceDenyResponses, CliDeviceLookupData, CliDeviceLookupErrors, CliDeviceLookupResponse, CliDeviceLookupResponse2, CliDeviceLookupResponses, CliDevicePollData, CliDevicePollErrors, CliDevicePollRequest, CliDevicePollResponse, CliDevicePollResponse2, CliDevicePollResponses, CliDeviceStartData, CliDeviceStartErrors, CliDeviceStartRequest, CliDeviceStartResponse, CliDeviceStartResponse2, CliDeviceStartResponses, ClientOptions, CliLoginRequest, CliLogoutData, CliLogoutErrors, CliLogoutResponse, CliLogoutResponses, CloudAiCapability, CloudAnalyticsWriteMode, CloudBackfillStatusResponse, CloudCapability, CloudFeatureSwitchesRequest, CloudflareConfig, CloudProvider, CloudSettings, CloudStatus, CloudTelemetryBackfillStatus, CloudTelemetryFidelity, CloudTelemetryWriteMode, CloudTelemetryWriteStatusResponse, ClusterCapacity, ClusterDnsSettings, ClusterDnsStatusData, ClusterDnsStatusErrors, ClusterDnsStatusResponse, ClusterDnsStatusResponse2, ClusterDnsStatusResponses, ClusterHealthReportResponse, ClusterMemberHealthResponse, ClusterMemberRequest, ClusterNetworkSettings, CmdBody, CmdData, CmdErrors, CmdInner, CmdKillBody, CmdKillData, CmdKillErrors, CmdKillResponse, CmdKillResponses, CmdLogsData, CmdLogsErrors, CmdLogsResponses, CmdResponse, CmdResponse2, CmdResponses, CommitExistsResponse, CommitInfo, CommitListResponse, Comparator, ComposePortMapping, ComposePreviewRequest, ComposePreviewResponse, ComposePublicPort, ComposeServiceFamily, ComposeServicePreviewResponse, ConfirmPendingActionData, ConfirmPendingActionErrors, ConfirmPendingActionResponse, ConfirmPendingActionResponses, ConfirmUserPendingActionData, ConfirmUserPendingActionErrors, ConfirmUserPendingActionResponse, ConfirmUserPendingActionResponses, ConnectionLimitSettings, ConnectionListQuery, ConnectionListResponse, ConnectionResponse, ConnectionTestResult, ConsoleEventPayload, ContainerActionResponse, ContainerDetailResponse, ContainerEnvironmentVariableValueResponse, ContainerHistoryEntry, ContainerHistoryListResponse, ContainerInfoResponse, ContainerInventoryItem, ContainerListResponse, ContainerLogSettings, ContainerLogsQuery, ContainerMetricHistoryPoint, ContainerMetricsGetHistoryData, ContainerMetricsGetHistoryErrors, ContainerMetricsGetHistoryResponse, ContainerMetricsGetHistoryResponses, ContainerMetricsHistoryQuery, ContainerMetricsResponse, ContainerResponse, ContainerRuntimeInfo, ContainerStatsSample, ContentPart, ContextLine, ContextLogsRequest, ContextLogsResponse, ContinuousArchiveSourceResponse, ControlApplicationWorkspaceData, ControlApplicationWorkspaceErrors, ControlApplicationWorkspaceRequest, ControlApplicationWorkspaceResponse, ControlApplicationWorkspaceResponses, ConversationDetailResponse, ConversationListScope, ConversationListStatus, ConversationMessagePageResponse, ConversationResponse, ConversationsQueryParams, ConversationSummary, CopyBlobRequest, CostAnalysis, CreatableServiceTypeRoute, CreateAgentData, CreateAgentErrors, CreateAgentResponse, CreateAgentResponses, CreateAlertData, CreateAlertError, CreateAlertErrors, CreateAlertResponse, CreateAlertResponses, CreateAlertRuleData, CreateAlertRuleErrors, CreateAlertRuleRequest, CreateAlertRuleResponse, CreateAlertRuleResponses, CreateAnalyticsIngestKeyData, CreateAnalyticsIngestKeyErrors, CreateAnalyticsIngestKeyRequest, CreateAnalyticsIngestKeyResponse, CreateAnalyticsIngestKeyResponses, CreateApiKeyData, CreateApiKeyErrors, CreateApiKeyRequest, CreateApiKeyResponse, CreateApiKeyResponse2, CreateApiKeyResponses, CreateApplicationConversationData, CreateApplicationConversationErrors, CreateApplicationConversationRequest, CreateApplicationConversationResponse, CreateApplicationConversationResponses, CreateApplicationData, CreateApplicationErrors, CreateApplicationPreviewLinkData, CreateApplicationPreviewLinkErrors, CreateApplicationPreviewLinkRequest, CreateApplicationPreviewLinkResponse, CreateApplicationPreviewLinkResponses, CreateApplicationProjectData, CreateApplicationProjectErrors, CreateApplicationProjectRequest, CreateApplicationProjectResponse, CreateApplicationProjectResponses, CreateApplicationRequest, CreateApplicationResponse, CreateApplicationResponses, CreateBackupScheduleData, CreateBackupScheduleError, CreateBackupScheduleErrors, CreateBackupScheduleRequest, CreateBackupScheduleResponse, CreateBackupScheduleResponses, CreateBitbucketProviderData, CreateBitbucketProviderErrors, CreateBitbucketProviderResponse, CreateBitbucketProviderResponses, CreateBitbucketRequest, CreateBulkActivationJobData, CreateBulkActivationJobError, CreateBulkActivationJobErrors, CreateBulkActivationJobRequest, CreateBulkActivationJobResponse, CreateBulkActivationJobResponses, CreateCloudflareProviderData, CreateCloudflareProviderErrors, CreateCloudflareProviderRequest, CreateCloudflareProviderResponse, CreateCloudflareProviderResponses, CreateConversationData, CreateConversationErrors, CreateConversationRequest, CreateConversationResponse, CreateConversationResponses, CreateCustomDomainData, CreateCustomDomainErrors, CreateCustomDomainResponse, CreateCustomDomainResponses, CreateDashboardData, CreateDashboardError, CreateDashboardErrors, CreateDashboardRequest, CreateDashboardResponse, CreateDashboardResponses, CreateDeploymentTokenData, CreateDeploymentTokenErrors, CreateDeploymentTokenRequest, CreateDeploymentTokenResponse, CreateDeploymentTokenResponse2, CreateDeploymentTokenResponses, CreateDnsProviderData, CreateDnsProviderErrors, CreateDnsProviderRequest, CreateDnsProviderResponse, CreateDnsProviderResponses, CreateDomainData, CreateDomainErrors, CreateDomainRequest, CreateDomainResponse, CreateDomainResponses, CreatedResource, CreateDsnData, CreateDsnErrors, CreateDsnRequest, CreateDsnResponse, CreateDsnResponses, CreateEmailDomainData, CreateEmailDomainErrors, CreateEmailDomainRequest, CreateEmailDomainResponse, CreateEmailDomainResponses, CreateEmailProviderData, CreateEmailProviderErrors, CreateEmailProviderRequest, CreateEmailProviderResponse, CreateEmailProviderResponses, CreateEnvironmentData, CreateEnvironmentErrors, CreateEnvironmentRequest, CreateEnvironmentResponse, CreateEnvironmentResponses, CreateEnvironmentVariableData, CreateEnvironmentVariableErrors, CreateEnvironmentVariableRequest, CreateEnvironmentVariableResponse, CreateEnvironmentVariableResponses, CreateExternalServiceRequest, CreateFacetData, CreateFacetError, CreateFacetErrors, CreateFacetRequest, CreateFacetResponse, CreateFacetResponses, CreateFlagData, CreateFlagErrors, CreateFlagRequest, CreateFlagResponse, CreateFlagResponses, CreateFunnelData, CreateFunnelErrors, CreateFunnelRequest, CreateFunnelResponse, CreateFunnelResponse2, CreateFunnelResponses, CreateFunnelStep, CreateGenericProviderData, CreateGenericProviderErrors, CreateGenericProviderResponse, CreateGenericProviderResponses, CreateGenericRequest, CreateGiteaPatProviderData, CreateGiteaPatProviderErrors, CreateGiteaPatProviderResponse, CreateGiteaPatProviderResponses, CreateGiteaPatRequest, CreateGithubPatProviderData, CreateGithubPatProviderErrors, CreateGithubPatProviderResponse, CreateGithubPatProviderResponses, CreateGitHubPatRequest, CreateGitlabOauthProviderData, CreateGitlabOauthProviderErrors, CreateGitlabOauthProviderResponse, CreateGitlabOauthProviderResponses, CreateGitLabOAuthRequest, CreateGitlabPatProviderData, CreateGitlabPatProviderErrors, CreateGitlabPatProviderResponse, CreateGitlabPatProviderResponses, CreateGitLabPatRequest, CreateGitProviderData, CreateGitProviderErrors, CreateGitProviderResponse, CreateGitProviderResponses, CreateGlobalConversationData, CreateGlobalConversationErrors, CreateGlobalConversationRequest, CreateGlobalConversationResponse, CreateGlobalConversationResponses, CreateGlobalMcpData, CreateGlobalMcpErrors, CreateGlobalMcpResponse, CreateGlobalMcpResponses, CreateGlobalSkillData, CreateGlobalSkillErrors, CreateGlobalSkillResponse, CreateGlobalSkillResponses, CreateGlobalWorkspacePreviewLinkData, CreateGlobalWorkspacePreviewLinkErrors, CreateGlobalWorkspacePreviewLinkResponse, CreateGlobalWorkspacePreviewLinkResponses, CreateIncidentData, CreateIncidentErrors, CreateIncidentRequest, CreateIncidentResponse, CreateIncidentResponses, CreateIntegrationBody, CreateIpAccessControlData, CreateIpAccessControlError, CreateIpAccessControlErrors, CreateIpAccessControlRequest, CreateIpAccessControlResponse, CreateIpAccessControlResponses, CreateMcpData, CreateMcpErrors, CreateMcpRequest, CreateMcpResponse, CreateMcpResponses, CreateMetricAlertRequest, CreateMonitorData, CreateMonitorErrors, CreateMonitorRequest, CreateMonitorResponse, CreateMonitorResponses, CreateNotificationEmailProviderData, CreateNotificationEmailProviderErrors, CreateNotificationEmailProviderRequest, CreateNotificationEmailProviderResponse, CreateNotificationEmailProviderResponses, CreateNotificationProviderData, CreateNotificationProviderErrors, CreateNotificationProviderResponse, CreateNotificationProviderResponses, CreateNotificationRouteData, CreateNotificationRouteErrors, CreateNotificationRouteRequest, CreateNotificationRouteResponse, CreateNotificationRouteResponses, CreateOidcProviderData, CreateOidcProviderErrors, CreateOidcProviderRequest, CreateOidcProviderResponse, CreateOidcProviderResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingRequest, CreateOidcRoleMappingResponse, CreateOidcRoleMappingResponses, CreateOrRecreateOrderData, CreateOrRecreateOrderErrors, CreateOrRecreateOrderResponse, CreateOrRecreateOrderResponses, CreatePlanData, CreatePlanErrors, CreatePlanRequest, CreatePlanResponse, CreatePlanResponse2, CreatePlanResponses, CreatePrData, CreatePrErrors, CreateProjectAccessRequest, CreateProjectData, CreateProjectErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateErrors, CreateProjectFromTemplateRequest, CreateProjectFromTemplateResponse, CreateProjectFromTemplateResponse2, CreateProjectFromTemplateResponses, CreateProjectReleaseData, CreateProjectReleaseErrors, CreateProjectReleaseResponse, CreateProjectReleaseResponses, CreateProjectRequest, CreateProjectResponse, CreateProjectResponses, CreateProjectSecretData, CreateProjectSecretErrors, CreateProjectSecretRequest, CreateProjectSecretResponse, CreateProjectSecretResponses, CreateProviderKeyData, CreateProviderKeyError, CreateProviderKeyErrors, CreateProviderKeyRequest, CreateProviderKeyResponse, CreateProviderKeyResponses, CreateProviderRequest, CreatePrResponse, CreatePrResponse2, CreatePrResponses, CreateReleaseData, CreateReleaseErrors, CreateReleaseResponse, CreateReleaseResponses, CreateRouteData, CreateRouteErrors, CreateRouteRequest, CreateRouteResponse, CreateRouteResponses, CreateS3SourceData, CreateS3SourceError, CreateS3SourceErrors, CreateS3SourceRequest, CreateS3SourceResponse, CreateS3SourceResponses, CreateSandboxBody, CreateSandboxData, CreateSandboxErrors, CreateSandboxResponse, CreateSandboxResponses, CreateServiceData, CreateServiceErrors, CreateServiceResponse, CreateServiceResponses, CreateSkillData, CreateSkillErrors, CreateSkillRequest, CreateSkillResponse, CreateSkillResponses, CreateSlackProviderData, CreateSlackProviderErrors, CreateSlackProviderRequest, CreateSlackProviderResponse, CreateSlackProviderResponses, CreateSnapshotBody, CreateSnapshotData, CreateSnapshotErrors, CreateSnapshotResponse, CreateSnapshotResponses, CreateTeamData, CreateTeamErrors, CreateTeamMemberRequest, CreateTeamRequest, CreateTeamResponse, CreateTeamResponses, CreateThreadArtifactData, CreateThreadArtifactErrors, CreateThreadArtifactRequest, CreateThreadArtifactResponse, CreateThreadArtifactResponses, CreateUserData, CreateUserErrors, CreateUserRequest, CreateUserResponse, CreateUserResponses, CreateWebhookData, CreateWebhookErrors, CreateWebhookProviderData, CreateWebhookProviderErrors, CreateWebhookProviderRequest, CreateWebhookProviderResponse, CreateWebhookProviderResponses, CreateWebhookRequestBody, CreateWebhookResponse, CreateWebhookResponses, CronExecutionInfo, CronInfo, CrossProjectSiblingRef, CrossProjectTraceResponse, CurrentStatusResponse, CustomDomainRequest, CustomDomainResponse, CustomerMovementResponse, DashboardLayout, DashboardProjectsAnalyticsQuery, DashboardProjectsAnalyticsResponse, DashboardSection, DashboardTile, DatabaseMetricsResponse, DatabaseMetricsRow, DataImplication, DataImplicationSeverity, DeactivateApiKeyData, DeactivateApiKeyErrors, DeactivateApiKeyResponse, DeactivateApiKeyResponses, DeactivateConnectionData, DeactivateConnectionErrors, DeactivateConnectionResponses, DeactivateProviderData, DeactivateProviderErrors, DeactivateProviderResponses, DeauthorizeDiscoveredRouteCertData, DeauthorizeDiscoveredRouteCertError, DeauthorizeDiscoveredRouteCertErrors, DeauthorizeDiscoveredRouteCertResponse, DeauthorizeDiscoveredRouteCertResponses, DeleteAgentData, DeleteAgentErrors, DeleteAgentResponse, DeleteAgentResponses, DeleteAlertData, DeleteAlertError, DeleteAlertErrors, DeleteAlertResponse, DeleteAlertResponses, DeleteAlertRuleData, DeleteAlertRuleErrors, DeleteAlertRuleResponse, DeleteAlertRuleResponses, DeleteApiKeyData, DeleteApiKeyErrors, DeleteApiKeyResponse, DeleteApiKeyResponses, DeleteBackupData, DeleteBackupError, DeleteBackupErrors, DeleteBackupResponse, DeleteBackupResponses, DeleteBackupScheduleData, DeleteBackupScheduleError, DeleteBackupScheduleErrors, DeleteBackupScheduleResponse, DeleteBackupScheduleResponses, DeleteBlobRequest, DeleteBlobResponse, DeleteConnectionData, DeleteConnectionErrors, DeleteConnectionResponse, DeleteConnectionResponses, DeleteCustomDomainData, DeleteCustomDomainErrors, DeleteCustomDomainResponse, DeleteCustomDomainResponses, DeleteDashboardData, DeleteDashboardError, DeleteDashboardErrors, DeleteDashboardResponse, DeleteDashboardResponses, DeleteDeploymentTokenData, DeleteDeploymentTokenErrors, DeleteDeploymentTokenResponse, DeleteDeploymentTokenResponses, DeleteDnsProviderData, DeleteDnsProviderErrors, DeleteDnsProviderResponse, DeleteDnsProviderResponses, DeleteDomainData, DeleteDomainErrors, DeleteDomainResponse, DeleteDomainResponses, DeleteEmailDomainData, DeleteEmailDomainErrors, DeleteEmailDomainResponse, DeleteEmailDomainResponses, DeleteEmailProviderData, DeleteEmailProviderErrors, DeleteEmailProviderResponse, DeleteEmailProviderResponses, DeleteEnvironmentData, DeleteEnvironmentDomainData, DeleteEnvironmentDomainErrors, DeleteEnvironmentDomainResponse, DeleteEnvironmentDomainResponses, DeleteEnvironmentErrors, DeleteEnvironmentResponse, DeleteEnvironmentResponses, DeleteEnvironmentVariableData, DeleteEnvironmentVariableErrors, DeleteEnvironmentVariableResponse, DeleteEnvironmentVariableResponses, DeleteExternalImageData, DeleteExternalImageErrors, DeleteExternalImageResponse, DeleteExternalImageResponses, DeleteFacetData, DeleteFacetError, DeleteFacetErrors, DeleteFacetResponse, DeleteFacetResponses, DeleteFunnelData, DeleteFunnelErrors, DeleteFunnelResponses, DeleteGitProviderData, DeleteGitProviderErrors, DeleteGitProviderResponse, DeleteGitProviderResponses, DeleteGlobalMcpData, DeleteGlobalMcpErrors, DeleteGlobalMcpResponse, DeleteGlobalMcpResponses, DeleteGlobalSkillData, DeleteGlobalSkillErrors, DeleteGlobalSkillResponse, DeleteGlobalSkillResponses, DeleteIpAccessControlData, DeleteIpAccessControlError, DeleteIpAccessControlErrors, DeleteIpAccessControlResponse, DeleteIpAccessControlResponses, DeleteMcpData, DeleteMcpErrors, DeleteMcpResponse, DeleteMcpResponses, DeleteMonitorData, DeleteMonitorErrors, DeleteMonitorResponse, DeleteMonitorResponses, DeleteNotificationProviderData, DeleteNotificationProviderErrors, DeleteNotificationProviderResponse, DeleteNotificationProviderResponses, DeleteNotificationRouteData, DeleteNotificationRouteErrors, DeleteNotificationRouteResponse, DeleteNotificationRouteResponses, DeleteOidcProviderData, DeleteOidcProviderResponse, DeleteOidcProviderResponses, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponse, DeleteOidcRoleMappingResponses, DeletePreferencesData, DeletePreferencesErrors, DeletePreferencesResponse, DeletePreferencesResponses, DeleteProjectData, DeleteProjectErrors, DeleteProjectResponse, DeleteProjectResponses, DeleteProjectSecretData, DeleteProjectSecretErrors, DeleteProjectSecretResponse, DeleteProjectSecretResponses, DeleteProviderKeyData, DeleteProviderKeyError, DeleteProviderKeyErrors, DeleteProviderKeyResponse, DeleteProviderKeyResponses, DeleteProviderModelData, DeleteProviderModelError, DeleteProviderModelErrors, DeleteProviderModelResponse, DeleteProviderModelResponses, DeleteProviderSafelyData, DeleteProviderSafelyErrors, DeleteProviderSafelyResponse, DeleteProviderSafelyResponses, DeleteReleaseSourceFilesData, DeleteReleaseSourceFilesErrors, DeleteReleaseSourceFilesResponse, DeleteReleaseSourceFilesResponses, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsErrors, DeleteReleaseSourceMapsResponse, DeleteReleaseSourceMapsResponses, DeleteResponse, DeleteRouteData, DeleteRouteErrors, DeleteRouteResponse, DeleteRouteResponses, DeleteS3SourceData, DeleteS3SourceError, DeleteS3SourceErrors, DeleteS3SourceResponse, DeleteS3SourceResponses, DeleteScanData, DeleteScanError, DeleteScanErrors, DeleteScanResponse, DeleteScanResponses, DeleteSecretData, DeleteSecretErrors, DeleteSecretResponse, DeleteSecretResponses, DeleteServiceData, DeleteServiceErrors, DeleteServiceResponse, DeleteServiceResponses, DeleteSessionReplayData, DeleteSessionReplayError, DeleteSessionReplayErrors, DeleteSessionReplayResponses, DeleteSkillData, DeleteSkillErrors, DeleteSkillResponse, DeleteSkillResponses, DeleteSnapshotData, DeleteSnapshotErrors, DeleteSnapshotResponse, DeleteSnapshotResponses, DeleteSourceMapData, DeleteSourceMapErrors, DeleteSourceMapResponse, DeleteSourceMapResponses, DeleteStaticBundleData, DeleteStaticBundleErrors, DeleteStaticBundleResponse, DeleteStaticBundleResponses, DeleteTeamData, DeleteTeamErrors, DeleteTeamResponse, DeleteTeamResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponse, DeleteUserResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponse, DeleteWebhookResponses, DelRequest, DelResponse, DeployApplicationProjectRequest, DeployApplicationWorkspaceProjectData, DeployApplicationWorkspaceProjectErrors, DeployApplicationWorkspaceProjectResponse, DeployApplicationWorkspaceProjectResponses, DeployFromImageData, DeployFromImageErrors, DeployFromImageRequest, DeployFromImageResponse, DeployFromImageResponses, DeployFromImageUploadData, DeployFromImageUploadErrors, DeployFromImageUploadQuery, DeployFromImageUploadResponse, DeployFromImageUploadResponses, DeployFromStaticData, DeployFromStaticErrors, DeployFromStaticRequest, DeployFromStaticResponse, DeployFromStaticResponses, DeployFromUploadedSourceData, DeployFromUploadedSourceErrors, DeployFromUploadedSourceResponse, DeployFromUploadedSourceResponses, DeploymentConfig, DeploymentConfigSnapshot, DeploymentConfiguration, DeploymentContainerLogContentResponse, DeploymentContainerLogResponse, DeploymentContainerLogsListResponse, DeploymentEnvironmentResponse, DeploymentJobResponse, DeploymentJobsResponse, DeploymentListResponse, DeploymentMetadata, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestErrors, DeploymentMetricsGetLatestResponse, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeErrors, DeploymentMetricsGetRangeResponse, DeploymentMetricsGetRangeResponses, DeploymentMetricsToggleData, DeploymentMetricsToggleErrors, DeploymentMetricsToggleResponses, DeploymentResponse, DeploymentStateResponse, DeploymentStrategy, DeploymentTokenListResponse, DeploymentTokenResponse, DestroySandboxData, DestroySandboxErrors, DestroySandboxResponse, DestroySandboxResponses, DetachScheduleServiceData, DetachScheduleServiceError, DetachScheduleServiceErrors, DetachScheduleServiceResponse, DetachScheduleServiceResponses, DetectionConfig, DetectPublicEnvExampleData, DetectPublicEnvExampleErrors, DetectPublicEnvExampleResponse, DetectPublicEnvExampleResponses, DetectPublicPresetsData, DetectPublicPresetsErrors, DetectPublicPresetsResponse, DetectPublicPresetsResponses, DeviceCount, DigestSections, Direction, DisableBackupScheduleData, DisableBackupScheduleError, DisableBackupScheduleErrors, DisableBackupScheduleResponse, DisableBackupScheduleResponses, DisableBlobResponse, DisableKvResponse, DisableMfaData, DisableMfaErrors, DisableMfaRequest, DisableMfaResponse, DisableMfaResponses, DisconnectCloudData, DisconnectCloudResponse, DisconnectCloudResponses, DiscoverRequest, DiscoverResponse, DiscoverWorkloadsData, DiscoverWorkloadsErrors, DiscoverWorkloadsResponse, DiscoverWorkloadsResponses, DiskInfo, DiskSpaceAlert, DiskSpaceAlertSettings, DiskSpaceCheckResult, DnsAckRequest, DnsAckResponse, DnsChallengeRecordResult, DnsChangesResponse, DnsCompletionResponse, DnsLookupError, DnsLookupRequest, DnsLookupResponse, DnsProviderCredentials, DnsProviderResponse, DnsProviderSettings, DnsProviderSettingsMasked, DnsProviderType, DnsRecord, DnsRecordChange, DnsRecordContent, DnsRecordResponse, DnsRecordSetupResult, DnsRecordStatusResponse, DnsResolverHeartbeat, DnsZone, DockerComposePresetConfig, DockerfilePresetConfig, DockerfileVariant, DockerRegistrySettings, DockerRegistrySettingsMasked, DomainAction, DomainChallengeResponse, DomainData, DomainEnvironmentResponse, DomainError, DomainErrors, DomainPlan, DomainResponse, DomainResponse2, DomainResponses, DownloadApplicationWorkspaceFileData, DownloadApplicationWorkspaceFileErrors, DownloadApplicationWorkspaceFileResponse, DownloadApplicationWorkspaceFileResponses, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveErrors, DownloadGlobalSkillArchiveResponse, DownloadGlobalSkillArchiveResponses, DownloadGlobalWorkspaceFileData, DownloadGlobalWorkspaceFileErrors, DownloadGlobalWorkspaceFileResponse, DownloadGlobalWorkspaceFileResponses, DownloadObjectData, DownloadObjectErrors, DownloadObjectResponse, DownloadObjectResponses, DownloadSkillArchiveData, DownloadSkillArchiveErrors, DownloadSkillArchiveResponse, DownloadSkillArchiveResponses, DrainNodeResponse, DrainStatusResponse, DropArchiveUpload, DropInspectionResponse, DropOffPoint, DropPresetCandidate, EmailConfig, EmailDomainResponse, EmailDomainWithDnsResponse, EmailProviderResponse, EmailProviderTypeRoute, EmailRequest, EmailResponse, EmailStatsResponse, EmailStatusData, EmailStatusErrors, EmailStatusResponse, EmailStatusResponse2, EmailStatusResponses, EmailTrackingResponse, EmailTrackingSetupResponse, EmailTrackingStatusResponse, EmbeddingData, EmbeddingInput, EmbeddingRequest, EmbeddingResponse, EmbeddingsData, EmbeddingsError, EmbeddingsErrors, EmbeddingsResponse, EmbeddingsResponses, EmbeddingUsage, EnableBackupScheduleData, EnableBackupScheduleError, EnableBackupScheduleErrors, EnableBackupScheduleResponse, EnableBackupScheduleResponses, EnableBlobRequest, EnableBlobResponse, EnableKvRequest, EnableKvResponse, EnablePgStatStatementsResponse, EndpointDto, EnqueuedJob, EnrichVisitorData, EnrichVisitorErrors, EnrichVisitorRequest, EnrichVisitorResponse, EnrichVisitorResponse2, EnrichVisitorResponses, EnrollCloudData, EnrollCloudRequest, EnrollCloudResponse, EnrollCloudResponses, EnrollmentTokenInfo, EnrollmentTokenListResponse, EntityInfoResponse, EntityResponse, EnvExampleVariable, EnvExampleVariableResponse, EnvironmentConfiguration, EnvironmentDomainResponse, EnvironmentInfo, EnvironmentResponse, EnvironmentVariable, EnvironmentVariableInfo, EnvironmentVariableResponse, EnvironmentVariableValueResponse, EnvVarInput, EnvVarIntegrationInfo, EnvVarResponse, EnvVarTemplate, EnvVarTemplateResponse, ErrorDashboardStatsQuery, ErrorDashboardStatsResponse, ErrorEventResponse, ErrorGroupDeploymentResponse, ErrorGroupResponse, ErrorGroupStatsResponse, ErrorResponse, ErrorRow, ErrorTimeSeriesDataResponse, ErrorTimeSeriesQuery, EstimateBulkActivationData, EstimateBulkActivationError, EstimateBulkActivationErrors, EstimateBulkActivationRequest, EstimateBulkActivationResponse, EstimateBulkActivationResponses, EventActivityBucket, EventBreakdown, EventBrowserStats, EventCount, EventCountryStats, EventDetailQuery, EventDetailResponse, EventEntriesQuery, EventEntriesResponse, EventEntryInfo, EventKind, EventMetricsPayload, EventReferrerStats, EventsCountQuery, EventsResponse, EventTimeline, EventTimelineQuery, EventType, EventTypeBreakdown, EventTypeBreakdownQuery, EventTypeResponse, EventTypesResponse, EventVisitorInfo, EventVisitorsQuery, EventVisitorsResponse, ExecBody, ExecData, ExecDetachedData, ExecDetachedErrors, ExecDetachedResponse, ExecDetachedResponse2, ExecDetachedResponses, ExecErrors, ExecResponse, ExecResponse2, ExecResponses, ExecuteDeploymentOperationData, ExecuteDeploymentOperationErrors, ExecuteDeploymentOperationResponse, ExecuteDeploymentOperationResponses, ExecuteImportData, ExecuteImportErrors, ExecuteImportRequest, ExecuteImportResponse, ExecuteImportResponse2, ExecuteImportResponses, ExecuteOperationRequest, ExpireRequest, ExpireResponse, ExplorerSupportResponse, ExtendTimeoutBody, ExtendTimeoutData, ExtendTimeoutErrors, ExtendTimeoutResponse, ExtendTimeoutResponses, ExternalImageResponse, ExternalServiceBackupCapabilityResponse, ExternalServiceBackupResponse, ExternalServiceDetails, ExternalServiceEnablePgStatStatementsData, ExternalServiceEnablePgStatStatementsErrors, ExternalServiceEnablePgStatStatementsResponse, ExternalServiceEnablePgStatStatementsResponses, ExternalServiceInfo, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsByDatabaseResponse, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsCreateAlertRuleResponse, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleResponse, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsGetAlertRulesResponse, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsGetLatestResponse, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetRangeResponse, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusErrors, ExternalServiceMetricsStatusResponse, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleResponse, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceResetPgStatStatementsData, ExternalServiceResetPgStatStatementsErrors, ExternalServiceResetPgStatStatementsResponse, ExternalServiceResetPgStatStatementsResponses, ExternalServiceSummary, FacetBackendKind, FacetCapability, FacetInfo, FacetsResponse, FacetStatus, FailureReportPreviewResponse, FeatureMaturity, FieldResponse, FinalizeOrderData, FinalizeOrderErrors, FinalizeOrderResponse, FinalizeOrderResponses, FinalizeProjectReleaseData, FinalizeProjectReleaseErrors, FinalizeProjectReleaseResponse, FinalizeProjectReleaseResponses, FindConversationData, FindConversationErrors, FindConversationResponse, FindConversationResponses, FiringSeriesEntry, FlagEnvironmentResponse, FlagListResponse, FlagResponse, FlagSnapshot, FlagSnapshotResponse, FlagValueType, ForecastAlgorithm, ForecastParams, FullError, FullEvent, FullRequest, FunnelMetricsResponse, FunnelResponse, GatewayStatus, GenAiEvent, GenAiSpanDetail, GenAiTraceDetailResponse, GenAiTraceSummariesResponse, GenAiTraceSummary, GeneralStatsQuery, GeneralStatsResponse, GenerateDockerfileRequest, GenerateDockerfileResponse, GenerateJoinTokenData, GenerateJoinTokenErrors, GenerateJoinTokenResponse, GenerateJoinTokenResponse2, GenerateJoinTokenResponses, GeneratePresetDockerfileData, GeneratePresetDockerfileErrors, GeneratePresetDockerfileResponse, GeneratePresetDockerfileResponses, GeoLocationResponse, GeoRestrictionsConfig, GetAccessInfoData, GetAccessInfoErrors, GetAccessInfoResponse, GetAccessInfoResponses, GetActiveVisitorsData, GetActiveVisitorsErrors, GetActiveVisitorsResponse, GetActiveVisitorsResponses, GetActivityGraphData, GetActivityGraphError, GetActivityGraphErrors, GetActivityGraphResponse, GetActivityGraphResponses, GetAdminGateData, GetAdminGateErrors, GetAdminGateResponse, GetAdminGateResponses, GetAgentData, GetAgentErrors, GetAgentResponse, GetAgentResponses, GetAggregatedBucketsData, GetAggregatedBucketsErrors, GetAggregatedBucketsResponse, GetAggregatedBucketsResponses, GetAiAgentBreakdownData, GetAiAgentBreakdownError, GetAiAgentBreakdownErrors, GetAiAgentBreakdownResponse, GetAiAgentBreakdownResponses, GetAiAgentPagesData, GetAiAgentPagesError, GetAiAgentPagesErrors, GetAiAgentPagesResponse, GetAiAgentPagesResponses, GetAiAgentTimelineData, GetAiAgentTimelineError, GetAiAgentTimelineErrors, GetAiAgentTimelineResponse, GetAiAgentTimelineResponses, GetAiDataAccessData, GetAiDataAccessErrors, GetAiDataAccessResponse, GetAiDataAccessResponses, GetAiPageBreakdownData, GetAiPageBreakdownError, GetAiPageBreakdownErrors, GetAiPageBreakdownResponse, GetAiPageBreakdownResponses, GetAiProviderStatusData, GetAiProviderStatusError, GetAiProviderStatusErrors, GetAiProviderStatusResponse, GetAiProviderStatusResponses, GetAiStatusBreakdownData, GetAiStatusBreakdownError, GetAiStatusBreakdownErrors, GetAiStatusBreakdownResponse, GetAiStatusBreakdownResponses, GetAlertData, GetAlertError, GetAlertErrors, GetAlertResponse, GetAlertResponses, GetAlertRuleData, GetAlertRuleErrors, GetAlertRuleResponse, GetAlertRuleResponses, GetAllRepositoriesByNameData, GetAllRepositoriesByNameErrors, GetAllRepositoriesByNameResponse, GetAllRepositoriesByNameResponses, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsErrors, GetAnalyticsActiveVisitorsResponse, GetAnalyticsActiveVisitorsResponses, GetAnalyticsEventsCountData, GetAnalyticsEventsCountErrors, GetAnalyticsEventsCountResponse, GetAnalyticsEventsCountResponses, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsErrors, GetAnalyticsSessionEventsResponse, GetAnalyticsSessionEventsResponses, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsErrors, GetAnalyticsVisitorSessionsResponse, GetAnalyticsVisitorSessionsResponses, GetApiCallersData, GetApiCallersErrors, GetApiCallersResponse, GetApiCallersResponses, GetApiKeyData, GetApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsErrors, GetApiKeyPermissionsResponse, GetApiKeyPermissionsResponses, GetApiKeyResponse, GetApiKeyResponses, GetApiRoutesData, GetApiRoutesErrors, GetApiRoutesResponse, GetApiRoutesResponses, GetApiSummaryData, GetApiSummaryErrors, GetApiSummaryResponse, GetApiSummaryResponses, GetApiTimeseriesData, GetApiTimeseriesErrors, GetApiTimeseriesResponse, GetApiTimeseriesResponses, GetApiTrafficProxyLogAccessData, GetApiTrafficProxyLogAccessError, GetApiTrafficProxyLogAccessErrors, GetApiTrafficProxyLogAccessResponse, GetApiTrafficProxyLogAccessResponses, GetApplicationData, GetApplicationErrors, GetApplicationResponse, GetApplicationResponses, GetApplicationWorkspaceChangesData, GetApplicationWorkspaceChangesErrors, GetApplicationWorkspaceChangesResponse, GetApplicationWorkspaceChangesResponses, GetApplicationWorkspaceData, GetApplicationWorkspaceDiffData, GetApplicationWorkspaceDiffErrors, GetApplicationWorkspaceDiffResponse, GetApplicationWorkspaceDiffResponses, GetApplicationWorkspaceDirectoryData, GetApplicationWorkspaceDirectoryErrors, GetApplicationWorkspaceDirectoryResponse, GetApplicationWorkspaceDirectoryResponses, GetApplicationWorkspaceErrors, GetApplicationWorkspaceFileData, GetApplicationWorkspaceFileErrors, GetApplicationWorkspaceFileResponse, GetApplicationWorkspaceFileResponses, GetApplicationWorkspaceResponse, GetApplicationWorkspaceResponses, GetAuditLogData, GetAuditLogErrors, GetAuditLogResponse, GetAuditLogResponses, GetBackupData, GetBackupError, GetBackupErrors, GetBackupResponse, GetBackupResponses, GetBackupScheduleData, GetBackupScheduleError, GetBackupScheduleErrors, GetBackupScheduleResponse, GetBackupScheduleResponses, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdErrors, GetBranchesByRepositoryIdResponse, GetBranchesByRepositoryIdResponses, GetBucketedIncidentsData, GetBucketedIncidentsErrors, GetBucketedIncidentsResponse, GetBucketedIncidentsResponses, GetBucketedStatusData, GetBucketedStatusErrors, GetBucketedStatusResponse, GetBucketedStatusResponses, GetBulkActivationJobData, GetBulkActivationJobError, GetBulkActivationJobErrors, GetBulkActivationJobResponse, GetBulkActivationJobResponses, GetChallengeTokenData, GetChallengeTokenErrors, GetChallengeTokenResponse, GetChallengeTokenResponses, GetChatReadinessData, GetChatReadinessErrors, GetChatReadinessResponse, GetChatReadinessResponses, GetCliStatusData, GetCliStatusErrors, GetCliStatusResponses, GetCloudAiCapabilityData, GetCloudAiCapabilityResponse, GetCloudAiCapabilityResponses, GetCloudBackfillStatusData, GetCloudBackfillStatusError, GetCloudBackfillStatusErrors, GetCloudBackfillStatusResponse, GetCloudBackfillStatusResponses, GetCloudCapabilityData, GetCloudCapabilityResponse, GetCloudCapabilityResponses, GetCloudStatusData, GetCloudStatusResponse, GetCloudStatusResponses, GetCloudTelemetryStatusData, GetCloudTelemetryStatusError, GetCloudTelemetryStatusErrors, GetCloudTelemetryStatusResponse, GetCloudTelemetryStatusResponses, GetClusterHealthData, GetClusterHealthErrors, GetClusterHealthResponse, GetClusterHealthResponses, GetClusterMemberData, GetClusterMemberErrors, GetClusterMemberResponse, GetClusterMemberResponses, GetCmdData, GetCmdErrors, GetCmdResponse, GetCmdResponses, GetContainerDetailData, GetContainerDetailErrors, GetContainerDetailResponse, GetContainerDetailResponses, GetContainerEnvironmentVariableData, GetContainerEnvironmentVariableErrors, GetContainerEnvironmentVariableResponse, GetContainerEnvironmentVariableResponses, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerLogsData, GetContainerLogsErrors, GetContainerMetricsData, GetContainerMetricsErrors, GetContainerMetricsResponse, GetContainerMetricsResponses, GetConversationData, GetConversationDetailData, GetConversationDetailError, GetConversationDetailErrors, GetConversationDetailResponse, GetConversationDetailResponses, GetConversationErrors, GetConversationResponse, GetConversationResponses, GetConversationsData, GetConversationsError, GetConversationsErrors, GetConversationsResponse, GetConversationsResponses, GetCronByIdData, GetCronByIdErrors, GetCronByIdResponse, GetCronByIdResponses, GetCronExecutionsData, GetCronExecutionsErrors, GetCronExecutionsResponse, GetCronExecutionsResponses, GetCrossProjectTraceSiblingsData, GetCrossProjectTraceSiblingsError, GetCrossProjectTraceSiblingsErrors, GetCrossProjectTraceSiblingsResponse, GetCrossProjectTraceSiblingsResponses, GetCurrentBulkActivationJobData, GetCurrentBulkActivationJobError, GetCurrentBulkActivationJobErrors, GetCurrentBulkActivationJobResponse, GetCurrentBulkActivationJobResponses, GetCurrentMonitorStatusData, GetCurrentMonitorStatusErrors, GetCurrentMonitorStatusResponse, GetCurrentMonitorStatusResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponse, GetCurrentUserResponses, GetCustomDomainData, GetCustomDomainErrors, GetCustomDomainResponse, GetCustomDomainResponses, GetDashboardData, GetDashboardError, GetDashboardErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsErrors, GetDashboardProjectsAnalyticsResponse, GetDashboardProjectsAnalyticsResponses, GetDashboardResponse, GetDashboardResponses, GetDeliveryData, GetDeliveryErrors, GetDeliveryResponse, GetDeliveryResponses, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentErrors, GetDeploymentContainerLogContentResponse, GetDeploymentContainerLogContentResponses, GetDeploymentData, GetDeploymentErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsErrors, GetDeploymentJobLogsResponse, GetDeploymentJobLogsResponses, GetDeploymentJobsData, GetDeploymentJobsErrors, GetDeploymentJobsResponse, GetDeploymentJobsResponses, GetDeploymentOperationsData, GetDeploymentOperationsErrors, GetDeploymentOperationsResponse, GetDeploymentOperationsResponses, GetDeploymentOperationStatusData, GetDeploymentOperationStatusErrors, GetDeploymentOperationStatusResponse, GetDeploymentOperationStatusResponses, GetDeploymentResponse, GetDeploymentResponses, GetDeploymentsParams, GetDeploymentTokenData, GetDeploymentTokenErrors, GetDeploymentTokenResponse, GetDeploymentTokenResponses, GetDiskStatusData, GetDiskStatusErrors, GetDiskStatusResponse, GetDiskStatusResponses, GetDnsChangesData, GetDnsChangesErrors, GetDnsChangesResponse, GetDnsChangesResponses, GetDnsProviderData, GetDnsProviderErrors, GetDnsProviderResponse, GetDnsProviderResponses, GetDomainByHostData, GetDomainByHostErrors, GetDomainByHostResponse, GetDomainByHostResponses, GetDomainByIdData, GetDomainByIdErrors, GetDomainByIdResponse, GetDomainByIdResponses, GetDomainByNameData, GetDomainByNameErrors, GetDomainByNameResponse, GetDomainByNameResponses, GetDomainData, GetDomainDnsRecordsData, GetDomainDnsRecordsErrors, GetDomainDnsRecordsResponse, GetDomainDnsRecordsResponses, GetDomainErrors, GetDomainOrderData, GetDomainOrderErrors, GetDomainOrderResponse, GetDomainOrderResponses, GetDomainResponse, GetDomainResponses, GetEmailData, GetEmailErrors, GetEmailEventsData, GetEmailEventsErrors, GetEmailEventsResponse, GetEmailEventsResponses, GetEmailLinksData, GetEmailLinksErrors, GetEmailLinksResponse, GetEmailLinksResponses, GetEmailProviderData, GetEmailProviderErrors, GetEmailProviderResponse, GetEmailProviderResponses, GetEmailResponse, GetEmailResponses, GetEmailStatsData, GetEmailStatsErrors, GetEmailStatsResponse, GetEmailStatsResponses, GetEmailTrackingData, GetEmailTrackingErrors, GetEmailTrackingResponse, GetEmailTrackingResponses, GetEmailTrackingStatusData, GetEmailTrackingStatusErrors, GetEmailTrackingStatusResponse, GetEmailTrackingStatusResponses, GetEntityInfoData, GetEntityInfoErrors, GetEntityInfoResponse, GetEntityInfoResponses, GetEnvironmentCronsData, GetEnvironmentCronsErrors, GetEnvironmentCronsResponse, GetEnvironmentCronsResponses, GetEnvironmentData, GetEnvironmentDomainsData, GetEnvironmentDomainsErrors, GetEnvironmentDomainsResponse, GetEnvironmentDomainsResponses, GetEnvironmentErrors, GetEnvironmentResponse, GetEnvironmentResponses, GetEnvironmentsData, GetEnvironmentsErrors, GetEnvironmentsResponse, GetEnvironmentsResponses, GetEnvironmentVariablesData, GetEnvironmentVariablesErrors, GetEnvironmentVariablesQuery, GetEnvironmentVariablesResponse, GetEnvironmentVariablesResponses, GetEnvironmentVariableValueData, GetEnvironmentVariableValueErrors, GetEnvironmentVariableValueResponse, GetEnvironmentVariableValueResponses, GetErrorDashboardStatsData, GetErrorDashboardStatsErrors, GetErrorDashboardStatsResponse, GetErrorDashboardStatsResponses, GetErrorEventData, GetErrorEventErrors, GetErrorEventResponse, GetErrorEventResponses, GetErrorGroupData, GetErrorGroupErrors, GetErrorGroupResponse, GetErrorGroupResponses, GetErrorStatsData, GetErrorStatsErrors, GetErrorStatsResponse, GetErrorStatsResponses, GetErrorTimeSeriesData, GetErrorTimeSeriesErrors, GetErrorTimeSeriesResponse, GetErrorTimeSeriesResponses, GetEventDetailData, GetEventDetailErrors, GetEventDetailResponse, GetEventDetailResponses, GetEventEntriesData, GetEventEntriesErrors, GetEventEntriesResponse, GetEventEntriesResponses, GetEventsCountData, GetEventsCountErrors, GetEventsCountResponse, GetEventsCountResponses, GetEventsTimelineData, GetEventsTimelineErrors, GetEventsTimelineResponse, GetEventsTimelineResponses, GetEventTypeBreakdownData, GetEventTypeBreakdownErrors, GetEventTypeBreakdownResponse, GetEventTypeBreakdownResponses, GetEventVisitorsData, GetEventVisitorsErrors, GetEventVisitorsResponse, GetEventVisitorsResponses, GetExternalImageData, GetExternalImageErrors, GetExternalImageResponse, GetExternalImageResponses, GetExternalServiceBackupCapabilityData, GetExternalServiceBackupCapabilityError, GetExternalServiceBackupCapabilityErrors, GetExternalServiceBackupCapabilityResponse, GetExternalServiceBackupCapabilityResponses, GetFailureReportPreviewData, GetFailureReportPreviewErrors, GetFailureReportPreviewResponse, GetFailureReportPreviewResponses, GetFeatureMaturityData, GetFeatureMaturityErrors, GetFeatureMaturityResponse, GetFeatureMaturityResponses, GetFileData, GetFileErrors, GetFileResponse, GetFileResponses, GetFlagData, GetFlagErrors, GetFlagResponse, GetFlagResponses, GetFlagSnapshotData, GetFlagSnapshotErrors, GetFlagSnapshotResponse, GetFlagSnapshotResponses, GetFunnelMetricsData, GetFunnelMetricsErrors, GetFunnelMetricsQuery, GetFunnelMetricsResponse, GetFunnelMetricsResponses, GetGenaiTraceData, GetGenaiTraceError, GetGenaiTraceErrors, GetGenaiTraceResponse, GetGenaiTraceResponses, GetGeneralStatsData, GetGeneralStatsErrors, GetGeneralStatsResponse, GetGeneralStatsResponses, GetGitProviderData, GetGitProviderErrors, GetGitProviderResponse, GetGitProviderResponses, GetGlobalAiWorkspaceData, GetGlobalAiWorkspaceErrors, GetGlobalAiWorkspaceResponse, GetGlobalAiWorkspaceResponses, GetGlobalEventsData, GetGlobalEventsErrors, GetGlobalEventsResponse, GetGlobalEventsResponses, GetGlobalEventStatsData, GetGlobalEventStatsErrors, GetGlobalEventStatsResponse, GetGlobalEventStatsResponses, GetGlobalMcpData, GetGlobalMcpErrors, GetGlobalMcpResponse, GetGlobalMcpResponses, GetGlobalSandboxStatusData, GetGlobalSandboxStatusErrors, GetGlobalSandboxStatusResponse, GetGlobalSandboxStatusResponses, GetGlobalSkillData, GetGlobalSkillErrors, GetGlobalSkillResponse, GetGlobalSkillResponses, GetGlobalWorkspaceChangesData, GetGlobalWorkspaceChangesErrors, GetGlobalWorkspaceChangesResponse, GetGlobalWorkspaceChangesResponses, GetGlobalWorkspaceDiffData, GetGlobalWorkspaceDiffErrors, GetGlobalWorkspaceDiffResponse, GetGlobalWorkspaceDiffResponses, GetGlobalWorkspaceDirectoryData, GetGlobalWorkspaceDirectoryErrors, GetGlobalWorkspaceDirectoryResponse, GetGlobalWorkspaceDirectoryResponses, GetGlobalWorkspaceFileData, GetGlobalWorkspaceFileErrors, GetGlobalWorkspaceFileResponse, GetGlobalWorkspaceFileResponses, GetGroupedPageMetricsData, GetGroupedPageMetricsError, GetGroupedPageMetricsErrors, GetGroupedPageMetricsResponse, GetGroupedPageMetricsResponses, GetHealthData, GetHealthError, GetHealthErrors, GetHealthResponse, GetHealthResponses, GetHourlyVisitsData, GetHourlyVisitsErrors, GetHourlyVisitsResponse, GetHourlyVisitsResponses, GetHttpChallengeDebugData, GetHttpChallengeDebugErrors, GetHttpChallengeDebugResponse, GetHttpChallengeDebugResponses, GetImportStatusData, GetImportStatusErrors, GetImportStatusResponse, GetImportStatusResponses, GetIncidentData, GetIncidentErrors, GetIncidentResponse, GetIncidentResponses, GetIncidentUpdatesData, GetIncidentUpdatesErrors, GetIncidentUpdatesResponse, GetIncidentUpdatesResponses, GetIngestErrorsData, GetIngestErrorsError, GetIngestErrorsErrors, GetIngestErrorsResponse, GetIngestErrorsResponses, GetIpAccessControlData, GetIpAccessControlError, GetIpAccessControlErrors, GetIpAccessControlResponse, GetIpAccessControlResponses, GetIpGeolocationData, GetIpGeolocationError, GetIpGeolocationErrors, GetIpGeolocationResponse, GetIpGeolocationResponses, GetJoinTokenStatusData, GetJoinTokenStatusErrors, GetJoinTokenStatusResponse, GetJoinTokenStatusResponses, GetLastDeploymentData, GetLastDeploymentErrors, GetLastDeploymentResponse, GetLastDeploymentResponses, GetLatestDeploymentMediaData, GetLatestDeploymentMediaError, GetLatestDeploymentMediaErrors, GetLatestDeploymentMediaResponse, GetLatestDeploymentMediaResponses, GetLatestScanData, GetLatestScanError, GetLatestScanErrors, GetLatestScanResponse, GetLatestScanResponses, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentError, GetLatestScansPerEnvironmentErrors, GetLatestScansPerEnvironmentResponse, GetLatestScansPerEnvironmentResponses, GetLiveVisitorsListData, GetLiveVisitorsListErrors, GetLiveVisitorsListResponse, GetLiveVisitorsListResponses, GetLogContextData, GetLogContextError, GetLogContextErrors, GetLogContextResponse, GetLogContextResponses, GetMcpData, GetMcpErrors, GetMcpResponse, GetMcpResponses, GetMetricsOverTimeData, GetMetricsOverTimeError, GetMetricsOverTimeErrors, GetMetricsOverTimeResponse, GetMetricsOverTimeResponses, GetMonitorData, GetMonitorErrors, GetMonitorResponse, GetMonitorResponses, GetNotificationProviderData, GetNotificationProviderErrors, GetNotificationProviderResponse, GetNotificationProviderResponses, GetNotificationRouteData, GetNotificationRouteErrors, GetNotificationRouteResponse, GetNotificationRouteResponses, GetOnDemandCertStatusData, GetOnDemandCertStatusErrors, GetOnDemandCertStatusResponse, GetOnDemandCertStatusResponses, GetOrCreateDsnData, GetOrCreateDsnErrors, GetOrCreateDsnRequest, GetOrCreateDsnResponse, GetOrCreateDsnResponses, GetPageFlowData, GetPageFlowErrors, GetPageFlowResponse, GetPageFlowResponses, GetPageHourlySessionsData, GetPageHourlySessionsErrors, GetPageHourlySessionsResponse, GetPageHourlySessionsResponses, GetPagePathDetailData, GetPagePathDetailErrors, GetPagePathDetailResponse, GetPagePathDetailResponses, GetPagePathsData, GetPagePathsErrors, GetPagePathsResponse, GetPagePathsResponses, GetPagePathsSparklinesData, GetPagePathsSparklinesErrors, GetPagePathsSparklinesResponse, GetPagePathsSparklinesResponses, GetPagePathVisitorsData, GetPagePathVisitorsErrors, GetPagePathVisitorsResponse, GetPagePathVisitorsResponses, GetPendingActionData, GetPendingActionErrors, GetPendingActionResponse, GetPendingActionResponses, GetPerformanceMetricsData, GetPerformanceMetricsError, GetPerformanceMetricsErrors, GetPerformanceMetricsResponse, GetPerformanceMetricsResponses, GetPgUpgradeData, GetPgUpgradeError, GetPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsError, GetPgUpgradeLogsErrors, GetPgUpgradeLogsResponse, GetPgUpgradeLogsResponses, GetPgUpgradeResponse, GetPgUpgradeResponses, GetPipelineHistoryData, GetPipelineHistoryError, GetPipelineHistoryErrors, GetPipelineHistoryResponse, GetPipelineHistoryResponses, GetPipelineStatsData, GetPipelineStatsError, GetPipelineStatsErrors, GetPipelineStatsResponse, GetPipelineStatsResponses, GetPlatformInfoData, GetPlatformInfoErrors, GetPlatformInfoResponse, GetPlatformInfoResponses, GetPostgresWalHealthData, GetPostgresWalHealthErrors, GetPostgresWalHealthResponse, GetPostgresWalHealthResponses, GetPreferencesData, GetPreferencesErrors, GetPreferencesResponse, GetPreferencesResponses, GetPreviewGatewayLogsData, GetPreviewGatewayLogsError, GetPreviewGatewayLogsErrors, GetPreviewGatewayLogsResponse, GetPreviewGatewayLogsResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponse, GetPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusError, GetPreviewGatewayStatusErrors, GetPreviewGatewayStatusResponse, GetPreviewGatewayStatusResponses, GetPricingData, GetPricingError, GetPricingErrors, GetPricingResponse, GetPricingResponses, GetPrivateIpData, GetPrivateIpErrors, GetPrivateIpResponses, GetProjectAlarmsSummaryData, GetProjectAlarmsSummaryErrors, GetProjectAlarmsSummaryResponse, GetProjectAlarmsSummaryResponses, GetProjectBySlugData, GetProjectBySlugErrors, GetProjectBySlugResponse, GetProjectBySlugResponses, GetProjectCloudTelemetryData, GetProjectCloudTelemetryError, GetProjectCloudTelemetryErrors, GetProjectCloudTelemetryResponse, GetProjectCloudTelemetryResponses, GetProjectData, GetProjectDeploymentsData, GetProjectDeploymentsErrors, GetProjectDeploymentsResponse, GetProjectDeploymentsResponses, GetProjectErrors, GetProjectResponse, GetProjectResponses, GetProjectsData, GetProjectSecretsQuery, GetProjectsErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesErrors, GetProjectServiceEnvironmentVariablesResponse, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceTemplateData, GetProjectServiceTemplateErrors, GetProjectServiceTemplateResponse, GetProjectServiceTemplateResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysError, GetProjectSessionReplaysErrors, GetProjectSessionReplaysQuery, GetProjectSessionReplaysResponse, GetProjectSessionReplaysResponse2, GetProjectSessionReplaysResponses, GetProjectsHealthData, GetProjectsHealthError, GetProjectsHealthErrors, GetProjectsHealthResponse, GetProjectsHealthResponses, GetProjectsMonitorHealthData, GetProjectsMonitorHealthErrors, GetProjectsMonitorHealthResponse, GetProjectsMonitorHealthResponses, GetProjectsResponse, GetProjectsResponses, GetProjectStatisticsData, GetProjectStatisticsErrors, GetProjectStatisticsResponse, GetProjectStatisticsResponses, GetProjectTemplateData, GetProjectTemplateErrors, GetProjectTemplateResponse, GetProjectTemplateResponses, GetPropertyBreakdownData, GetPropertyBreakdownErrors, GetPropertyBreakdownResponse, GetPropertyBreakdownResponses, GetPropertyTimelineData, GetPropertyTimelineErrors, GetPropertyTimelineResponse, GetPropertyTimelineResponses, GetProviderConnectionsData, GetProviderConnectionsErrors, GetProviderConnectionsResponse, GetProviderConnectionsResponses, GetProviderKeyData, GetProviderKeyError, GetProviderKeyErrors, GetProviderKeyResponse, GetProviderKeyResponses, GetProviderMetadataData, GetProviderMetadataErrors, GetProviderMetadataResponse, GetProviderMetadataResponses, GetProvidersMetadataData, GetProvidersMetadataErrors, GetProvidersMetadataResponse, GetProvidersMetadataResponses, GetProxyLogByIdData, GetProxyLogByIdError, GetProxyLogByIdErrors, GetProxyLogByIdResponse, GetProxyLogByIdResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdError, GetProxyLogByRequestIdErrors, GetProxyLogByRequestIdResponse, GetProxyLogByRequestIdResponses, GetProxyLogsData, GetProxyLogsError, GetProxyLogsErrors, GetProxyLogsResponse, GetProxyLogsResponses, GetPublicBranchesData, GetPublicBranchesErrors, GetPublicBranchesResponse, GetPublicBranchesResponses, GetPublicComposePreviewData, GetPublicComposePreviewErrors, GetPublicComposePreviewResponse, GetPublicComposePreviewResponses, GetPublicComposeServicesData, GetPublicComposeServicesErrors, GetPublicComposeServicesResponse, GetPublicComposeServicesResponses, GetPublicIpData, GetPublicIpErrors, GetPublicIpResponses, GetPublicRepositoryData, GetPublicRepositoryErrors, GetPublicRepositoryResponse, GetPublicRepositoryResponses, GetQueryContainerInfoData, GetQueryContainerInfoErrors, GetQueryContainerInfoResponse, GetQueryContainerInfoResponses, GetQuotaData, GetQuotaError, GetQuotaErrors, GetQuotaResponse, GetQuotaResponses, GetRecentActivityData, GetRecentActivityErrors, GetRecentActivityResponse, GetRecentActivityResponses, GetRemoteExternalImageData, GetRemoteExternalImageErrors, GetRemoteExternalImageResponse, GetRemoteExternalImageResponses, GetRepositoryBranchesData, GetRepositoryBranchesErrors, GetRepositoryBranchesResponse, GetRepositoryBranchesResponses, GetRepositoryByIdData, GetRepositoryByIdErrors, GetRepositoryByIdResponse, GetRepositoryByIdResponses, GetRepositoryByNameData, GetRepositoryByNameErrors, GetRepositoryByNameResponse, GetRepositoryByNameResponses, GetRepositoryComposePreviewData, GetRepositoryComposePreviewErrors, GetRepositoryComposePreviewResponse, GetRepositoryComposePreviewResponses, GetRepositoryComposeServicesLiveData, GetRepositoryComposeServicesLiveErrors, GetRepositoryComposeServicesLiveResponse, GetRepositoryComposeServicesLiveResponses, GetRepositoryEnvExampleLiveData, GetRepositoryEnvExampleLiveErrors, GetRepositoryEnvExampleLiveResponse, GetRepositoryEnvExampleLiveResponses, GetRepositoryPresetByNameData, GetRepositoryPresetByNameErrors, GetRepositoryPresetByNameResponse, GetRepositoryPresetByNameResponses, GetRepositoryPresetLiveData, GetRepositoryPresetLiveErrors, GetRepositoryPresetLiveResponse, GetRepositoryPresetLiveResponses, GetRepositoryTagsData, GetRepositoryTagsErrors, GetRepositoryTagsResponse, GetRepositoryTagsResponses, GetRequest, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariablesResponse, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueErrors, GetResolvedEnvironmentVariableValueResponse, GetResolvedEnvironmentVariableValueResponses, GetResponse, GetRestoreCapabilitiesData, GetRestoreCapabilitiesError, GetRestoreCapabilitiesErrors, GetRestoreCapabilitiesResponse, GetRestoreCapabilitiesResponses, GetRestoreRunData, GetRestoreRunError, GetRestoreRunErrors, GetRestoreRunResponse, GetRestoreRunResponses, GetRouteData, GetRouteErrors, GetRouteResponse, GetRouteResponses, GetRunData, GetRunErrors, GetRunResponse, GetRunResponses, GetRunWithLogsData, GetRunWithLogsErrors, GetRunWithLogsResponse, GetRunWithLogsResponses, GetS3CredentialsData, GetS3CredentialsErrors, GetS3CredentialsResponse, GetS3CredentialsResponses, GetS3SourceData, GetS3SourceError, GetS3SourceErrors, GetS3SourceResponse, GetS3SourceResponses, GetSandboxData, GetSandboxErrors, GetSandboxResponse, GetSandboxResponses, GetSandboxStatusData, GetSandboxStatusErrors, GetSandboxStatusResponse, GetSandboxStatusResponses, GetScanByDeploymentData, GetScanByDeploymentError, GetScanByDeploymentErrors, GetScanByDeploymentResponse, GetScanByDeploymentResponses, GetScanData, GetScanError, GetScanErrors, GetScanResponse, GetScanResponses, GetScanVulnerabilitiesData, GetScanVulnerabilitiesError, GetScanVulnerabilitiesErrors, GetScanVulnerabilitiesResponse, GetScanVulnerabilitiesResponses, GetServiceBySlugData, GetServiceBySlugErrors, GetServiceBySlugResponse, GetServiceBySlugResponses, GetServiceData, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableErrors, GetServiceEnvironmentVariableResponse, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariablesResponse, GetServiceEnvironmentVariablesResponses, GetServiceErrors, GetServiceHealthStatusData, GetServiceHealthStatusErrors, GetServiceHealthStatusResponse, GetServiceHealthStatusResponses, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesErrors, GetServicePreviewEnvironmentVariableNamesResponse, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariablesMaskedResponse, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServiceResponse, GetServiceResponses, GetServiceRuntimeData, GetServiceRuntimeErrors, GetServiceRuntimeResponse, GetServiceRuntimeResponses, GetServiceStatsData, GetServiceStatsErrors, GetServiceStatsResponse, GetServiceStatsResponses, GetServiceTypeParametersData, GetServiceTypeParametersErrors, GetServiceTypeParametersResponses, GetServiceTypesData, GetServiceTypesErrors, GetServiceTypesResponse, GetServiceTypesResponses, GetSessionDetailsData, GetSessionDetailsErrors, GetSessionDetailsResponse, GetSessionDetailsResponses, GetSessionEventsData, GetSessionEventsErrors, GetSessionEventsResponse, GetSessionEventsResponses, GetSessionLogsData, GetSessionLogsErrors, GetSessionLogsResponse, GetSessionLogsResponses, GetSessionReplayData, GetSessionReplayError, GetSessionReplayErrors, GetSessionReplayEventsData, GetSessionReplayEventsError, GetSessionReplayEventsErrors, GetSessionReplayEventsResponse, GetSessionReplayEventsResponses, GetSessionReplayResponse, GetSessionReplayResponse2, GetSessionReplayResponses, GetSettingsData, GetSettingsErrors, GetSettingsResponse, GetSettingsResponses, GetSkillData, GetSkillErrors, GetSkillResponse, GetSkillResponses, GetSlowQueriesData, GetSlowQueriesErrors, GetSlowQueriesResponse, GetSlowQueriesResponses, GetSnapshotData, GetSnapshotErrors, GetSnapshotResponse, GetSnapshotResponses, GetStaticBundleData, GetStaticBundleErrors, GetStaticBundleResponse, GetStaticBundleResponses, GetStatusOverviewData, GetStatusOverviewErrors, GetStatusOverviewResponse, GetStatusOverviewResponses, GetSystemAlarmsSummaryData, GetSystemAlarmsSummaryErrors, GetSystemAlarmsSummaryResponse, GetSystemAlarmsSummaryResponses, GetTagsByRepositoryIdData, GetTagsByRepositoryIdErrors, GetTagsByRepositoryIdResponse, GetTagsByRepositoryIdResponses, GetTeamData, GetTeamErrors, GetTeamResponse, GetTeamResponses, GetTimeBucketStatsData, GetTimeBucketStatsError, GetTimeBucketStatsErrors, GetTimeBucketStatsResponse, GetTimeBucketStatsResponses, GetTodayStatsData, GetTodayStatsError, GetTodayStatsErrors, GetTodayStatsResponse, GetTodayStatsResponses, GetTraceData, GetTraceError, GetTraceErrors, GetTraceResponse, GetTraceResponses, GetTraefikDiscoveryStatusData, GetTraefikDiscoveryStatusError, GetTraefikDiscoveryStatusErrors, GetTraefikDiscoveryStatusResponse, GetTraefikDiscoveryStatusResponses, GetUnifiedTraceData, GetUnifiedTraceError, GetUnifiedTraceErrors, GetUnifiedTraceResponse, GetUnifiedTraceResponses, GetUniqueCountsData, GetUniqueCountsErrors, GetUniqueCountsResponse, GetUniqueCountsResponses, GetUniqueEventsData, GetUniqueEventsErrors, GetUniqueEventsQuery, GetUniqueEventsResponse, GetUniqueEventsResponses, GetUpdateCapabilityData, GetUpdateCapabilityErrors, GetUpdateCapabilityResponse, GetUpdateCapabilityResponses, GetUpdateStatusData, GetUpdateStatusErrors, GetUpdateStatusResponse, GetUpdateStatusResponses, GetUptimeHistoryData, GetUptimeHistoryErrors, GetUptimeHistoryResponse, GetUptimeHistoryResponses, GetUsageByProviderData, GetUsageByProviderError, GetUsageByProviderErrors, GetUsageByProviderResponse, GetUsageByProviderResponses, GetUsageRecentData, GetUsageRecentError, GetUsageRecentErrors, GetUsageRecentResponse, GetUsageRecentResponses, GetUsageSummaryData, GetUsageSummaryError, GetUsageSummaryErrors, GetUsageSummaryResponse, GetUsageSummaryResponses, GetUsageTimeseriesData, GetUsageTimeseriesError, GetUsageTimeseriesErrors, GetUsageTimeseriesResponse, GetUsageTimeseriesResponses, GetUsageTopModelsData, GetUsageTopModelsError, GetUsageTopModelsErrors, GetUsageTopModelsResponse, GetUsageTopModelsResponses, GetUserConversationAttachmentData, GetUserConversationAttachmentErrors, GetUserConversationAttachmentResponse, GetUserConversationAttachmentResponses, GetUserConversationData, GetUserConversationErrors, GetUserConversationResponse, GetUserConversationResponses, GetUserPendingActionData, GetUserPendingActionErrors, GetUserPendingActionResponse, GetUserPendingActionResponses, GetVisibleCustomDomainByHostnameData, GetVisibleCustomDomainByHostnameErrors, GetVisibleCustomDomainByHostnameResponse, GetVisibleCustomDomainByHostnameResponses, GetVisitorByGuidData, GetVisitorByGuidErrors, GetVisitorByGuidResponse, GetVisitorByGuidResponses, GetVisitorByIdData, GetVisitorByIdErrors, GetVisitorByIdResponse, GetVisitorByIdResponses, GetVisitorDetailsData, GetVisitorDetailsErrors, GetVisitorDetailsResponse, GetVisitorDetailsResponses, GetVisitorFacetsData, GetVisitorFacetsErrors, GetVisitorFacetsResponse, GetVisitorFacetsResponses, GetVisitorInfoData, GetVisitorInfoErrors, GetVisitorInfoResponse, GetVisitorInfoResponses, GetVisitorJourneyData, GetVisitorJourneyErrors, GetVisitorJourneyResponse, GetVisitorJourneyResponses, GetVisitorsData, GetVisitorsErrors, GetVisitorSessionsData, GetVisitorSessionsError, GetVisitorSessionsErrors, GetVisitorSessionsQuery, GetVisitorSessionsResponse, GetVisitorSessionsResponse2, GetVisitorSessionsResponses, GetVisitorsResponse, GetVisitorsResponses, GetVisitorStatsData, GetVisitorStatsErrors, GetVisitorStatsResponse, GetVisitorStatsResponses, GetWebhookData, GetWebhookErrors, GetWebhookResponse, GetWebhookResponses, GetWorkspaceFileLimitsData, GetWorkspaceFileLimitsErrors, GetWorkspaceFileLimitsResponse, GetWorkspaceFileLimitsResponses, GitPushEvent, GitRef, GitRefResponse, GitSourcePlan, GlobalConversationResponse, GlobalEventStatsResponse, GlobalMrrResponse, GlobalRecentEventResponse, GlobalRevenueSummaryResponse, GrantProjectAccessData, GrantProjectAccessErrors, GrantProjectAccessResponse, GrantProjectAccessResponses, GroupedPageMetric, GroupedPageMetricsQuery, GroupedPageMetricsResponse, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, HasAnalyticsEventsData, HasAnalyticsEventsErrors, HasAnalyticsEventsResponse, HasAnalyticsEventsResponse2, HasAnalyticsEventsResponses, HasErrorGroupsData, HasErrorGroupsErrors, HasErrorGroupsResponse, HasErrorGroupsResponse2, HasErrorGroupsResponses, HasEventsQuery, HasEventsResponse, HasMetricsQuery, HasMetricsResponse, HasPerformanceMetricsData, HasPerformanceMetricsError, HasPerformanceMetricsErrors, HasPerformanceMetricsResponse, HasPerformanceMetricsResponses, HasTracesData, HasTracesError, HasTracesErrors, HasTracesResponse, HasTracesResponse2, HasTracesResponses, HealthCheckConfiguration, HealthCheckEntryResponse, HealthResponse, HealthStatus, HealthSummary, HeartbeatApiRequest, HeartbeatResponse, HierarchyLevel, HistogramSummary, HostnameChange, HostnamePreviewResponse, HourlyPageSessions, HourlyVisitsQuery, HttpChallengeDebugResponse, ImageRetentionSettings, ImageRuntimeConfig, ImportApplicationWorkspaceGitData, ImportApplicationWorkspaceGitErrors, ImportApplicationWorkspaceGitRequest, ImportApplicationWorkspaceGitResponse, ImportApplicationWorkspaceGitResponses, ImportCredentials, ImportedHostVerdict, ImportEmailDomainData, ImportEmailDomainErrors, ImportEmailDomainRequest, ImportEmailDomainResponse, ImportEmailDomainResponses, ImportExecutionStatus, ImportExternalServiceData, ImportExternalServiceErrors, ImportExternalServiceRequest, ImportExternalServiceResponse, ImportExternalServiceResponses, ImportOutcomeResponse, ImportPlan, ImportRowErrorResponse, ImportSelector, ImportSource, ImportSourceCapabilities, ImportSourceInfo, ImportStatusResponse, ImportTraefikAcmeJsonData, ImportTraefikAcmeJsonError, ImportTraefikAcmeJsonErrors, ImportTraefikAcmeJsonRequest, ImportTraefikAcmeJsonResponse, ImportTraefikAcmeJsonResponse2, ImportTraefikAcmeJsonResponses, IncidentBucket, IncidentBucketedResponse, IncidentResponse, IncidentUpdateResponse, IncrRequest, IncrResponse, IngestErrorsResponse, IngestErrorSummary, IngestLogsByPathData, IngestLogsByPathError, IngestLogsByPathErrors, IngestLogsByPathResponses, IngestLogsData, IngestLogsError, IngestLogsErrors, IngestLogsResponses, IngestMetricsByPathData, IngestMetricsByPathError, IngestMetricsByPathErrors, IngestMetricsByPathResponses, IngestMetricsData, IngestMetricsError, IngestMetricsErrors, IngestMetricsResponses, IngestSentryEnvelopeData, IngestSentryEnvelopeErrors, IngestSentryEnvelopeResponses, IngestSentryEventData, IngestSentryEventErrors, IngestSentryEventResponse, IngestSentryEventResponses, IngestTracesByPathData, IngestTracesByPathError, IngestTracesByPathErrors, IngestTracesByPathResponses, IngestTracesData, IngestTracesError, IngestTracesErrors, IngestTracesResponses, IngestTunneledEnvelopeData, IngestTunneledEnvelopeErrors, IngestTunneledEnvelopeResponse, IngestTunneledEnvelopeResponses, InitAuthResponse, InitSessionReplayData, InitSessionReplayError, InitSessionReplayErrors, InitSessionReplayResponse, InitSessionReplayResponses, Insight, InsightSeverity, InsightsResponse, InsightStatus, InspectDropArchiveData, InspectDropArchiveErrors, InspectDropArchiveResponse, InspectDropArchiveResponses, IntegrationResponse, IpAccessControlQuery, IpAccessControlResponse, IssueRuntimeCredentialsData, IssueRuntimeCredentialsErrors, IssueRuntimeCredentialsResponse, IssueRuntimeCredentialsResponses, JobLogsData, JobLogsErrors, JobLogsResponses, JobStatusData, JobStatusErrors, JobStatusResponse, JobStatusResponse2, JobStatusResponses, JobSummaryResponse, JoinTokenStatusResponse, JourneyEvent, JourneySession, KeysRequest, KeysResponse, KillJobBody, KillJobData, KillJobErrors, KillJobResponse, KillJobResponses, KnownAiAgentsResponse, KvDelData, KvDelErrors, KvDelResponse, KvDelResponses, KvDisableData, KvDisableErrors, KvDisableResponse, KvDisableResponses, KvEnableData, KvEnableErrors, KvEnableResponse, KvEnableResponses, KvExpireData, KvExpireErrors, KvExpireResponse, KvExpireResponses, KvGetData, KvGetErrors, KvGetResponse, KvGetResponses, KvIncrData, KvIncrErrors, KvIncrResponse, KvIncrResponses, KvKeysData, KvKeysErrors, KvKeysResponse, KvKeysResponses, KvSetData, KvSetErrors, KvSetResponse, KvSetResponses, KvStatusData, KvStatusErrors, KvStatusResponse, KvStatusResponse2, KvStatusResponses, KvTtlData, KvTtlErrors, KvTtlResponse, KvTtlResponses, KvUpdateData, KvUpdateErrors, KvUpdateResponse, KvUpdateResponses, LatestDeploymentMediaResponse, LatestDeploymentMediaResponseItem, LatestRunForSourceData, LatestRunForSourceErrors, LatestRunForSourceResponse, LatestRunForSourceResponses, LemonSqueezyConfig, LetsEncryptSettings, LineContext, LinkApplicationProjectData, LinkApplicationProjectErrors, LinkApplicationProjectRequest, LinkApplicationProjectResponse, LinkApplicationProjectResponses, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateErrors, LinkCustomDomainToCertificateResponse, LinkCustomDomainToCertificateResponses, LinkServiceRequest, LinkServiceToProjectData, LinkServiceToProjectErrors, LinkServiceToProjectResponse, LinkServiceToProjectResponses, ListAgentRunsData, ListAgentRunsErrors, ListAgentRunsResponse, ListAgentRunsResponses, ListAgentsData, ListAgentsErrors, ListAgentsResponse, ListAgentsResponse2, ListAgentsResponses, ListAiProvidersData, ListAiProvidersErrors, ListAiProvidersResponse, ListAiProvidersResponses, ListAlertRulesData, ListAlertRulesErrors, ListAlertRulesResponse, ListAlertRulesResponses, ListAlertsData, ListAlertsError, ListAlertsErrors, ListAlertsResponse, ListAlertsResponses, ListAllConversationsData, ListAllConversationsErrors, ListAllConversationsResponse, ListAllConversationsResponses, ListAllRunsData, ListAllRunsErrors, ListAllRunsResponse, ListAllRunsResponses, ListAnalyticsIngestKeysData, ListAnalyticsIngestKeysErrors, ListAnalyticsIngestKeysResponse, ListAnalyticsIngestKeysResponses, ListApiKeysData, ListApiKeysErrors, ListApiKeysQuery, ListApiKeysResponse, ListApiKeysResponses, ListApplicationConversationsData, ListApplicationConversationsErrors, ListApplicationConversationsResponse, ListApplicationConversationsResponses, ListApplicationsData, ListApplicationsErrors, ListApplicationsResponse, ListApplicationsResponses, ListAuditLogsData, ListAuditLogsErrors, ListAuditLogsQuery, ListAuditLogsResponse, ListAuditLogsResponses, ListAvailableContainersData, ListAvailableContainersErrors, ListAvailableContainersResponse, ListAvailableContainersResponses, ListBackupAlertsData, ListBackupAlertsError, ListBackupAlertsErrors, ListBackupAlertsResponse, ListBackupAlertsResponses, ListBackupChildrenData, ListBackupChildrenError, ListBackupChildrenErrors, ListBackupChildrenResponse, ListBackupChildrenResponses, ListBackupSchedulesData, ListBackupSchedulesError, ListBackupSchedulesErrors, ListBackupSchedulesResponse, ListBackupSchedulesResponses, ListBackupsForScheduleData, ListBackupsForScheduleError, ListBackupsForScheduleErrors, ListBackupsForScheduleResponse, ListBackupsForScheduleResponses, ListBlobsQuery, ListBlobsResponse, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdErrors, ListCommitsByRepositoryIdResponse, ListCommitsByRepositoryIdResponses, ListConnectionsData, ListConnectionsErrors, ListConnectionsResponse, ListConnectionsResponses, ListContainerHistoryData, ListContainerHistoryError, ListContainerHistoryErrors, ListContainerHistoryResponse, ListContainerHistoryResponses, ListContainersAtPathData, ListContainersAtPathErrors, ListContainersAtPathResponse, ListContainersAtPathResponses, ListContainersData, ListContainersErrors, ListContainersResponse, ListContainersResponses, ListConversationsData, ListConversationsErrors, ListConversationsResponse, ListConversationsResponses, ListCustomDomainsForProjectData, ListCustomDomainsForProjectErrors, ListCustomDomainsForProjectResponse, ListCustomDomainsForProjectResponses, ListCustomDomainsResponse, ListDashboardsData, ListDashboardsError, ListDashboardsErrors, ListDashboardsResponse, ListDashboardsResponses, ListDeliveriesData, ListDeliveriesErrors, ListDeliveriesResponse, ListDeliveriesResponses, ListDeploymentContainerLogsData, ListDeploymentContainerLogsErrors, ListDeploymentContainerLogsResponse, ListDeploymentContainerLogsResponses, ListDeploymentTokensData, ListDeploymentTokensErrors, ListDeploymentTokensQuery, ListDeploymentTokensResponse, ListDeploymentTokensResponses, ListDiscoverableDomainsData, ListDiscoverableDomainsErrors, ListDiscoverableDomainsResponse, ListDiscoverableDomainsResponses, ListDiscoveredRoutesQuery, ListDnsProvidersData, ListDnsProvidersErrors, ListDnsProvidersResponse, ListDnsProvidersResponses, ListDomainsData, ListDomainsErrors, ListDomainsResponse, ListDomainsResponse2, ListDomainsResponses, ListDsnsData, ListDsnsErrors, ListDsnsResponse, ListDsnsResponses, ListEmailDomainProjectsData, ListEmailDomainProjectsErrors, ListEmailDomainProjectsResponse, ListEmailDomainProjectsResponses, ListEmailDomainsData, ListEmailDomainsErrors, ListEmailDomainsResponse, ListEmailDomainsResponses, ListEmailProvidersData, ListEmailProvidersErrors, ListEmailProvidersResponse, ListEmailProvidersResponses, ListEmailsData, ListEmailsErrors, ListEmailsResponse, ListEmailsResponses, ListEnrollmentTokensData, ListEnrollmentTokensErrors, ListEnrollmentTokensResponse, ListEnrollmentTokensResponses, ListEntitiesData, ListEntitiesErrors, ListEntitiesQuery, ListEntitiesResponse, ListEntitiesResponses, ListErrorEventsData, ListErrorEventsErrors, ListErrorEventsQuery, ListErrorEventsResponse, ListErrorEventsResponses, ListErrorGroupsData, ListErrorGroupsErrors, ListErrorGroupsQuery, ListErrorGroupsResponse, ListErrorGroupsResponses, ListEventsData, ListEventsResponse, ListEventsResponses, ListEventTypesData, ListEventTypesResponse, ListEventTypesResponses, ListExternalImagesData, ListExternalImagesErrors, ListExternalImagesResponse, ListExternalImagesResponses, ListExternalPluginsData, ListExternalPluginsErrors, ListExternalPluginsResponse, ListExternalPluginsResponses, ListExternalServiceBackupsData, ListExternalServiceBackupsError, ListExternalServiceBackupsErrors, ListExternalServiceBackupsResponse, ListExternalServiceBackupsResponses, ListFacetsData, ListFacetsError, ListFacetsErrors, ListFacetsResponse, ListFacetsResponses, ListFlagsData, ListFlagsErrors, ListFlagsResponse, ListFlagsResponses, ListFunnelsData, ListFunnelsErrors, ListFunnelsResponse, ListFunnelsResponses, ListGitProvidersData, ListGitProvidersErrors, ListGitProvidersResponse, ListGitProvidersResponses, ListGlobalMcpsData, ListGlobalMcpsErrors, ListGlobalMcpsResponse, ListGlobalMcpsResponses, ListGlobalSkillsData, ListGlobalSkillsErrors, ListGlobalSkillsResponse, ListGlobalSkillsResponses, ListIncidentsData, ListIncidentsErrors, ListIncidentsResponses, ListInsightsData, ListInsightsError, ListInsightsErrors, ListInsightsResponse, ListInsightsResponses, ListIpAccessControlData, ListIpAccessControlError, ListIpAccessControlErrors, ListIpAccessControlResponse, ListIpAccessControlResponses, ListJobsData, ListJobsErrors, ListJobsResponse, ListJobsResponse2, ListJobsResponses, ListKnownAiAgentsData, ListKnownAiAgentsError, ListKnownAiAgentsErrors, ListKnownAiAgentsResponse, ListKnownAiAgentsResponses, ListManagedDomainsData, ListManagedDomainsErrors, ListManagedDomainsResponse, ListManagedDomainsResponses, ListManagedEnvironmentVariablesData, ListManagedEnvironmentVariablesErrors, ListManagedEnvironmentVariablesResponse, ListManagedEnvironmentVariablesResponses, ListMcpsData, ListMcpsErrors, ListMcpsResponse, ListMcpsResponse2, ListMcpsResponses, ListMetricLabelKeysData, ListMetricLabelKeysError, ListMetricLabelKeysErrors, ListMetricLabelKeysResponse, ListMetricLabelKeysResponses, ListMetricLabelValuesData, ListMetricLabelValuesError, ListMetricLabelValuesErrors, ListMetricLabelValuesResponse, ListMetricLabelValuesResponses, ListMetricNamesData, ListMetricNamesError, ListMetricNamesErrors, ListMetricNamesResponse, ListMetricNamesResponses, ListModelsData, ListModelsError, ListModelsErrors, ListModelsResponse, ListModelsResponses, ListMonitorsData, ListMonitorsErrors, ListMonitorsResponse, ListMonitorsResponses, ListNotificationProvidersData, ListNotificationProvidersErrors, ListNotificationProvidersResponse, ListNotificationProvidersResponses, ListNotificationRoutesData, ListNotificationRoutesErrors, ListNotificationRoutesResponse, ListNotificationRoutesResponses, ListOidcProvidersData, ListOidcProvidersResponse, ListOidcProvidersResponses, ListOidcProviderUsersData, ListOidcProviderUsersErrors, ListOidcProviderUsersResponse, ListOidcProviderUsersResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponse, ListOidcRoleMappingsResponses, ListOnDemandCertsData, ListOnDemandCertsErrors, ListOnDemandCertsResponse, ListOnDemandCertsResponse2, ListOnDemandCertsResponses, ListOrdersData, ListOrdersErrors, ListOrdersResponse, ListOrdersResponse2, ListOrdersResponses, ListPeersData, ListPeersErrors, ListPeersResponse, ListPeersResponses, ListPendingActionsData, ListPendingActionsErrors, ListPendingActionsResponse, ListPendingActionsResponses, ListPgUpgradesData, ListPgUpgradesError, ListPgUpgradesErrors, ListPgUpgradesResponse, ListPgUpgradesResponses, ListPresetsData, ListPresetsErrors, ListPresetsResponse, ListPresetsResponse2, ListPresetsResponses, ListProjectAccessData, ListProjectAccessErrors, ListProjectAccessResponse, ListProjectAccessResponses, ListProjectAlarmsData, ListProjectAlarmsErrors, ListProjectAlarmsResponse, ListProjectAlarmsResponses, ListProjectScansData, ListProjectScansError, ListProjectScansErrors, ListProjectScansResponse, ListProjectScansResponses, ListProjectSecretsData, ListProjectSecretsErrors, ListProjectSecretsResponse, ListProjectSecretsResponses, ListProjectServicesData, ListProjectServicesErrors, ListProjectServicesResponse, ListProjectServicesResponses, ListProjectTemplatesData, ListProjectTemplatesErrors, ListProjectTemplatesResponse, ListProjectTemplatesResponses, ListProjectTemplateTagsData, ListProjectTemplateTagsErrors, ListProjectTemplateTagsResponse, ListProjectTemplateTagsResponses, ListProviderDomainsResponse, ListProviderKeysData, ListProviderKeysError, ListProviderKeysErrors, ListProviderKeysResponse, ListProviderKeysResponses, ListProviderZonesData, ListProviderZonesErrors, ListProviderZonesResponse, ListProviderZonesResponses, ListPublicProvidersData, ListPublicProvidersResponse, ListPublicProvidersResponses, ListReleaseFilesData, ListReleaseFilesErrors, ListReleaseFilesResponse, ListReleaseFilesResponses, ListReleasesData, ListReleasesErrors, ListReleasesResponse, ListReleasesResponses, ListRemoteExternalImagesData, ListRemoteExternalImagesErrors, ListRemoteExternalImagesResponse, ListRemoteExternalImagesResponses, ListRenewalAttemptsData, ListRenewalAttemptsErrors, ListRenewalAttemptsResponse, ListRenewalAttemptsResponse2, ListRenewalAttemptsResponses, ListRepositoriesByConnectionData, ListRepositoriesByConnectionErrors, ListRepositoriesByConnectionResponse, ListRepositoriesByConnectionResponses, ListRepositoriesByProviderData, ListRepositoriesByProviderErrors, ListRepositoriesByProviderResponse, ListRepositoriesByProviderResponses, ListRestoreRunsForServiceData, ListRestoreRunsForServiceError, ListRestoreRunsForServiceErrors, ListRestoreRunsForServiceResponse, ListRestoreRunsForServiceResponses, ListRootContainersData, ListRootContainersErrors, ListRootContainersResponse, ListRootContainersResponses, ListRoutesData, ListRoutesErrors, ListRoutesResponse, ListRoutesResponses, ListRunsResponse, ListS3SourcesData, ListS3SourcesError, ListS3SourcesErrors, ListS3SourcesResponse, ListS3SourcesResponses, ListSandboxesData, ListSandboxesErrors, ListSandboxesResponse, ListSandboxesResponse2, ListSandboxesResponses, ListScansQuery, ListScheduleRunJobsData, ListScheduleRunJobsError, ListScheduleRunJobsErrors, ListScheduleRunJobsResponse, ListScheduleRunJobsResponses, ListScheduleRunsData, ListScheduleRunsError, ListScheduleRunsErrors, ListScheduleRunsResponse, ListScheduleRunsResponses, ListScheduleServicesData, ListScheduleServicesError, ListScheduleServicesErrors, ListScheduleServicesResponse, ListScheduleServicesResponses, ListSecretsData, ListSecretsErrors, ListSecretsResponse, ListSecretsResponse2, ListSecretsResponses, ListServiceHealthStatusesData, ListServiceHealthStatusesErrors, ListServiceHealthStatusesResponse, ListServiceHealthStatusesResponses, ListServiceProjectsData, ListServiceProjectsErrors, ListServiceProjectsResponse, ListServiceProjectsResponses, ListServiceSchedulesData, ListServiceSchedulesError, ListServiceSchedulesErrors, ListServiceSchedulesResponse, ListServiceSchedulesResponses, ListServicesData, ListServicesErrors, ListServicesResponse, ListServicesResponses, ListSkillsData, ListSkillsErrors, ListSkillsResponse, ListSkillsResponse2, ListSkillsResponses, ListSnapshotsData, ListSnapshotsErrors, ListSnapshotsResponse, ListSnapshotsResponse2, ListSnapshotsResponses, ListSourceBackupsData, ListSourceBackupsError, ListSourceBackupsErrors, ListSourceBackupsResponse, ListSourceBackupsResponses, ListSourceFilesData, ListSourceFilesErrors, ListSourceFilesResponse, ListSourceFilesResponses, ListSourceMapsData, ListSourceMapsErrors, ListSourceMapsResponse, ListSourceMapsResponses, ListSourcesData, ListSourcesErrors, ListSourcesResponse, ListSourcesResponses, ListStaticBundlesData, ListStaticBundlesErrors, ListStaticBundlesResponse, ListStaticBundlesResponses, ListSyncedRepositoriesData, ListSyncedRepositoriesErrors, ListSyncedRepositoriesResponse, ListSyncedRepositoriesResponses, ListSystemAlarmsData, ListSystemAlarmsErrors, ListSystemAlarmsResponse, ListSystemAlarmsResponses, ListTagsResponse, ListTeamMembersData, ListTeamMembersErrors, ListTeamMembersResponse, ListTeamMembersResponses, ListTeamProjectsData, ListTeamProjectsErrors, ListTeamProjectsResponse, ListTeamProjectsResponses, ListTeamsData, ListTeamsErrors, ListTeamsResponse, ListTeamsResponses, ListTemplatesQuery, ListTemplatesResponse, ListThreadArtifactsData, ListThreadArtifactsErrors, ListThreadArtifactsResponse, ListThreadArtifactsResponses, ListTraefikDiscoveredRoutesData, ListTraefikDiscoveredRoutesError, ListTraefikDiscoveredRoutesErrors, ListTraefikDiscoveredRoutesResponse, ListTraefikDiscoveredRoutesResponses, ListUserPendingActionsData, ListUserPendingActionsErrors, ListUserPendingActionsResponse, ListUserPendingActionsResponses, ListUsersData, ListUsersErrors, ListUsersResponse, ListUsersResponses, ListVulnerabilitiesQuery, ListWebhooksData, ListWebhooksErrors, ListWebhooksResponse, ListWebhooksResponses, LiveVisitorInfo, LiveVisitorsListResponse, LocationCount, LocationGranularity, LocationInfo, LoginData, LoginErrors, LoginRequest, LoginResponse, LoginResponses, LogLevel, LogoutData, LogoutErrors, LogoutResponses, LogRecord, LogSearchLine, LogSeverity, LogSource, LogsQuery, LogsResponse, LogStream, LookupDnsARecordsData, LookupDnsARecordsError, LookupDnsARecordsErrors, LookupDnsARecordsResponse, LookupDnsARecordsResponses, ManagedBackupSetup, ManagedBackupSetupAction, ManagedBackupSetupStatus, ManagedDomainResponse, ManagedEnvironmentVariable, ManagedEnvironmentVariableSource, ManualAction, ManualActionTiming, Maturity, McpDefinitionResponse, McpServerSettings, MessageContent, MessagePart, MessageResponse, MeteredMode, MetricAggregation, MetricBucket, MetricDataPoint, MetricsOverTimeResponse, MetricsQuery, MetricsRangeQuery, MetricsStatusResponse, MetricsStoreKind, MetricsSummaryResponse, MetricType, MfaRequiredResponse, MfaSetupResponse, MfaVerificationRequest, MigrationStep, MigrationSummary, MintEnrollmentTokenData, MintEnrollmentTokenErrors, MintEnrollmentTokenRequest, MintEnrollmentTokenResponse, MintEnrollmentTokenResponse2, MintEnrollmentTokenResponses, MiscResult, MkdirBody, MkdirData, MkdirErrors, MkdirResponse, MkdirResponses, ModelCapability, ModelInfo, ModelListResponse, ModelPricing, ModelUsage, MonitoringSettings, MonitoringSettingsMasked, MonitorResponse, MonitorStatus, MrrBucketResponse, MultiNodeSettings, MultiNodeSettingsMasked, MxResult, NavEntry, NavSection, NetworkConfiguration, NetworkMode, NetworkPoolEntry, NixpacksPresetConfig, NixpacksProvider, NodeContainerListResponse, NodeContainerResponse, NodeCostInfo, NodeDnsStatusEntry, NodeHeartbeatData, NodeHeartbeatErrors, NodeHeartbeatResponse, NodeHeartbeatResponses, NodeInfoResponse, NodeListResponse, NodeMetricsGetAlertRulesData, NodeMetricsGetAlertRulesErrors, NodeMetricsGetAlertRulesResponse, NodeMetricsGetAlertRulesResponses, NodeMetricsGetRangeData, NodeMetricsGetRangeErrors, NodeMetricsGetRangeResponse, NodeMetricsGetRangeResponses, NodeMetricsUpdateAlertRuleData, NodeMetricsUpdateAlertRuleErrors, NodeMetricsUpdateAlertRuleResponse, NodeMetricsUpdateAlertRuleResponses, NotificationPreferencesResponse, NotificationProviderResponse, NotificationRoute, NotificationRoutePage, ObservabilityCompressionSettings, ObservabilityEvent, ObservabilityFullEventData, ObservabilityFullEventError, ObservabilityFullEventErrors, ObservabilityFullEventResponse, ObservabilityFullEventResponses, ObservabilityListEventsData, ObservabilityListEventsError, ObservabilityListEventsErrors, ObservabilityListEventsResponse, ObservabilityListEventsResponses, ObservabilityRetentionSettings, OidcCallbackData, OidcProviderResponse, OidcProvidersListResponse, OidcProviderSummary, OidcProviderUserResponse, OidcRoleMappingResponse, OidcTestConnectionResponse, OnDemandCertAttemptResponse, OnDemandCertRow, OnDemandTlsSettings, OpenAiError, OpenAiErrorResponse, OperatingSystemCount, OperationResultResponse, OperationResultsResponse, OtelDashboardResponse, OtelDashboardsResponse, OtelMetricAlertRuleResponse, OtelMetricAlertsResponse, OtelMetricLabelKeysResponse, OtelMetricLabelValuesResponse, OtelMetricNamesResponse, OtelMetricsResponse, OutlierAlgorithm, OutlierParams, OverprovisioningAssessment, OverprovisioningVerdict, PageActivityBucket, PageCountryStats, PageFlowEntry, PageFlowQuery, PageFlowResponse, PageHourlySessionsQuery, PageHourlySessionsResponse, PagePathDetailQuery, PagePathDetailResponse, PagePathInfo, PagePathSparkline, PagePathSparklinePoint, PagePathsQuery, PagePathsResponse, PagePathsSparklineQuery, PagePathsSparklineResponse, PagePathVisitorsQuery, PagePathVisitorsResponse, PageReferrerStats, PagesComparisonResponse, PageSessionComparison, PageSessionStats, PageSessionStatsQuery, PageTransition, PageVisit, PageVisitorSession, PaginatedEmailsResponse, PaginatedEntitiesResponse, PaginatedErrorEventsResponse, PaginatedErrorGroupsResponse, PaginatedEventsResponse, PaginatedExternalImagesResponse, PaginatedProjectList, PaginatedStaticBundlesResponse, Pagination, PaginationMeta, PaginationParams, PasswordProtectionConfig, PatchAdminGateData, PatchAdminGateErrors, PatchAdminGateResponse, PatchAdminGateResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsError, PatchPreviewGatewaySettingsErrors, PatchPreviewGatewaySettingsResponse, PatchPreviewGatewaySettingsResponses, PatchSettingsRequest, PathVisitors, PathVisitorsAnalyticsQuery, PathVisitorsResponse, PauseDeploymentData, PauseDeploymentErrors, PauseDeploymentResponse, PauseDeploymentResponses, PauseSandboxData, PauseSandboxErrors, PauseSandboxResponse, PauseSandboxResponses, PeerEntry, PeerListResponse, PendingActionResponse, PerformanceMetricsQuery, PerformanceMetricsResponse, PermissionDecision, PermissionInfo, PermissionKind, PermissionRequest, PermissionRequestedEvent, PgUpgradeLogResponse, PgUpgradeResponse, PipelineHistoryPoint, PipelineHistoryResponse, PipelineSeries, PipelineStats, PipelineStatsResponse, PlanComplexity, PlanMetadata, PlanRestoreData, PlanRestoreError, PlanRestoreErrors, PlanRestoreResponse, PlanRestoreResponses, PlanSourceBackup, PlanTarget, PlatformInfo, PluginCapability, PluginManifest, PortMapping, PostDnsAckData, PostDnsAckErrors, PostDnsAckResponse, PostDnsAckResponses, PostgresWalHealth, PresetConfigSchema, PresetInfo, PresetResponse, PreviewAlertData, PreviewAlertError, PreviewAlertErrors, PreviewAlertResponse, PreviewAlertResponses, PreviewFunnelMetricsData, PreviewFunnelMetricsErrors, PreviewFunnelMetricsResponse, PreviewFunnelMetricsResponses, PreviewGatewayLogsResponse, PreviewGatewaySettings, PreviewGatewaySettingsMasked, PreviewGatewaySettingsResponse, PreviewHostnameModeData, PreviewHostnameModeErrors, PreviewHostnameModeResponse, PreviewHostnameModeResponses, PreviewShareLinkBody, PreviewShareLinkResponse, PricingResponse, ProblemDetails, ProjectAccessResponse, ProjectCloudTelemetryResponse, ProjectConfiguration, ProjectDashboardAnalytics, ProjectDsnResponse, ProjectEnvVarInput, ProjectHealthSummary, ProjectHourlyRequestCount, ProjectInfo, ProjectMonitorHealth, ProjectPresetResponse, ProjectQuery, ProjectRef, ProjectResponse, ProjectSecretEnvironmentInfo, ProjectSecretResponse, ProjectServiceInfo, ProjectsHealthResponse, ProjectsMonitorHealthResponse, ProjectStatisticsResponse, ProjectStatsBreakdown, ProjectTemplate, ProjectType, ProjectUsageInfoResponse, PromoteClusterMemberData, PromoteClusterMemberErrors, PromoteClusterMemberResponses, PromoteDeploymentData, PromoteDeploymentErrors, PromoteDeploymentRequest, PromoteDeploymentResponse, PromoteDeploymentResponses, PropertyBreakdownItem, PropertyBreakdownQuery, PropertyBreakdownResponse, PropertyColumn, PropertyTimelineItem, PropertyTimelineQuery, PropertyTimelineResponse, Protocol, ProviderCatalogDto, ProviderCatalogResponse, ProviderConfig, ProviderConfigMasked, ProviderDeletionCheckResponse, ProviderDescriptor, ProviderDetailResponse, ProviderDomainIdentityResponse, ProviderKeyResponse, ProviderMetadata, ProviderModelResponse, ProviderResponse, ProviderUsage, ProvisionDomainData, ProvisionDomainErrors, ProvisionDomainResponse, ProvisionDomainResponses, ProvisionResponse, ProxyLogAccessResponse, ProxyLogResponse, ProxyLogsPaginatedResponse, PublicComposePreviewRequest, PublicComposePreviewResponse, PublicComposeServicePreview, PublicComposeServicesResponse, PublicEnvExampleResponse, PublicHostnameStrategy, PublicPresetResponse, PublicRepositoryInfo, PurgeLogsRequest, PurgeProjectLogsData, PurgeProjectLogsError, PurgeProjectLogsErrors, PurgeProjectLogsResponses, PushedExternalImageResponse, PushExternalImageData, PushExternalImageErrors, PushExternalImageResponse, PushExternalImageResponses, PushImageRequest, QueryDataData, QueryDataErrors, QueryDataRequest, QueryDataResponse, QueryDataResponse2, QueryDataResponses, QueryGenaiTracesData, QueryGenaiTracesError, QueryGenaiTracesErrors, QueryGenaiTracesResponse, QueryGenaiTracesResponses, QueryLogsData, QueryLogsError, QueryLogsErrors, QueryLogsResponse, QueryLogsResponses, QueryMetricsData, QueryMetricsError, QueryMetricsErrors, QueryMetricsResponse, QueryMetricsResponses, QuerySpanStatsData, QuerySpanStatsError, QuerySpanStatsErrors, QuerySpanStatsResponse, QuerySpanStatsResponses, QueryTracesData, QueryTracesError, QueryTracesErrors, QueryTracesResponse, QueryTracesResponses, QueryTraceSummariesData, QueryTraceSummariesError, QueryTraceSummariesErrors, QueryTraceSummariesResponse, QueryTraceSummariesResponses, QuotaResponse, RateLimitConfig, RateLimitSettings, ReachabilityStatus, ReadEntityRowsData, ReadEntityRowsErrors, ReadEntityRowsResponse, ReadEntityRowsResponses, ReadFileData, ReadFileErrors, ReadFileResponse, ReadFileResponse2, ReadFileResponses, ReadRowsQuery, ReAnalyzeData, ReAnalyzeErrors, ReAnalyzeResponses, ReassignCustomDomainRequest, ReassignProjectCustomDomainData, ReassignProjectCustomDomainErrors, ReassignProjectCustomDomainResponse, ReassignProjectCustomDomainResponses, RecentActivityQuery, RecentActivityResponse, RecentEventResponse, RecentQueryParams, ReconcileCloudBackupSourceData, ReconcileCloudBackupSourceResponse, ReconcileCloudBackupSourceResponses, RecordConsoleEventData, RecordConsoleEventErrors, RecordConsoleEventResponses, RecordEventMetricsData, RecordEventMetricsErrors, RecordEventMetricsResponse, RecordEventMetricsResponses, RecordExposureRequest, RecordExposureResponse, RecordFlagExposureData, RecordFlagExposureErrors, RecordFlagExposureResponse, RecordFlagExposureResponses, RecordListResponse, RecordSpeedMetricsData, RecordSpeedMetricsError, RecordSpeedMetricsErrors, RecordSpeedMetricsResponse, RecordSpeedMetricsResponses, RecoveryTarget, ReferrerCount, ReferrersAnalyticsQuery, RefreshAiProviderStatusData, RefreshAiProviderStatusError, RefreshAiProviderStatusErrors, RefreshAiProviderStatusResponse, RefreshAiProviderStatusResponses, RefreshProviderModelsData, RefreshProviderModelsError, RefreshProviderModelsErrors, RefreshProviderModelsResponse, RefreshProviderModelsResponses, RefreshRouteTableData, RefreshRouteTableErrors, RefreshRouteTableResponse, RefreshRouteTableResponses, RegenerateDsnData, RegenerateDsnErrors, RegenerateDsnRequest, RegenerateDsnResponse, RegenerateDsnResponses, RegisterExternalImageData, RegisterExternalImageErrors, RegisterExternalImageResponse, RegisterExternalImageResponses, RegisterImageRequest, RegisterNodeApiRequest, RegisterNodeData, RegisterNodeErrors, RegisterNodeResponse, RegisterNodeResponse2, RegisterNodeResponses, RegisterRequest, ReinstallGitlabWebhookData, ReinstallGitlabWebhookErrors, ReinstallGitlabWebhookResponse, ReinstallGitlabWebhookResponses, ReinstallWebhookResponse, RejectPendingActionData, RejectPendingActionErrors, RejectPendingActionResponse, RejectPendingActionResponses, RejectUserPendingActionData, RejectUserPendingActionErrors, RejectUserPendingActionResponse, RejectUserPendingActionResponses, ReleaseCheckResult, ReleaseListResponse, ReloadPluginsData, ReloadPluginsErrors, ReloadPluginsResponse, ReloadPluginsResponses, ReloadResponse, RemoteDeploymentResponse, RemoveClusterMemberData, RemoveClusterMemberErrors, RemoveClusterMemberResponse, RemoveClusterMemberResponses, RemoveManagedDomainData, RemoveManagedDomainErrors, RemoveManagedDomainResponse, RemoveManagedDomainResponses, RemoveNodeResponse, RemoveRoleData, RemoveRoleErrors, RemoveRoleResponse, RemoveRoleResponses, RemoveTeamMemberData, RemoveTeamMemberErrors, RemoveTeamMemberResponse, RemoveTeamMemberResponses, RenameConversationData, RenameConversationErrors, RenameConversationRequest, RenameConversationResponse, RenameConversationResponses, RenameUserConversationData, RenameUserConversationErrors, RenameUserConversationResponse, RenameUserConversationResponses, RenewalAttemptResponse, RenewDomainData, RenewDomainErrors, RenewDomainResponse, RenewDomainResponses, RepointContinuousArchiveSourceData, RepointContinuousArchiveSourceErrors, RepointContinuousArchiveSourceRequest, RepointContinuousArchiveSourceResponse, RepointContinuousArchiveSourceResponses, RepositoryComposeServicesResponse, RepositoryEnvExampleResponse, RepositoryListQuery, RepositoryListResponse, RepositoryPresetResponse, RepositoryResponse, RepositorySyncStartedResponse, RequestDiscoveredRouteCertData, RequestDiscoveredRouteCertError, RequestDiscoveredRouteCertErrors, RequestDiscoveredRouteCertRequest, RequestDiscoveredRouteCertResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponse, RequestPasswordResetResponses, RequestRow, RequestTimeoutSettings, RequiredPasswordChangeRequest, RequiredPasswordChangeResponse, ResetPasswordData, ResetPasswordErrors, ResetPasswordRequest, ResetPasswordResponse, ResetPasswordResponses, ResetPgStatStatementsRequest, ResetPgStatStatementsResponse, ResizeSandboxBody, ResizeSandboxData, ResizeSandboxErrors, ResizeSandboxResponse, ResizeSandboxResponses, ResolveAlarmData, ResolveAlarmErrors, ResolveAlarmResponses, ResolvedEnvVarResponse, ResolvedEnvVarSource, ResolvePermissionData, ResolvePermissionErrors, ResolvePermissionRequest, ResolvePermissionResponse, ResolvePermissionResponses, ResolveSystemAlarmData, ResolveSystemAlarmErrors, ResolveSystemAlarmResponses, ResolveUserPermissionData, ResolveUserPermissionErrors, ResolveUserPermissionResponse, ResolveUserPermissionResponses, ResourceCounts, ResourceFootprint, ResourceInfo, ResourceLimitApplyResult, ResourceLimits, ResourceLimitsResponse, ResourceLimitsUpdateResponse, ResourcesBody, RestartContainerData, RestartContainerErrors, RestartContainerResponse, RestartContainerResponses, RestartPreviewGatewayData, RestartPreviewGatewayError, RestartPreviewGatewayErrors, RestartPreviewGatewayResponse, RestartPreviewGatewayResponses, RestartSandboxData, RestartSandboxErrors, RestartSandboxResponse, RestartSandboxResponses, RestoreApplicationData, RestoreApplicationErrors, RestoreApplicationResponse, RestoreApplicationResponses, RestoreCapabilities, RestoreCapabilitiesResponse, RestoreFlagData, RestoreFlagErrors, RestoreFlagResponse, RestoreFlagResponses, RestorePlan, RestoreRequestMode, RestoreRunView, RestoreUserConversationData, RestoreUserConversationErrors, RestoreUserConversationResponse, RestoreUserConversationResponses, RestoreUserData, RestoreUserErrors, RestoreUserResponse, RestoreUserResponses, ResumeDeploymentData, ResumeDeploymentErrors, ResumeDeploymentResponse, ResumeDeploymentResponses, ResumeSandboxData, ResumeSandboxErrors, ResumeSandboxResponse, ResumeSandboxResponses, RetentionCleanupFailure, RetentionCleanupReport, RetryClusterData, RetryClusterErrors, RetryClusterRequest, RetryClusterResponse, RetryClusterResponses, RetryDeliveryData, RetryDeliveryErrors, RetryDeliveryResponse, RetryDeliveryResponses, RetryFacetBackfillData, RetryFacetBackfillError, RetryFacetBackfillErrors, RetryFacetBackfillResponse, RetryFacetBackfillResponses, RetryPgUpgradeData, RetryPgUpgradeError, RetryPgUpgradeErrors, RetryPgUpgradeResponse, RetryPgUpgradeResponses, RetryRunData, RetryRunErrors, RetryRunResponse, RetryRunResponses, RevealGlobalMcpConfigData, RevealGlobalMcpConfigErrors, RevealGlobalMcpConfigResponse, RevealGlobalMcpConfigResponses, RevealMcpConfigData, RevealMcpConfigErrors, RevealMcpConfigResponse, RevealMcpConfigResponses, RevealNotificationProviderConfigData, RevealNotificationProviderConfigErrors, RevealNotificationProviderConfigResponse, RevealNotificationProviderConfigResponses, RevealServiceEnvironmentVariablesData, RevealServiceEnvironmentVariablesErrors, RevealServiceEnvironmentVariablesResponse, RevealServiceEnvironmentVariablesResponses, RevealServiceParameterData, RevealServiceParameterErrors, RevealServiceParameterResponse, RevealServiceParameterResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationErrors, RevenueCreateIntegrationResponse, RevenueCreateIntegrationResponses, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponse, RevenueDeleteIntegrationResponses, RevenueGlobalEventsData, RevenueGlobalEventsResponse, RevenueGlobalEventsResponses, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvErrors, RevenueImportInvoicesCsvResponse, RevenueImportInvoicesCsvResponses, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvErrors, RevenueImportSubscriptionsCsvResponse, RevenueImportSubscriptionsCsvResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponse, RevenueListIntegrationsResponses, RevenueListProvidersData, RevenueListProvidersResponse, RevenueListProvidersResponses, RevenueMetricsCustomersData, RevenueMetricsCustomersResponse, RevenueMetricsCustomersResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponse, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponse, RevenueMetricsGlobalSummaryResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponse, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponse, RevenueMetricsSummaryResponses, RevenueRecentEventsData, RevenueRecentEventsResponse, RevenueRecentEventsResponses, RevenueRotateTokenData, RevenueRotateTokenResponse, RevenueRotateTokenResponses, RevenueRow, RevenueUpdateConfigData, RevenueUpdateConfigErrors, RevenueUpdateConfigResponse, RevenueUpdateConfigResponses, RevenueUpdateSecretData, RevenueUpdateSecretErrors, RevenueUpdateSecretResponse, RevenueUpdateSecretResponses, RevokeAnalyticsIngestKeyData, RevokeAnalyticsIngestKeyErrors, RevokeAnalyticsIngestKeyResponse, RevokeAnalyticsIngestKeyResponses, RevokeDsnData, RevokeDsnErrors, RevokeDsnResponse, RevokeDsnResponses, RevokeEmailDomainProjectData, RevokeEmailDomainProjectErrors, RevokeEmailDomainProjectResponse, RevokeEmailDomainProjectResponses, RevokeEnrollmentTokenData, RevokeEnrollmentTokenErrors, RevokeEnrollmentTokenResponse, RevokeEnrollmentTokenResponses, RevokeJoinTokenData, RevokeJoinTokenErrors, RevokeJoinTokenResponse, RevokeJoinTokenResponses, RevokeProjectAccessData, RevokeProjectAccessErrors, RevokeProjectAccessResponse, RevokeProjectAccessResponses, RiskLevel, RoleInfo, RollbackPgUpgradeData, RollbackPgUpgradeError, RollbackPgUpgradeErrors, RollbackPgUpgradeResponse, RollbackPgUpgradeResponses, RollbackToDeploymentData, RollbackToDeploymentErrors, RollbackToDeploymentResponse, RollbackToDeploymentResponses, RootfsCacheEntry, RootfsGcData, RootfsGcReport, RootfsGcResponses, RootfsReport, RootfsReportData, RootfsReportResponses, RootfsVmEntry, RotateAnalyticsIngestKeyData, RotateAnalyticsIngestKeyErrors, RotateAnalyticsIngestKeyResponse, RotateAnalyticsIngestKeyResponses, RotateApiKeyData, RotateApiKeyErrors, RotateApiKeyResponse, RotateApiKeyResponses, RotateClusterCaData, RotateClusterCaErrors, RotateClusterCaRequest, RotateClusterCaResponse, RotateClusterCaResponse2, RotateClusterCaResponses, RotateDeploymentTokenData, RotateDeploymentTokenErrors, RotateDeploymentTokenResponse, RotateDeploymentTokenResponses, RouteRefreshResponse, RouteResponse, RouteRole, RouteUser, RouteUserWithRoles, RunBackupForSourceData, RunBackupForSourceError, RunBackupForSourceErrors, RunBackupForSourceResponse, RunBackupForSourceResponses, RunBackupRequest, RunConnectionHealthCheckData, RunConnectionHealthCheckErrors, RunConnectionHealthCheckResponse, RunConnectionHealthCheckResponses, RunExternalServiceBackupData, RunExternalServiceBackupError, RunExternalServiceBackupErrors, RunExternalServiceBackupRequest, RunExternalServiceBackupResponse, RunExternalServiceBackupResponses, RunScheduleNowData, RunScheduleNowError, RunScheduleNowErrors, RunScheduleNowResponse, RunScheduleNowResponses, RuntimeCredentialsResponse, S3ConnectionTestResponse, S3CredentialsResponse, S3SourceResponse, S3SourceResponseWritable, SandboxCreatePreviewLinkData, SandboxCreatePreviewLinkErrors, SandboxCreatePreviewLinkResponse, SandboxCreatePreviewLinkResponses, SandboxDomainResponse, SandboxEvent, SandboxEventsResponse, SandboxInner, SandboxResponse, SandboxRoute, SandboxStatusResponse, SaveAgentTokenData, SaveAgentTokenErrors, SaveAgentTokenRequest, SaveAgentTokenResponse, SaveAgentTokenResponse2, SaveAgentTokenResponses, SaveAiProviderCredentialData, SaveAiProviderCredentialErrors, SaveAiProviderCredentialResponse, SaveAiProviderCredentialResponses, SaveCredentialRequest, SaveCredentialResponse, ScalewayCredentialsRequest, ScanResponse, ScheduleRunEntry, ScheduleRunJobEntry, ScheduleRunListResponse, ScheduleRunResponse, ScheduleRunSummary, ScheduleRunSummaryList, ScreenshotSettings, SearchLogsData, SearchLogsError, SearchLogsErrors, SearchLogsRequest, SearchLogsResponse, SearchLogsResponse2, SearchLogsResponses, SearchMode, Seasonality, SecretResponse, SecurityConfig, SecurityHeadersConfig, SecurityHeadersSettings, SelectOption, SelfUpdateAttempt, SelfUpdateBlocker, SelfUpdatePhase, SelfUpdateRestartMode, SelfUpdateSettings, SelfUpdateStatus, SendEmailData, SendEmailErrors, SendEmailRequestBody, SendEmailResponse, SendEmailResponseBody, SendEmailResponses, SendFailureReportData, SendFailureReportErrors, SendFailureReportRequest, SendFailureReportResponse, SendFailureReportResponses, SendMessageAcceptedResponse, SendMessageRequest, SendProjectAiMessageData, SendProjectAiMessageErrors, SendProjectAiMessageResponse, SendProjectAiMessageResponses, SendUserMessageData, SendUserMessageErrors, SendUserMessageResponse, SendUserMessageResponses, SensitiveConfigValueResponse, SensitiveMcpConfigValueResponse, SensitiveValueResponse, SentryChunkUploadResponse, SentryCreateReleaseRequest, SentryEventRequest, SentryEventResponse, SentryReleaseFileResponse, SentryReleaseProjectRef, SentryReleaseResponse, SeriesStateEntry, ServiceAccessInfo, ServiceAction, ServiceAlertRuleResponse, ServiceBackupEntryResponse, ServiceBackupListResponse, ServiceCreateAlertRuleRequest, ServiceHealthResponse, ServiceHealthStatusBatchResponse, ServiceHealthStatusEntryResponse, ServiceMemberInfo, ServiceParameter, ServicePlan, ServiceResourceLimits, ServiceRuntimeReport, ServiceStatsReport, ServiceTemplateChangeKind, ServiceTemplateInstance, ServiceTemplateInstanceResponse, ServiceTemplateUpgradeChange, ServiceTypeInfo, ServiceTypeRoute, ServiceUpdateAlertRuleRequest, SesCredentialsRequest, SessionDetails, SessionDetailsQuery, SessionEvent, SessionEventDto, SessionEventsQuery, SessionEventsResponse, SessionLogsQuery, SessionLogsResponse, SessionReplayEventsRequest, SessionReplayInfoDto, SessionReplayInitRequest, SessionReplayInitResponse, SessionReplayWithEventsDto, SessionReplayWithVisitorDto, SessionRequestLog, SessionSummary, SetAiDataAccessData, SetAiDataAccessErrors, SetAiDataAccessResponse, SetAiDataAccessResponses, SetAlternateSourcesData, SetAlternateSourcesErrors, SetAlternateSourcesRequest, SetAlternateSourcesResponse, SetAlternateSourcesResponses, SetApplicationPrimaryProjectData, SetApplicationPrimaryProjectErrors, SetApplicationPrimaryProjectResponse, SetApplicationPrimaryProjectResponses, SetDefaultS3SourceData, SetDefaultS3SourceError, SetDefaultS3SourceErrors, SetDefaultS3SourceResponse, SetDefaultS3SourceResponses, SetFlagEnvironmentData, SetFlagEnvironmentErrors, SetFlagEnvironmentRequest, SetFlagEnvironmentResponse, SetFlagEnvironmentResponses, SetPreviewPasswordBody, SetPreviewPasswordData, SetPreviewPasswordErrors, SetPreviewPasswordResponse, SetPreviewPasswordResponse2, SetPreviewPasswordResponses, SetRequest, SetResponse, SettingsUpdateResponse, SetTraefikDiscoveredRouteEnabledData, SetTraefikDiscoveredRouteEnabledError, SetTraefikDiscoveredRouteEnabledErrors, SetTraefikDiscoveredRouteEnabledResponse, SetTraefikDiscoveredRouteEnabledResponses, SetupDnsChallengeData, SetupDnsChallengeErrors, SetupDnsChallengeRequest, SetupDnsChallengeResponse, SetupDnsChallengeResponse2, SetupDnsChallengeResponses, SetupDnsData, SetupDnsErrors, SetupDnsRequest, SetupDnsResponse, SetupDnsResponse2, SetupDnsResponses, SetupEmailTrackingData, SetupEmailTrackingErrors, SetupEmailTrackingResponse, SetupEmailTrackingResponses, SetupMfaData, SetupMfaErrors, SetupMfaRequest, SetupMfaResponse, SetupMfaResponses, SiblingRef, SilenceAlarmData, SilenceAlarmErrors, SilenceAlarmRequest, SilenceAlarmResponses, SilenceSystemAlarmData, SilenceSystemAlarmErrors, SilenceSystemAlarmResponses, SkillDefinitionResponse, SlackConfig, SleepEnvironmentData, SleepEnvironmentErrors, SleepEnvironmentResponse, SleepEnvironmentResponses, SlowQueriesResponse, SlowQueryRow, SmartFilter, SmokeTestAgentData, SmokeTestAgentErrors, SmokeTestAgentResponse, SmokeTestAgentResponses, SmokeTestResponse, SmtpCredentialsRequest, SmtpEncryptionRoute, SmtpResult, SnapshotResponse, SourceArchiveUpload, SourceBackupEntry, SourceBackupIndexResponse, SourceBody, SourceFileListResponse, SourceFileResponse, SourceMapListResponse, SourceMapResponse, SourceSandboxData, SourceSandboxErrors, SourceSandboxResponse, SourceSandboxResponses, SourceType, SpanEvent, SpanKind, SpanRecord, SpanRow, SpanStats, SpanStatsResponse, SpanStatusCode, SpeedMetricsPayload, SpeedSegmentFilters, StaleSlot, StartAnalysisData, StartAnalysisErrors, StartAnalysisRequest, StartAnalysisResponse, StartAnalysisResponses, StartContainerData, StartContainerErrors, StartContainerResponse, StartContainerResponses, StartFixData, StartFixErrors, StartFixResponses, StartGitProviderOauthData, StartGitProviderOauthErrors, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, StartPgUpgradeData, StartPgUpgradeError, StartPgUpgradeErrors, StartPgUpgradeRequest, StartPgUpgradeResponse, StartPgUpgradeResponses, StartRestoreData, StartRestoreError, StartRestoreErrors, StartRestoreRequest, StartRestoreResponse, StartRestoreResponses, StartServiceData, StartServiceErrors, StartServiceResponse, StartServiceResponses, StartUpdateData, StartUpdateError, StartUpdateErrors, StartUpdateRequest, StartUpdateResponse, StartUpdateResponse2, StartUpdateResponses, StaticBundleResponse, StaticParams, StaticPresetConfig, StatPathData, StatPathErrors, StatPathResponse, StatPathResponses, StatResponse, StatsFilters, StatusBucket, StatusBucketedResponse, StatusCodeCount, StatusCodesQuery, StatusPageOverview, StepConversionResponse, StepResourceType, StepResult, StepUpResponse, StopContainerData, StopContainerErrors, StopContainerResponse, StopContainerResponses, StopSandboxData, StopSandboxErrors, StopSandboxResponse, StopSandboxResponses, StopSequence, StopServiceData, StopServiceErrors, StopServiceResponse, StopServiceResponses, StopTurnData, StopTurnErrors, StopTurnResponse, StopTurnResponses, StopUserTurnData, StopUserTurnErrors, StopUserTurnResponse, StopUserTurnResponses, StorageQuota, StorageSummary, StorageSummaryData, StorageSummaryErrors, StorageSummaryResponse, StorageSummaryResponses, StreamContainerMetricsData, StreamContainerMetricsErrors, StreamContainerMetricsResponses, StreamEventsData, StreamEventsErrors, StreamEventsResponses, StreamRunEventsData, StreamRunEventsErrors, StreamRunEventsResponses, StreamStructuredOutputRequest, StripeConfig, SupervisorKind, SyncedRepositoryListQuery, SyncRepositoriesData, SyncRepositoriesErrors, SyncRepositoriesResponse, SyncRepositoriesResponses, SyntaxResult, TagInfo, TagListResponse, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, TailLogsData, TailLogsError, TailLogsErrors, TailLogsRequest, TailLogsResponses, TargetRecommendation, TeamListResponse, TeamMemberResponse, TeamResponse, TeamRole, TeardownDeploymentData, TeardownDeploymentErrors, TeardownDeploymentResponse, TeardownDeploymentResponses, TeardownEnvironmentData, TeardownEnvironmentErrors, TeardownEnvironmentResponse, TeardownEnvironmentResponses, TelemetryGapWindowResponse, TelemetryWriteIntervalReason, TelemetryWriteIntervalResponse, TemplateKind, TemplateResources, TemplateResponse, TenantResourceCeilings, TerminalData, TerminalErrors, TestEmailRequest, TestEmailResponse, TestNotificationProviderData, TestNotificationProviderErrors, TestNotificationProviderResponse, TestNotificationProviderResponses, TestOidcProviderData, TestOidcProviderResponse, TestOidcProviderResponses, TestProviderConnectionData, TestProviderConnectionErrors, TestProviderConnectionResponse, TestProviderConnectionResponses, TestProviderData, TestProviderErrors, TestProviderKeyByIdData, TestProviderKeyByIdError, TestProviderKeyByIdErrors, TestProviderKeyByIdResponse, TestProviderKeyByIdResponses, TestProviderKeyInlineData, TestProviderKeyInlineError, TestProviderKeyInlineErrors, TestProviderKeyInlineResponse, TestProviderKeyInlineResponses, TestProviderKeyRequest, TestProviderKeyResponse, TestProviderResponse, TestProviderResponse2, TestProviderResponses, TestS3ConnectionPreviewData, TestS3ConnectionPreviewError, TestS3ConnectionPreviewErrors, TestS3ConnectionPreviewResponse, TestS3ConnectionPreviewResponses, TestS3SourceConnectionData, TestS3SourceConnectionError, TestS3SourceConnectionErrors, TestS3SourceConnectionResponse, TestS3SourceConnectionResponses, ThreadArtifactResponse, TimeBucketStats, TimeBucketStatsResponse, TimeseriesBucket, TimeseriesQueryParams, TlsMode, TodayStatsResponse, ToggleAiDataAccessRequest, ToggleDeploymentMetricsRequest, ToggleServiceMetricsRequest, TokenRenewalRequest, ToolCallEvent, ToolInfo, ToolResultEvent, TopModelsQueryParams, TraceProjectRef, TracesResponse, TraceSummariesResponse, TraceSummary, TrackClickData, TrackClickErrors, TrackedLinkResponse, TrackingEventResponse, TrackOpenData, TrackOpenErrors, TrackOpenResponses, TraefikDiscoveredRouteListResponse, TraefikDiscoveredRouteResponse, TraefikDiscoveryConflictResponse, TraefikDiscoverySetupResponse, TraefikDiscoveryStatusResponse, TraefikReconciliationResponse, TraefikRouteTlsBlock, TrafficAggregationRequest, TrafficAggregationResponse, TrafficAggregationRow, TrafficDimension, TrafficDimensionValue, TrafficFilter, TrafficFilterOperator, TrafficMetric, TrafficMetricValues, TrafficOrderBy, TrafficOrderField, TrafficSortDirection, TriggerAgentData, TriggerAgentErrors, TriggerAgentRequest, TriggerAgentResponse, TriggerAgentResponses, TriggerDigestResponse, TriggerPipelinePayload, TriggerPipelineResponse, TriggerProjectPipelineData, TriggerProjectPipelineErrors, TriggerProjectPipelineResponse, TriggerProjectPipelineResponses, TriggerScanData, TriggerScanError, TriggerScanErrors, TriggerScanRequest, TriggerScanResponse, TriggerScanResponse2, TriggerScanResponses, TriggerServiceHealthCheckData, TriggerServiceHealthCheckErrors, TriggerServiceHealthCheckResponse, TriggerServiceHealthCheckResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestErrors, TriggerWeeklyDigestResponse, TriggerWeeklyDigestResponses, TtlRequest, TtlResponse, TxtRecord, UiManifest, UiRoute, UndrainNodeResponse, UnifiedTrace, UniqueCountsQuery, UniqueCountsResponse, UnlinkApplicationProjectData, UnlinkApplicationProjectErrors, UnlinkApplicationProjectResponse, UnlinkApplicationProjectResponses, UnlinkServiceFromProjectData, UnlinkServiceFromProjectErrors, UnlinkServiceFromProjectResponse, UnlinkServiceFromProjectResponses, UnsupportedFeature, UpdateAdminGateRequest, UpdateAgentData, UpdateAgentErrors, UpdateAgentResponse, UpdateAgentResponses, UpdateAiProviderData, UpdateAiProviderErrors, UpdateAiProviderPreferenceData, UpdateAiProviderPreferenceError, UpdateAiProviderPreferenceErrors, UpdateAiProviderPreferenceResponse, UpdateAiProviderPreferenceResponses, UpdateAiProviderRequest, UpdateAiProviderResponse, UpdateAiProviderResponse2, UpdateAiProviderResponses, UpdateAiSummaryPreferenceData, UpdateAiSummaryPreferenceError, UpdateAiSummaryPreferenceErrors, UpdateAiSummaryPreferenceRequest, UpdateAiSummaryPreferenceResponse, UpdateAiSummaryPreferenceResponses, UpdateAlertData, UpdateAlertError, UpdateAlertErrors, UpdateAlertResponse, UpdateAlertResponses, UpdateAlertRuleData, UpdateAlertRuleErrors, UpdateAlertRuleRequest, UpdateAlertRuleResponse, UpdateAlertRuleResponses, UpdateAnalyticsIngestKeyData, UpdateAnalyticsIngestKeyErrors, UpdateAnalyticsIngestKeyRequest, UpdateAnalyticsIngestKeyResponse, UpdateAnalyticsIngestKeyResponses, UpdateApiKeyData, UpdateApiKeyErrors, UpdateApiKeyRequest, UpdateApiKeyResponse, UpdateApiKeyResponses, UpdateApplicationWorkspaceData, UpdateApplicationWorkspaceErrors, UpdateApplicationWorkspaceRequest, UpdateApplicationWorkspaceResponse, UpdateApplicationWorkspaceResponses, UpdateAutomaticDeployData, UpdateAutomaticDeployErrors, UpdateAutomaticDeployRequest, UpdateAutomaticDeployResponse, UpdateAutomaticDeployResponses, UpdateBackupScheduleData, UpdateBackupScheduleError, UpdateBackupScheduleErrors, UpdateBackupScheduleRequest, UpdateBackupScheduleResponse, UpdateBackupScheduleResponses, UpdateBlobRequest, UpdateBlobResponse, UpdateCapabilityResponse, UpdateCloudFeaturesData, UpdateCloudFeaturesResponse, UpdateCloudFeaturesResponses, UpdateCloudflareProviderData, UpdateCloudflareProviderErrors, UpdateCloudflareProviderRequest, UpdateCloudflareProviderResponse, UpdateCloudflareProviderResponses, UpdateConfigBody, UpdateConnectionTokenData, UpdateConnectionTokenErrors, UpdateConnectionTokenResponse, UpdateConnectionTokenResponses, UpdateCustomDomainData, UpdateCustomDomainErrors, UpdateCustomDomainRequest, UpdateCustomDomainResponse, UpdateCustomDomainResponses, UpdateDashboardData, UpdateDashboardError, UpdateDashboardErrors, UpdateDashboardRequest, UpdateDashboardResponse, UpdateDashboardResponses, UpdateDeploymentConfigRequest, UpdateDeploymentTokenData, UpdateDeploymentTokenErrors, UpdateDeploymentTokenRequest, UpdateDeploymentTokenResponse, UpdateDeploymentTokenResponses, UpdateDnsProviderRequest, UpdateEmailProviderData, UpdateEmailProviderErrors, UpdateEmailProviderRequest, UpdateEmailProviderResponse, UpdateEmailProviderResponses, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsErrors, UpdateEnvironmentSettingsRequest, UpdateEnvironmentSettingsResponse, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainErrors, UpdateEnvironmentSubdomainRequest, UpdateEnvironmentSubdomainResponse, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentVariableData, UpdateEnvironmentVariableErrors, UpdateEnvironmentVariableRequest, UpdateEnvironmentVariableResponse, UpdateEnvironmentVariableResponses, UpdateErrorGroupData, UpdateErrorGroupErrors, UpdateErrorGroupRequest, UpdateErrorGroupResponses, UpdateExternalServiceRequest, UpdateFlagData, UpdateFlagErrors, UpdateFlagRequest, UpdateFlagResponse, UpdateFlagResponses, UpdateFunnelData, UpdateFunnelErrors, UpdateFunnelResponses, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsErrors, UpdateGitProviderCredentialsResponse, UpdateGitProviderCredentialsResponses, UpdateGitSettingsData, UpdateGitSettingsErrors, UpdateGitSettingsRequest, UpdateGitSettingsResponse, UpdateGitSettingsResponses, UpdateGlobalMcpData, UpdateGlobalMcpErrors, UpdateGlobalMcpResponse, UpdateGlobalMcpResponses, UpdateGlobalSkillData, UpdateGlobalSkillErrors, UpdateGlobalSkillResponse, UpdateGlobalSkillResponses, UpdateIncidentStatusData, UpdateIncidentStatusErrors, UpdateIncidentStatusRequest, UpdateIncidentStatusResponse, UpdateIncidentStatusResponses, UpdateIpAccessControlData, UpdateIpAccessControlError, UpdateIpAccessControlErrors, UpdateIpAccessControlRequest, UpdateIpAccessControlResponse, UpdateIpAccessControlResponses, UpdateKvRequest, UpdateKvResponse, UpdateManagedDomainApiRequest, UpdateManagedDomainData, UpdateManagedDomainErrors, UpdateManagedDomainResponse, UpdateManagedDomainResponses, UpdateMcpData, UpdateMcpErrors, UpdateMcpRequest, UpdateMcpResponse, UpdateMcpResponses, UpdateMemberRoleRequest, UpdateMetricAlertRequest, UpdateNotificationEmailProviderData, UpdateNotificationEmailProviderErrors, UpdateNotificationEmailProviderRequest, UpdateNotificationEmailProviderResponse, UpdateNotificationEmailProviderResponses, UpdateNotificationProviderData, UpdateNotificationProviderErrors, UpdateNotificationProviderResponse, UpdateNotificationProviderResponses, UpdateNotificationRouteData, UpdateNotificationRouteErrors, UpdateNotificationRouteRequest, UpdateNotificationRouteResponse, UpdateNotificationRouteResponses, UpdateOidcProviderData, UpdateOidcProviderRequest, UpdateOidcProviderResponse, UpdateOidcProviderResponses, UpdatePermissionModeData, UpdatePermissionModeErrors, UpdatePermissionModeRequest, UpdatePermissionModeResponse, UpdatePermissionModeResponses, UpdatePreferencesData, UpdatePreferencesErrors, UpdatePreferencesRequest, UpdatePreferencesResponse, UpdatePreferencesResponses, UpdateProjectCloudTelemetryData, UpdateProjectCloudTelemetryError, UpdateProjectCloudTelemetryErrors, UpdateProjectCloudTelemetryRequest, UpdateProjectCloudTelemetryResponse, UpdateProjectCloudTelemetryResponses, UpdateProjectData, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigErrors, UpdateProjectDeploymentConfigResponse, UpdateProjectDeploymentConfigResponses, UpdateProjectErrors, UpdateProjectResponse, UpdateProjectResponses, UpdateProjectSecretData, UpdateProjectSecretErrors, UpdateProjectSecretRequest, UpdateProjectSecretResponse, UpdateProjectSecretResponses, UpdateProjectSettingsData, UpdateProjectSettingsErrors, UpdateProjectSettingsRequest, UpdateProjectSettingsResponse, UpdateProjectSettingsResponses, UpdateProviderCredentialsRequest, UpdateProviderData, UpdateProviderErrors, UpdateProviderKeyData, UpdateProviderKeyError, UpdateProviderKeyErrors, UpdateProviderKeyRequest, UpdateProviderKeyResponse, UpdateProviderKeyResponses, UpdateProviderModelData, UpdateProviderModelError, UpdateProviderModelErrors, UpdateProviderModelRequest, UpdateProviderModelResponse, UpdateProviderModelResponses, UpdateProviderPreferenceRequest, UpdateProviderRequest, UpdateProviderResponse, UpdateProviderResponses, UpdateRouteData, UpdateRouteErrors, UpdateRouteRequest, UpdateRouteResponse, UpdateRouteResponses, UpdateS3SourceData, UpdateS3SourceError, UpdateS3SourceErrors, UpdateS3SourceRequest, UpdateS3SourceResponse, UpdateS3SourceResponses, UpdateSecretBody, UpdateSelfData, UpdateSelfErrors, UpdateSelfRequest, UpdateSelfResponse, UpdateSelfResponses, UpdateServiceData, UpdateServiceErrors, UpdateServiceResourcesData, UpdateServiceResourcesErrors, UpdateServiceResourcesResponse, UpdateServiceResourcesResponses, UpdateServiceResponse, UpdateServiceResponses, UpdateServiceTemplateRuntimeData, UpdateServiceTemplateRuntimeErrors, UpdateServiceTemplateRuntimeRequest, UpdateServiceTemplateRuntimeResponse, UpdateServiceTemplateRuntimeResponses, UpdateSessionDurationData, UpdateSessionDurationError, UpdateSessionDurationErrors, UpdateSessionDurationRequest, UpdateSessionDurationResponse, UpdateSessionDurationResponse2, UpdateSessionDurationResponses, UpdateSettingsData, UpdateSettingsErrors, UpdateSettingsResponse, UpdateSettingsResponses, UpdateSkillData, UpdateSkillErrors, UpdateSkillRequest, UpdateSkillResponse, UpdateSkillResponses, UpdateSlackProviderData, UpdateSlackProviderErrors, UpdateSlackProviderRequest, UpdateSlackProviderResponse, UpdateSlackProviderResponses, UpdateSpeedMetricsData, UpdateSpeedMetricsError, UpdateSpeedMetricsErrors, UpdateSpeedMetricsPayload, UpdateSpeedMetricsResponse, UpdateSpeedMetricsResponses, UpdateStatusResponse, UpdateTeamData, UpdateTeamErrors, UpdateTeamMemberRoleData, UpdateTeamMemberRoleErrors, UpdateTeamMemberRoleResponse, UpdateTeamMemberRoleResponses, UpdateTeamRequest, UpdateTeamResponse, UpdateTeamResponses, UpdateTokenRequest, UpdateTokenResponse, UpdateTraefikRouteEnabledRequest, UpdateUserData, UpdateUserErrors, UpdateUserPermissionModeData, UpdateUserPermissionModeErrors, UpdateUserPermissionModeResponse, UpdateUserPermissionModeResponses, UpdateUserRequest, UpdateUserResponse, UpdateUserResponses, UpdateWebhookData, UpdateWebhookErrors, UpdateWebhookProviderData, UpdateWebhookProviderErrors, UpdateWebhookProviderRequest, UpdateWebhookProviderResponse, UpdateWebhookProviderResponses, UpdateWebhookRequestBody, UpdateWebhookResponse, UpdateWebhookResponses, UpgradeExternalServiceRequest, UpgradePreviewGatewayData, UpgradePreviewGatewayError, UpgradePreviewGatewayErrors, UpgradePreviewGatewayResponse, UpgradePreviewGatewayResponses, UpgradeProjectServiceTemplateData, UpgradeProjectServiceTemplateErrors, UpgradeProjectServiceTemplateResponse, UpgradeProjectServiceTemplateResponses, UpgradeRequest, UpgradeServiceData, UpgradeServiceErrors, UpgradeServiceResponse, UpgradeServiceResponses, UpgradeServiceTemplateRequest, UploadApplicationWorkspaceFilesData, UploadApplicationWorkspaceFilesErrors, UploadApplicationWorkspaceFilesResponse, UploadApplicationWorkspaceFilesResponses, UploadGlobalSkillData, UploadGlobalSkillErrors, UploadGlobalSkillResponse, UploadGlobalSkillResponses, UploadGlobalWorkspaceFilesData, UploadGlobalWorkspaceFilesErrors, UploadGlobalWorkspaceFilesResponse, UploadGlobalWorkspaceFilesResponses, UploadReleaseFileData, UploadReleaseFileErrors, UploadReleaseFileResponse, UploadReleaseFileResponses, UploadSkillData, UploadSkillErrors, UploadSkillResponse, UploadSkillResponses, UploadSourceFileData, UploadSourceFileErrors, UploadSourceFileResponse, UploadSourceFileResponses, UploadSourceMapData, UploadSourceMapErrors, UploadSourceMapResponse, UploadSourceMapResponses, UploadStaticBundleData, UploadStaticBundleErrors, UploadStaticBundleResponse, UploadStaticBundleResponses, UploadUserConversationAttachmentData, UploadUserConversationAttachmentErrors, UploadUserConversationAttachmentResponse, UploadUserConversationAttachmentResponses, UpsertAgentRequest, UpsertSecretData, UpsertSecretErrors, UpsertSecretRequest, UpsertSecretResponse, UpsertSecretResponses, UptimeDataPoint, UptimeHistoryResponse, UsageFilter, UsageInfo, UsageLogEntry, UsageLogPage, UsageQueryParams, UsageSource, UsageSummary, UserResponse, ValidateConnectionData, ValidateConnectionErrors, ValidateConnectionResponse, ValidateConnectionResponses, ValidateEmailData, ValidateEmailErrors, ValidateEmailRequest, ValidateEmailResponse, ValidateEmailResponse2, ValidateEmailResponses, ValidationLevel, ValidationReport, ValidationResponse, ValidationResult, ValidationStatus, ValidationSummary, VerifyAndEnableMfaData, VerifyAndEnableMfaErrors, VerifyAndEnableMfaResponse, VerifyAndEnableMfaResponses, VerifyDomainData, VerifyDomainErrors, VerifyDomainResponse, VerifyDomainResponses, VerifyEmailData, VerifyEmailErrors, VerifyEmailResponse, VerifyEmailResponses, VerifyManagedDomainData, VerifyManagedDomainErrors, VerifyManagedDomainResponse, VerifyManagedDomainResponses, VerifyMfaChallengeData, VerifyMfaChallengeErrors, VerifyMfaChallengeResponse, VerifyMfaChallengeResponses, VerifyMfaRequest, VerifyStepUpData, VerifyStepUpErrors, VerifyStepUpRequest, VerifyStepUpResponse, VerifyStepUpResponses, ViewItem, ViewsOverTime, ViewsOverTimeQuery, VisitorDetails, VisitorFacets, VisitorFacetsQuery, VisitorFacetValue, VisitorInfo, VisitorJourneyQuery, VisitorJourneyResponse, VisitorLocationsQuery, VisitorRecord, VisitorSegmentFilters, VisitorSessionsQuery, VisitorSessionsResponse, VisitorsListQuery, VisitorsResponse, VisitorStats, VisitorWithGeolocation, VolumeMount, VolumeType, VulnerabilityResponse, WakeEnvironmentData, WakeEnvironmentErrors, WakeEnvironmentResponse, WakeEnvironmentResponses, WalWarning, WalWarningSeverity, WebhookConfig, WebhookDeliveryResponse, WebhookResponse, WebhookTriggerData, WebhookTriggerErrors, WebhookTriggerRequest, WebhookTriggerResponse, WebhookTriggerResponse2, WebhookTriggerResponses, WorkflowDryRunData, WorkflowDryRunErrors, WorkflowDryRunRequest, WorkflowDryRunResponse, WorkflowDryRunResponses, WorkloadDescriptor, WorkloadId, WorkloadStatus, WorkloadType, WriteApplicationWorkspaceFilesData, WriteApplicationWorkspaceFilesErrors, WriteApplicationWorkspaceFilesRequest, WriteApplicationWorkspaceFilesResponse, WriteApplicationWorkspaceFilesResponse2, WriteApplicationWorkspaceFilesResponses, WriteFileBody, WriteFileData, WriteFileErrors, WriteFileResponse, WriteFileResponses, WriteFilesBody, WriteFilesData, WriteFilesErrors, WriteFilesResponse, WriteFilesResponse2, WriteFilesResponses, ZoneListResponse } from './types.gen'; diff --git a/web/src/api/client/sdk.gen.ts b/web/src/api/client/sdk.gen.ts index 509242f94..8e26d629d 100644 --- a/web/src/api/client/sdk.gen.ts +++ b/web/src/api/client/sdk.gen.ts @@ -7,6 +7,8 @@ import { type Client, type ClientMeta, formDataBodySerializer, type Options as O import { client } from './client.gen'; import type { AcknowledgeAlarmData, AcknowledgeAlarmErrors, AcknowledgeAlarmResponses, AcknowledgeSystemAlarmData, AcknowledgeSystemAlarmErrors, AcknowledgeSystemAlarmResponses, ActivateAiProviderData, ActivateAiProviderErrors, ActivateAiProviderResponses, ActivateApiKeyData, ActivateApiKeyErrors, ActivateApiKeyResponses, ActivateConnectionData, ActivateConnectionErrors, ActivateConnectionResponses, ActivateProviderData, ActivateProviderErrors, ActivateProviderResponses, AddClusterMemberData, AddClusterMemberErrors, AddClusterMemberResponses, AddContextData, AddContextErrors, AddContextResponses, AddEnvironmentDomainData, AddEnvironmentDomainErrors, AddEnvironmentDomainResponses, AddEventsData, AddEventsErrors, AddEventsResponses, AddManagedDomainData, AddManagedDomainErrors, AddManagedDomainResponses, AddProviderModelData, AddProviderModelErrors, AddProviderModelResponses, AddSessionReplayEventsData, AddSessionReplayEventsErrors, AddSessionReplayEventsResponses, AddTeamMemberData, AddTeamMemberErrors, AddTeamMemberResponses, AdminDrainNodeData, AdminDrainNodeErrors, AdminDrainNodeResponses, AdminDrainStatusData, AdminDrainStatusErrors, AdminDrainStatusResponses, AdminGetNodeData, AdminGetNodeErrors, AdminGetNodeResponses, AdminListNodeContainersData, AdminListNodeContainersErrors, AdminListNodeContainersResponses, AdminListNodesData, AdminListNodesErrors, AdminListNodesResponses, AdminRemoveNodeData, AdminRemoveNodeErrors, AdminRemoveNodeResponses, AdminUndrainNodeData, AdminUndrainNodeErrors, AdminUndrainNodeResponses, AggregateApiTrafficData, AggregateApiTrafficErrors, AggregateApiTrafficResponses, ApplyHostnameModeData, ApplyHostnameModeErrors, ApplyHostnameModeResponses, ArchiveApplicationData, ArchiveApplicationErrors, ArchiveApplicationResponses, ArchiveConversationData, ArchiveConversationErrors, ArchiveConversationResponses, ArchiveFlagData, ArchiveFlagErrors, ArchiveFlagResponses, ArchiveUserConversationData, ArchiveUserConversationErrors, ArchiveUserConversationResponses, AssignRoleData, AssignRoleErrors, AssignRoleResponses, AttachScheduleServicesData, AttachScheduleServicesErrors, AttachScheduleServicesResponses, AuthorizeEmailDomainProjectData, AuthorizeEmailDomainProjectErrors, AuthorizeEmailDomainProjectResponses, BlobCopyData, BlobCopyErrors, BlobCopyResponses, BlobDeleteData, BlobDeleteErrors, BlobDeleteResponses, BlobDisableData, BlobDisableErrors, BlobDisableResponses, BlobDownloadData, BlobDownloadErrors, BlobDownloadResponses, BlobEnableData, BlobEnableErrors, BlobEnableResponses, BlobHeadData, BlobHeadErrors, BlobHeadResponses, BlobListData, BlobListErrors, BlobListResponses, BlobPutData, BlobPutErrors, BlobPutResponses, BlobStatusData, BlobStatusErrors, BlobStatusResponses, BlobUpdateData, BlobUpdateErrors, BlobUpdateResponses, CancelBackupData, CancelBackupErrors, CancelBackupResponses, CancelBulkActivationJobData, CancelBulkActivationJobErrors, CancelBulkActivationJobResponses, CancelData, CancelDeploymentData, CancelDeploymentErrors, CancelDeploymentResponses, CancelDomainOrderData, CancelDomainOrderErrors, CancelDomainOrderResponses, CancelErrors, CancelPgUpgradeData, CancelPgUpgradeErrors, CancelPgUpgradeResponses, CancelResponses, CancelRunData, CancelRunErrors, CancelRunResponses, CancelScheduleRunData, CancelScheduleRunErrors, CancelScheduleRunResponses, ChangePasswordSelfData, ChangePasswordSelfErrors, ChangePasswordSelfResponses, ChangeProjectSourceData, ChangeProjectSourceErrors, ChangeProjectSourceResponses, ChangeRequiredPasswordData, ChangeRequiredPasswordErrors, ChangeRequiredPasswordResponses, ChatCompletionsData, ChatCompletionsErrors, ChatCompletionsResponses, CheckAnalyticsHasEventsData, CheckAnalyticsHasEventsErrors, CheckAnalyticsHasEventsResponses, CheckCommitExistsData, CheckCommitExistsErrors, CheckCommitExistsResponses, CheckDomainStatusData, CheckDomainStatusErrors, CheckDomainStatusResponses, CheckExplorerSupportData, CheckExplorerSupportErrors, CheckExplorerSupportResponses, CheckForUpdateData, CheckForUpdateErrors, CheckForUpdateResponses, CheckIpBlockedData, CheckIpBlockedErrors, CheckIpBlockedResponses, CheckProviderDeletionSafetyData, CheckProviderDeletionSafetyErrors, CheckProviderDeletionSafetyResponses, ChunkUploadOptionsData, ChunkUploadOptionsResponses, CleanupExpiredBackupsData, CleanupExpiredBackupsErrors, CleanupExpiredBackupsResponses, ClearPreviewPasswordData, ClearPreviewPasswordErrors, ClearPreviewPasswordResponses, CliDeviceApproveData, CliDeviceApproveErrors, CliDeviceApproveResponses, CliDeviceDenyData, CliDeviceDenyErrors, CliDeviceDenyResponses, CliDeviceLookupData, CliDeviceLookupErrors, CliDeviceLookupResponses, CliDevicePollData, CliDevicePollErrors, CliDevicePollResponses, CliDeviceStartData, CliDeviceStartErrors, CliDeviceStartResponses, CliLogoutData, CliLogoutErrors, CliLogoutResponses, ClusterDnsStatusData, ClusterDnsStatusErrors, ClusterDnsStatusResponses, CmdData, CmdErrors, CmdKillData, CmdKillErrors, CmdKillResponses, CmdLogsData, CmdLogsErrors, CmdLogsResponses, CmdResponses, ConfirmPendingActionData, ConfirmPendingActionErrors, ConfirmPendingActionResponses, ConfirmUserPendingActionData, ConfirmUserPendingActionErrors, ConfirmUserPendingActionResponses, ContainerMetricsGetHistoryData, ContainerMetricsGetHistoryErrors, ContainerMetricsGetHistoryResponses, ControlApplicationWorkspaceData, ControlApplicationWorkspaceErrors, ControlApplicationWorkspaceResponses, CreateAgentData, CreateAgentErrors, CreateAgentResponses, CreateAlertData, CreateAlertErrors, CreateAlertResponses, CreateAlertRuleData, CreateAlertRuleErrors, CreateAlertRuleResponses, CreateAnalyticsIngestKeyData, CreateAnalyticsIngestKeyErrors, CreateAnalyticsIngestKeyResponses, CreateApiKeyData, CreateApiKeyErrors, CreateApiKeyResponses, CreateApplicationConversationData, CreateApplicationConversationErrors, CreateApplicationConversationResponses, CreateApplicationData, CreateApplicationErrors, CreateApplicationPreviewLinkData, CreateApplicationPreviewLinkErrors, CreateApplicationPreviewLinkResponses, CreateApplicationProjectData, CreateApplicationProjectErrors, CreateApplicationProjectResponses, CreateApplicationResponses, CreateBackupScheduleData, CreateBackupScheduleErrors, CreateBackupScheduleResponses, CreateBitbucketProviderData, CreateBitbucketProviderErrors, CreateBitbucketProviderResponses, CreateBulkActivationJobData, CreateBulkActivationJobErrors, CreateBulkActivationJobResponses, CreateCloudflareProviderData, CreateCloudflareProviderErrors, CreateCloudflareProviderResponses, CreateConversationData, CreateConversationErrors, CreateConversationResponses, CreateCustomDomainData, CreateCustomDomainErrors, CreateCustomDomainResponses, CreateDashboardData, CreateDashboardErrors, CreateDashboardResponses, CreateDeploymentTokenData, CreateDeploymentTokenErrors, CreateDeploymentTokenResponses, CreateDnsProviderData, CreateDnsProviderErrors, CreateDnsProviderResponses, CreateDomainData, CreateDomainErrors, CreateDomainResponses, CreateDsnData, CreateDsnErrors, CreateDsnResponses, CreateEmailDomainData, CreateEmailDomainErrors, CreateEmailDomainResponses, CreateEmailProviderData, CreateEmailProviderErrors, CreateEmailProviderResponses, CreateEnvironmentData, CreateEnvironmentErrors, CreateEnvironmentResponses, CreateEnvironmentVariableData, CreateEnvironmentVariableErrors, CreateEnvironmentVariableResponses, CreateFacetData, CreateFacetErrors, CreateFacetResponses, CreateFlagData, CreateFlagErrors, CreateFlagResponses, CreateFunnelData, CreateFunnelErrors, CreateFunnelResponses, CreateGenericProviderData, CreateGenericProviderErrors, CreateGenericProviderResponses, CreateGiteaPatProviderData, CreateGiteaPatProviderErrors, CreateGiteaPatProviderResponses, CreateGithubPatProviderData, CreateGithubPatProviderErrors, CreateGithubPatProviderResponses, CreateGitlabOauthProviderData, CreateGitlabOauthProviderErrors, CreateGitlabOauthProviderResponses, CreateGitlabPatProviderData, CreateGitlabPatProviderErrors, CreateGitlabPatProviderResponses, CreateGitProviderData, CreateGitProviderErrors, CreateGitProviderResponses, CreateGlobalConversationData, CreateGlobalConversationErrors, CreateGlobalConversationResponses, CreateGlobalMcpData, CreateGlobalMcpErrors, CreateGlobalMcpResponses, CreateGlobalSkillData, CreateGlobalSkillErrors, CreateGlobalSkillResponses, CreateGlobalWorkspacePreviewLinkData, CreateGlobalWorkspacePreviewLinkErrors, CreateGlobalWorkspacePreviewLinkResponses, CreateIncidentData, CreateIncidentErrors, CreateIncidentResponses, CreateIpAccessControlData, CreateIpAccessControlErrors, CreateIpAccessControlResponses, CreateMcpData, CreateMcpErrors, CreateMcpResponses, CreateMonitorData, CreateMonitorErrors, CreateMonitorResponses, CreateNotificationEmailProviderData, CreateNotificationEmailProviderErrors, CreateNotificationEmailProviderResponses, CreateNotificationProviderData, CreateNotificationProviderErrors, CreateNotificationProviderResponses, CreateNotificationRouteData, CreateNotificationRouteErrors, CreateNotificationRouteResponses, CreateOidcProviderData, CreateOidcProviderErrors, CreateOidcProviderResponses, CreateOidcRoleMappingData, CreateOidcRoleMappingResponses, CreateOrRecreateOrderData, CreateOrRecreateOrderErrors, CreateOrRecreateOrderResponses, CreatePlanData, CreatePlanErrors, CreatePlanResponses, CreatePrData, CreatePrErrors, CreateProjectData, CreateProjectErrors, CreateProjectFromTemplateData, CreateProjectFromTemplateErrors, CreateProjectFromTemplateResponses, CreateProjectReleaseData, CreateProjectReleaseErrors, CreateProjectReleaseResponses, CreateProjectResponses, CreateProjectSecretData, CreateProjectSecretErrors, CreateProjectSecretResponses, CreateProviderKeyData, CreateProviderKeyErrors, CreateProviderKeyResponses, CreatePrResponses, CreateReleaseData, CreateReleaseErrors, CreateReleaseResponses, CreateRouteData, CreateRouteErrors, CreateRouteResponses, CreateS3SourceData, CreateS3SourceErrors, CreateS3SourceResponses, CreateSandboxData, CreateSandboxErrors, CreateSandboxResponses, CreateServiceData, CreateServiceErrors, CreateServiceResponses, CreateSkillData, CreateSkillErrors, CreateSkillResponses, CreateSlackProviderData, CreateSlackProviderErrors, CreateSlackProviderResponses, CreateSnapshotData, CreateSnapshotErrors, CreateSnapshotResponses, CreateTeamData, CreateTeamErrors, CreateTeamResponses, CreateThreadArtifactData, CreateThreadArtifactErrors, CreateThreadArtifactResponses, CreateUserData, CreateUserErrors, CreateUserResponses, CreateWebhookData, CreateWebhookErrors, CreateWebhookProviderData, CreateWebhookProviderErrors, CreateWebhookProviderResponses, CreateWebhookResponses, DeactivateApiKeyData, DeactivateApiKeyErrors, DeactivateApiKeyResponses, DeactivateConnectionData, DeactivateConnectionErrors, DeactivateConnectionResponses, DeactivateProviderData, DeactivateProviderErrors, DeactivateProviderResponses, DeauthorizeDiscoveredRouteCertData, DeauthorizeDiscoveredRouteCertErrors, DeauthorizeDiscoveredRouteCertResponses, DeleteAgentData, DeleteAgentErrors, DeleteAgentResponses, DeleteAlertData, DeleteAlertErrors, DeleteAlertResponses, DeleteAlertRuleData, DeleteAlertRuleErrors, DeleteAlertRuleResponses, DeleteApiKeyData, DeleteApiKeyErrors, DeleteApiKeyResponses, DeleteBackupData, DeleteBackupErrors, DeleteBackupResponses, DeleteBackupScheduleData, DeleteBackupScheduleErrors, DeleteBackupScheduleResponses, DeleteConnectionData, DeleteConnectionErrors, DeleteConnectionResponses, DeleteCustomDomainData, DeleteCustomDomainErrors, DeleteCustomDomainResponses, DeleteDashboardData, DeleteDashboardErrors, DeleteDashboardResponses, DeleteDeploymentTokenData, DeleteDeploymentTokenErrors, DeleteDeploymentTokenResponses, DeleteDnsProviderData, DeleteDnsProviderErrors, DeleteDnsProviderResponses, DeleteDomainData, DeleteDomainErrors, DeleteDomainResponses, DeleteEmailDomainData, DeleteEmailDomainErrors, DeleteEmailDomainResponses, DeleteEmailProviderData, DeleteEmailProviderErrors, DeleteEmailProviderResponses, DeleteEnvironmentData, DeleteEnvironmentDomainData, DeleteEnvironmentDomainErrors, DeleteEnvironmentDomainResponses, DeleteEnvironmentErrors, DeleteEnvironmentResponses, DeleteEnvironmentVariableData, DeleteEnvironmentVariableErrors, DeleteEnvironmentVariableResponses, DeleteExternalImageData, DeleteExternalImageErrors, DeleteExternalImageResponses, DeleteFacetData, DeleteFacetErrors, DeleteFacetResponses, DeleteFunnelData, DeleteFunnelErrors, DeleteFunnelResponses, DeleteGitProviderData, DeleteGitProviderErrors, DeleteGitProviderResponses, DeleteGlobalMcpData, DeleteGlobalMcpErrors, DeleteGlobalMcpResponses, DeleteGlobalSkillData, DeleteGlobalSkillErrors, DeleteGlobalSkillResponses, DeleteIpAccessControlData, DeleteIpAccessControlErrors, DeleteIpAccessControlResponses, DeleteMcpData, DeleteMcpErrors, DeleteMcpResponses, DeleteMonitorData, DeleteMonitorErrors, DeleteMonitorResponses, DeleteNotificationProviderData, DeleteNotificationProviderErrors, DeleteNotificationProviderResponses, DeleteNotificationRouteData, DeleteNotificationRouteErrors, DeleteNotificationRouteResponses, DeleteOidcProviderData, DeleteOidcProviderResponses, DeleteOidcRoleMappingData, DeleteOidcRoleMappingResponses, DeletePreferencesData, DeletePreferencesErrors, DeletePreferencesResponses, DeleteProjectData, DeleteProjectErrors, DeleteProjectResponses, DeleteProjectSecretData, DeleteProjectSecretErrors, DeleteProjectSecretResponses, DeleteProviderKeyData, DeleteProviderKeyErrors, DeleteProviderKeyResponses, DeleteProviderModelData, DeleteProviderModelErrors, DeleteProviderModelResponses, DeleteProviderSafelyData, DeleteProviderSafelyErrors, DeleteProviderSafelyResponses, DeleteReleaseSourceFilesData, DeleteReleaseSourceFilesErrors, DeleteReleaseSourceFilesResponses, DeleteReleaseSourceMapsData, DeleteReleaseSourceMapsErrors, DeleteReleaseSourceMapsResponses, DeleteRouteData, DeleteRouteErrors, DeleteRouteResponses, DeleteS3SourceData, DeleteS3SourceErrors, DeleteS3SourceResponses, DeleteScanData, DeleteScanErrors, DeleteScanResponses, DeleteSecretData, DeleteSecretErrors, DeleteSecretResponses, DeleteServiceData, DeleteServiceErrors, DeleteServiceResponses, DeleteSessionReplayData, DeleteSessionReplayErrors, DeleteSessionReplayResponses, DeleteSkillData, DeleteSkillErrors, DeleteSkillResponses, DeleteSnapshotData, DeleteSnapshotErrors, DeleteSnapshotResponses, DeleteSourceMapData, DeleteSourceMapErrors, DeleteSourceMapResponses, DeleteStaticBundleData, DeleteStaticBundleErrors, DeleteStaticBundleResponses, DeleteTeamData, DeleteTeamErrors, DeleteTeamResponses, DeleteUserData, DeleteUserErrors, DeleteUserResponses, DeleteWebhookData, DeleteWebhookErrors, DeleteWebhookResponses, DeployApplicationWorkspaceProjectData, DeployApplicationWorkspaceProjectErrors, DeployApplicationWorkspaceProjectResponses, DeployFromImageData, DeployFromImageErrors, DeployFromImageResponses, DeployFromImageUploadData, DeployFromImageUploadErrors, DeployFromImageUploadResponses, DeployFromStaticData, DeployFromStaticErrors, DeployFromStaticResponses, DeployFromUploadedSourceData, DeployFromUploadedSourceErrors, DeployFromUploadedSourceResponses, DeploymentMetricsGetLatestData, DeploymentMetricsGetLatestErrors, DeploymentMetricsGetLatestResponses, DeploymentMetricsGetRangeData, DeploymentMetricsGetRangeErrors, DeploymentMetricsGetRangeResponses, DeploymentMetricsToggleData, DeploymentMetricsToggleErrors, DeploymentMetricsToggleResponses, DestroySandboxData, DestroySandboxErrors, DestroySandboxResponses, DetachScheduleServiceData, DetachScheduleServiceErrors, DetachScheduleServiceResponses, DetectPublicEnvExampleData, DetectPublicEnvExampleErrors, DetectPublicEnvExampleResponses, DetectPublicPresetsData, DetectPublicPresetsErrors, DetectPublicPresetsResponses, DisableBackupScheduleData, DisableBackupScheduleErrors, DisableBackupScheduleResponses, DisableMfaData, DisableMfaErrors, DisableMfaResponses, DisconnectCloudData, DisconnectCloudResponses, DiscoverWorkloadsData, DiscoverWorkloadsErrors, DiscoverWorkloadsResponses, DomainData, DomainErrors, DomainResponses, DownloadApplicationWorkspaceFileData, DownloadApplicationWorkspaceFileErrors, DownloadApplicationWorkspaceFileResponses, DownloadGlobalSkillArchiveData, DownloadGlobalSkillArchiveErrors, DownloadGlobalSkillArchiveResponses, DownloadGlobalWorkspaceFileData, DownloadGlobalWorkspaceFileErrors, DownloadGlobalWorkspaceFileResponses, DownloadObjectData, DownloadObjectErrors, DownloadObjectResponses, DownloadSkillArchiveData, DownloadSkillArchiveErrors, DownloadSkillArchiveResponses, EmailStatusData, EmailStatusErrors, EmailStatusResponses, EmbeddingsData, EmbeddingsErrors, EmbeddingsResponses, EnableBackupScheduleData, EnableBackupScheduleErrors, EnableBackupScheduleResponses, EnrichVisitorData, EnrichVisitorErrors, EnrichVisitorResponses, EnrollCloudData, EnrollCloudResponses, EstimateBulkActivationData, EstimateBulkActivationErrors, EstimateBulkActivationResponses, ExecData, ExecDetachedData, ExecDetachedErrors, ExecDetachedResponses, ExecErrors, ExecResponses, ExecuteDeploymentOperationData, ExecuteDeploymentOperationErrors, ExecuteDeploymentOperationResponses, ExecuteImportData, ExecuteImportErrors, ExecuteImportResponses, ExtendTimeoutData, ExtendTimeoutErrors, ExtendTimeoutResponses, ExternalServiceEnablePgStatStatementsData, ExternalServiceEnablePgStatStatementsErrors, ExternalServiceEnablePgStatStatementsResponses, ExternalServiceMetricsByDatabaseData, ExternalServiceMetricsByDatabaseErrors, ExternalServiceMetricsByDatabaseResponses, ExternalServiceMetricsCreateAlertRuleData, ExternalServiceMetricsCreateAlertRuleErrors, ExternalServiceMetricsCreateAlertRuleResponses, ExternalServiceMetricsDeleteAlertRuleData, ExternalServiceMetricsDeleteAlertRuleErrors, ExternalServiceMetricsDeleteAlertRuleResponses, ExternalServiceMetricsGetAlertRulesData, ExternalServiceMetricsGetAlertRulesErrors, ExternalServiceMetricsGetAlertRulesResponses, ExternalServiceMetricsGetLatestData, ExternalServiceMetricsGetLatestErrors, ExternalServiceMetricsGetLatestResponses, ExternalServiceMetricsGetRangeData, ExternalServiceMetricsGetRangeErrors, ExternalServiceMetricsGetRangeResponses, ExternalServiceMetricsStatusData, ExternalServiceMetricsStatusErrors, ExternalServiceMetricsStatusResponses, ExternalServiceMetricsToggleData, ExternalServiceMetricsToggleErrors, ExternalServiceMetricsToggleResponses, ExternalServiceMetricsUpdateAlertRuleData, ExternalServiceMetricsUpdateAlertRuleErrors, ExternalServiceMetricsUpdateAlertRuleResponses, ExternalServiceResetPgStatStatementsData, ExternalServiceResetPgStatStatementsErrors, ExternalServiceResetPgStatStatementsResponses, FinalizeOrderData, FinalizeOrderErrors, FinalizeOrderResponses, FinalizeProjectReleaseData, FinalizeProjectReleaseErrors, FinalizeProjectReleaseResponses, FindConversationData, FindConversationErrors, FindConversationResponses, GenerateJoinTokenData, GenerateJoinTokenErrors, GenerateJoinTokenResponses, GeneratePresetDockerfileData, GeneratePresetDockerfileErrors, GeneratePresetDockerfileResponses, GetAccessInfoData, GetAccessInfoErrors, GetAccessInfoResponses, GetActiveVisitorsData, GetActiveVisitorsErrors, GetActiveVisitorsResponses, GetActivityGraphData, GetActivityGraphErrors, GetActivityGraphResponses, GetAdminGateData, GetAdminGateErrors, GetAdminGateResponses, GetAgentData, GetAgentErrors, GetAgentResponses, GetAggregatedBucketsData, GetAggregatedBucketsErrors, GetAggregatedBucketsResponses, GetAiAgentBreakdownData, GetAiAgentBreakdownErrors, GetAiAgentBreakdownResponses, GetAiAgentPagesData, GetAiAgentPagesErrors, GetAiAgentPagesResponses, GetAiAgentTimelineData, GetAiAgentTimelineErrors, GetAiAgentTimelineResponses, GetAiDataAccessData, GetAiDataAccessErrors, GetAiDataAccessResponses, GetAiPageBreakdownData, GetAiPageBreakdownErrors, GetAiPageBreakdownResponses, GetAiProviderStatusData, GetAiProviderStatusErrors, GetAiProviderStatusResponses, GetAiStatusBreakdownData, GetAiStatusBreakdownErrors, GetAiStatusBreakdownResponses, GetAlertData, GetAlertErrors, GetAlertResponses, GetAlertRuleData, GetAlertRuleErrors, GetAlertRuleResponses, GetAllRepositoriesByNameData, GetAllRepositoriesByNameErrors, GetAllRepositoriesByNameResponses, GetAnalyticsActiveVisitorsData, GetAnalyticsActiveVisitorsErrors, GetAnalyticsActiveVisitorsResponses, GetAnalyticsEventsCountData, GetAnalyticsEventsCountErrors, GetAnalyticsEventsCountResponses, GetAnalyticsSessionEventsData, GetAnalyticsSessionEventsErrors, GetAnalyticsSessionEventsResponses, GetAnalyticsVisitorSessionsData, GetAnalyticsVisitorSessionsErrors, GetAnalyticsVisitorSessionsResponses, GetApiCallersData, GetApiCallersErrors, GetApiCallersResponses, GetApiKeyData, GetApiKeyErrors, GetApiKeyPermissionsData, GetApiKeyPermissionsErrors, GetApiKeyPermissionsResponses, GetApiKeyResponses, GetApiRoutesData, GetApiRoutesErrors, GetApiRoutesResponses, GetApiSummaryData, GetApiSummaryErrors, GetApiSummaryResponses, GetApiTimeseriesData, GetApiTimeseriesErrors, GetApiTimeseriesResponses, GetApiTrafficProxyLogAccessData, GetApiTrafficProxyLogAccessErrors, GetApiTrafficProxyLogAccessResponses, GetApplicationData, GetApplicationErrors, GetApplicationResponses, GetApplicationWorkspaceChangesData, GetApplicationWorkspaceChangesErrors, GetApplicationWorkspaceChangesResponses, GetApplicationWorkspaceData, GetApplicationWorkspaceDiffData, GetApplicationWorkspaceDiffErrors, GetApplicationWorkspaceDiffResponses, GetApplicationWorkspaceDirectoryData, GetApplicationWorkspaceDirectoryErrors, GetApplicationWorkspaceDirectoryResponses, GetApplicationWorkspaceErrors, GetApplicationWorkspaceFileData, GetApplicationWorkspaceFileErrors, GetApplicationWorkspaceFileResponses, GetApplicationWorkspaceResponses, GetAuditLogData, GetAuditLogErrors, GetAuditLogResponses, GetBackupData, GetBackupErrors, GetBackupResponses, GetBackupScheduleData, GetBackupScheduleErrors, GetBackupScheduleResponses, GetBranchesByRepositoryIdData, GetBranchesByRepositoryIdErrors, GetBranchesByRepositoryIdResponses, GetBucketedIncidentsData, GetBucketedIncidentsErrors, GetBucketedIncidentsResponses, GetBucketedStatusData, GetBucketedStatusErrors, GetBucketedStatusResponses, GetBulkActivationJobData, GetBulkActivationJobErrors, GetBulkActivationJobResponses, GetChallengeTokenData, GetChallengeTokenErrors, GetChallengeTokenResponses, GetChatReadinessData, GetChatReadinessErrors, GetChatReadinessResponses, GetCliStatusData, GetCliStatusErrors, GetCliStatusResponses, GetCloudAiCapabilityData, GetCloudAiCapabilityResponses, GetCloudBackfillStatusData, GetCloudBackfillStatusErrors, GetCloudBackfillStatusResponses, GetCloudCapabilityData, GetCloudCapabilityResponses, GetCloudStatusData, GetCloudStatusResponses, GetCloudTelemetryStatusData, GetCloudTelemetryStatusErrors, GetCloudTelemetryStatusResponses, GetClusterHealthData, GetClusterHealthErrors, GetClusterHealthResponses, GetClusterMemberData, GetClusterMemberErrors, GetClusterMemberResponses, GetCmdData, GetCmdErrors, GetCmdResponses, GetContainerDetailData, GetContainerDetailErrors, GetContainerDetailResponses, GetContainerEnvironmentVariableData, GetContainerEnvironmentVariableErrors, GetContainerEnvironmentVariableResponses, GetContainerLogsByIdData, GetContainerLogsByIdErrors, GetContainerLogsData, GetContainerLogsErrors, GetContainerMetricsData, GetContainerMetricsErrors, GetContainerMetricsResponses, GetConversationData, GetConversationDetailData, GetConversationDetailErrors, GetConversationDetailResponses, GetConversationErrors, GetConversationResponses, GetConversationsData, GetConversationsErrors, GetConversationsResponses, GetCronByIdData, GetCronByIdErrors, GetCronByIdResponses, GetCronExecutionsData, GetCronExecutionsErrors, GetCronExecutionsResponses, GetCrossProjectTraceSiblingsData, GetCrossProjectTraceSiblingsErrors, GetCrossProjectTraceSiblingsResponses, GetCurrentBulkActivationJobData, GetCurrentBulkActivationJobErrors, GetCurrentBulkActivationJobResponses, GetCurrentMonitorStatusData, GetCurrentMonitorStatusErrors, GetCurrentMonitorStatusResponses, GetCurrentUserData, GetCurrentUserErrors, GetCurrentUserResponses, GetCustomDomainData, GetCustomDomainErrors, GetCustomDomainResponses, GetDashboardData, GetDashboardErrors, GetDashboardProjectsAnalyticsData, GetDashboardProjectsAnalyticsErrors, GetDashboardProjectsAnalyticsResponses, GetDashboardResponses, GetDeliveryData, GetDeliveryErrors, GetDeliveryResponses, GetDeploymentContainerLogContentData, GetDeploymentContainerLogContentErrors, GetDeploymentContainerLogContentResponses, GetDeploymentData, GetDeploymentErrors, GetDeploymentJobLogsData, GetDeploymentJobLogsErrors, GetDeploymentJobLogsResponses, GetDeploymentJobsData, GetDeploymentJobsErrors, GetDeploymentJobsResponses, GetDeploymentOperationsData, GetDeploymentOperationsErrors, GetDeploymentOperationsResponses, GetDeploymentOperationStatusData, GetDeploymentOperationStatusErrors, GetDeploymentOperationStatusResponses, GetDeploymentResponses, GetDeploymentTokenData, GetDeploymentTokenErrors, GetDeploymentTokenResponses, GetDiskStatusData, GetDiskStatusErrors, GetDiskStatusResponses, GetDnsChangesData, GetDnsChangesErrors, GetDnsChangesResponses, GetDnsProviderData, GetDnsProviderErrors, GetDnsProviderResponses, GetDomainByHostData, GetDomainByHostErrors, GetDomainByHostResponses, GetDomainByIdData, GetDomainByIdErrors, GetDomainByIdResponses, GetDomainByNameData, GetDomainByNameErrors, GetDomainByNameResponses, GetDomainData, GetDomainDnsRecordsData, GetDomainDnsRecordsErrors, GetDomainDnsRecordsResponses, GetDomainErrors, GetDomainOrderData, GetDomainOrderErrors, GetDomainOrderResponses, GetDomainResponses, GetEmailData, GetEmailErrors, GetEmailEventsData, GetEmailEventsErrors, GetEmailEventsResponses, GetEmailLinksData, GetEmailLinksErrors, GetEmailLinksResponses, GetEmailProviderData, GetEmailProviderErrors, GetEmailProviderResponses, GetEmailResponses, GetEmailStatsData, GetEmailStatsErrors, GetEmailStatsResponses, GetEmailTrackingData, GetEmailTrackingErrors, GetEmailTrackingResponses, GetEmailTrackingStatusData, GetEmailTrackingStatusErrors, GetEmailTrackingStatusResponses, GetEntityInfoData, GetEntityInfoErrors, GetEntityInfoResponses, GetEnvironmentCronsData, GetEnvironmentCronsErrors, GetEnvironmentCronsResponses, GetEnvironmentData, GetEnvironmentDomainsData, GetEnvironmentDomainsErrors, GetEnvironmentDomainsResponses, GetEnvironmentErrors, GetEnvironmentResponses, GetEnvironmentsData, GetEnvironmentsErrors, GetEnvironmentsResponses, GetEnvironmentVariablesData, GetEnvironmentVariablesErrors, GetEnvironmentVariablesResponses, GetEnvironmentVariableValueData, GetEnvironmentVariableValueErrors, GetEnvironmentVariableValueResponses, GetErrorDashboardStatsData, GetErrorDashboardStatsErrors, GetErrorDashboardStatsResponses, GetErrorEventData, GetErrorEventErrors, GetErrorEventResponses, GetErrorGroupData, GetErrorGroupErrors, GetErrorGroupResponses, GetErrorStatsData, GetErrorStatsErrors, GetErrorStatsResponses, GetErrorTimeSeriesData, GetErrorTimeSeriesErrors, GetErrorTimeSeriesResponses, GetEventDetailData, GetEventDetailErrors, GetEventDetailResponses, GetEventEntriesData, GetEventEntriesErrors, GetEventEntriesResponses, GetEventsCountData, GetEventsCountErrors, GetEventsCountResponses, GetEventsTimelineData, GetEventsTimelineErrors, GetEventsTimelineResponses, GetEventTypeBreakdownData, GetEventTypeBreakdownErrors, GetEventTypeBreakdownResponses, GetEventVisitorsData, GetEventVisitorsErrors, GetEventVisitorsResponses, GetExternalImageData, GetExternalImageErrors, GetExternalImageResponses, GetExternalServiceBackupCapabilityData, GetExternalServiceBackupCapabilityErrors, GetExternalServiceBackupCapabilityResponses, GetFailureReportPreviewData, GetFailureReportPreviewErrors, GetFailureReportPreviewResponses, GetFeatureMaturityData, GetFeatureMaturityErrors, GetFeatureMaturityResponses, GetFileData, GetFileErrors, GetFileResponses, GetFlagData, GetFlagErrors, GetFlagResponses, GetFlagSnapshotData, GetFlagSnapshotErrors, GetFlagSnapshotResponses, GetFunnelMetricsData, GetFunnelMetricsErrors, GetFunnelMetricsResponses, GetGenaiTraceData, GetGenaiTraceErrors, GetGenaiTraceResponses, GetGeneralStatsData, GetGeneralStatsErrors, GetGeneralStatsResponses, GetGitProviderData, GetGitProviderErrors, GetGitProviderResponses, GetGlobalAiWorkspaceData, GetGlobalAiWorkspaceErrors, GetGlobalAiWorkspaceResponses, GetGlobalEventsData, GetGlobalEventsErrors, GetGlobalEventsResponses, GetGlobalEventStatsData, GetGlobalEventStatsErrors, GetGlobalEventStatsResponses, GetGlobalMcpData, GetGlobalMcpErrors, GetGlobalMcpResponses, GetGlobalSandboxStatusData, GetGlobalSandboxStatusErrors, GetGlobalSandboxStatusResponses, GetGlobalSkillData, GetGlobalSkillErrors, GetGlobalSkillResponses, GetGlobalWorkspaceChangesData, GetGlobalWorkspaceChangesErrors, GetGlobalWorkspaceChangesResponses, GetGlobalWorkspaceDiffData, GetGlobalWorkspaceDiffErrors, GetGlobalWorkspaceDiffResponses, GetGlobalWorkspaceDirectoryData, GetGlobalWorkspaceDirectoryErrors, GetGlobalWorkspaceDirectoryResponses, GetGlobalWorkspaceFileData, GetGlobalWorkspaceFileErrors, GetGlobalWorkspaceFileResponses, GetGroupedPageMetricsData, GetGroupedPageMetricsErrors, GetGroupedPageMetricsResponses, GetHealthData, GetHealthErrors, GetHealthResponses, GetHourlyVisitsData, GetHourlyVisitsErrors, GetHourlyVisitsResponses, GetHttpChallengeDebugData, GetHttpChallengeDebugErrors, GetHttpChallengeDebugResponses, GetImportStatusData, GetImportStatusErrors, GetImportStatusResponses, GetIncidentData, GetIncidentErrors, GetIncidentResponses, GetIncidentUpdatesData, GetIncidentUpdatesErrors, GetIncidentUpdatesResponses, GetIngestErrorsData, GetIngestErrorsErrors, GetIngestErrorsResponses, GetIpAccessControlData, GetIpAccessControlErrors, GetIpAccessControlResponses, GetIpGeolocationData, GetIpGeolocationErrors, GetIpGeolocationResponses, GetJoinTokenStatusData, GetJoinTokenStatusErrors, GetJoinTokenStatusResponses, GetLastDeploymentData, GetLastDeploymentErrors, GetLastDeploymentResponses, GetLatestDeploymentMediaData, GetLatestDeploymentMediaErrors, GetLatestDeploymentMediaResponses, GetLatestScanData, GetLatestScanErrors, GetLatestScanResponses, GetLatestScansPerEnvironmentData, GetLatestScansPerEnvironmentErrors, GetLatestScansPerEnvironmentResponses, GetLiveVisitorsListData, GetLiveVisitorsListErrors, GetLiveVisitorsListResponses, GetLogContextData, GetLogContextErrors, GetLogContextResponses, GetMcpData, GetMcpErrors, GetMcpResponses, GetMetricsOverTimeData, GetMetricsOverTimeErrors, GetMetricsOverTimeResponses, GetMonitorData, GetMonitorErrors, GetMonitorResponses, GetNotificationProviderData, GetNotificationProviderErrors, GetNotificationProviderResponses, GetNotificationRouteData, GetNotificationRouteErrors, GetNotificationRouteResponses, GetOnDemandCertStatusData, GetOnDemandCertStatusErrors, GetOnDemandCertStatusResponses, GetOrCreateDsnData, GetOrCreateDsnErrors, GetOrCreateDsnResponses, GetPageFlowData, GetPageFlowErrors, GetPageFlowResponses, GetPageHourlySessionsData, GetPageHourlySessionsErrors, GetPageHourlySessionsResponses, GetPagePathDetailData, GetPagePathDetailErrors, GetPagePathDetailResponses, GetPagePathsData, GetPagePathsErrors, GetPagePathsResponses, GetPagePathsSparklinesData, GetPagePathsSparklinesErrors, GetPagePathsSparklinesResponses, GetPagePathVisitorsData, GetPagePathVisitorsErrors, GetPagePathVisitorsResponses, GetPendingActionData, GetPendingActionErrors, GetPendingActionResponses, GetPerformanceMetricsData, GetPerformanceMetricsErrors, GetPerformanceMetricsResponses, GetPgUpgradeData, GetPgUpgradeErrors, GetPgUpgradeLogsData, GetPgUpgradeLogsErrors, GetPgUpgradeLogsResponses, GetPgUpgradeResponses, GetPipelineHistoryData, GetPipelineHistoryErrors, GetPipelineHistoryResponses, GetPipelineStatsData, GetPipelineStatsErrors, GetPipelineStatsResponses, GetPlatformInfoData, GetPlatformInfoErrors, GetPlatformInfoResponses, GetPostgresWalHealthData, GetPostgresWalHealthErrors, GetPostgresWalHealthResponses, GetPreferencesData, GetPreferencesErrors, GetPreferencesResponses, GetPreviewGatewayLogsData, GetPreviewGatewayLogsErrors, GetPreviewGatewayLogsResponses, GetPreviewGatewaySettingsData, GetPreviewGatewaySettingsResponses, GetPreviewGatewayStatusData, GetPreviewGatewayStatusErrors, GetPreviewGatewayStatusResponses, GetPricingData, GetPricingErrors, GetPricingResponses, GetPrivateIpData, GetPrivateIpErrors, GetPrivateIpResponses, GetProjectAlarmsSummaryData, GetProjectAlarmsSummaryErrors, GetProjectAlarmsSummaryResponses, GetProjectBySlugData, GetProjectBySlugErrors, GetProjectBySlugResponses, GetProjectCloudTelemetryData, GetProjectCloudTelemetryErrors, GetProjectCloudTelemetryResponses, GetProjectData, GetProjectDeploymentsData, GetProjectDeploymentsErrors, GetProjectDeploymentsResponses, GetProjectErrors, GetProjectResponses, GetProjectsData, GetProjectsErrors, GetProjectServiceEnvironmentVariablesData, GetProjectServiceEnvironmentVariablesErrors, GetProjectServiceEnvironmentVariablesResponses, GetProjectServiceTemplateData, GetProjectServiceTemplateErrors, GetProjectServiceTemplateResponses, GetProjectSessionReplaysData, GetProjectSessionReplaysErrors, GetProjectSessionReplaysResponses, GetProjectsHealthData, GetProjectsHealthErrors, GetProjectsHealthResponses, GetProjectsMonitorHealthData, GetProjectsMonitorHealthErrors, GetProjectsMonitorHealthResponses, GetProjectsResponses, GetProjectStatisticsData, GetProjectStatisticsErrors, GetProjectStatisticsResponses, GetProjectTemplateData, GetProjectTemplateErrors, GetProjectTemplateResponses, GetPropertyBreakdownData, GetPropertyBreakdownErrors, GetPropertyBreakdownResponses, GetPropertyTimelineData, GetPropertyTimelineErrors, GetPropertyTimelineResponses, GetProviderConnectionsData, GetProviderConnectionsErrors, GetProviderConnectionsResponses, GetProviderKeyData, GetProviderKeyErrors, GetProviderKeyResponses, GetProviderMetadataData, GetProviderMetadataErrors, GetProviderMetadataResponses, GetProvidersMetadataData, GetProvidersMetadataErrors, GetProvidersMetadataResponses, GetProxyLogByIdData, GetProxyLogByIdErrors, GetProxyLogByIdResponses, GetProxyLogByRequestIdData, GetProxyLogByRequestIdErrors, GetProxyLogByRequestIdResponses, GetProxyLogsData, GetProxyLogsErrors, GetProxyLogsResponses, GetPublicBranchesData, GetPublicBranchesErrors, GetPublicBranchesResponses, GetPublicComposePreviewData, GetPublicComposePreviewErrors, GetPublicComposePreviewResponses, GetPublicComposeServicesData, GetPublicComposeServicesErrors, GetPublicComposeServicesResponses, GetPublicIpData, GetPublicIpErrors, GetPublicIpResponses, GetPublicRepositoryData, GetPublicRepositoryErrors, GetPublicRepositoryResponses, GetQueryContainerInfoData, GetQueryContainerInfoErrors, GetQueryContainerInfoResponses, GetQuotaData, GetQuotaErrors, GetQuotaResponses, GetRecentActivityData, GetRecentActivityErrors, GetRecentActivityResponses, GetRemoteExternalImageData, GetRemoteExternalImageErrors, GetRemoteExternalImageResponses, GetRepositoryBranchesData, GetRepositoryBranchesErrors, GetRepositoryBranchesResponses, GetRepositoryByIdData, GetRepositoryByIdErrors, GetRepositoryByIdResponses, GetRepositoryByNameData, GetRepositoryByNameErrors, GetRepositoryByNameResponses, GetRepositoryComposePreviewData, GetRepositoryComposePreviewErrors, GetRepositoryComposePreviewResponses, GetRepositoryComposeServicesLiveData, GetRepositoryComposeServicesLiveErrors, GetRepositoryComposeServicesLiveResponses, GetRepositoryEnvExampleLiveData, GetRepositoryEnvExampleLiveErrors, GetRepositoryEnvExampleLiveResponses, GetRepositoryPresetByNameData, GetRepositoryPresetByNameErrors, GetRepositoryPresetByNameResponses, GetRepositoryPresetLiveData, GetRepositoryPresetLiveErrors, GetRepositoryPresetLiveResponses, GetRepositoryTagsData, GetRepositoryTagsErrors, GetRepositoryTagsResponses, GetResolvedEnvironmentVariablesData, GetResolvedEnvironmentVariablesErrors, GetResolvedEnvironmentVariablesResponses, GetResolvedEnvironmentVariableValueData, GetResolvedEnvironmentVariableValueErrors, GetResolvedEnvironmentVariableValueResponses, GetRestoreCapabilitiesData, GetRestoreCapabilitiesErrors, GetRestoreCapabilitiesResponses, GetRestoreRunData, GetRestoreRunErrors, GetRestoreRunResponses, GetRouteData, GetRouteErrors, GetRouteResponses, GetRunData, GetRunErrors, GetRunResponses, GetRunWithLogsData, GetRunWithLogsErrors, GetRunWithLogsResponses, GetS3CredentialsData, GetS3CredentialsErrors, GetS3CredentialsResponses, GetS3SourceData, GetS3SourceErrors, GetS3SourceResponses, GetSandboxData, GetSandboxErrors, GetSandboxResponses, GetSandboxStatusData, GetSandboxStatusErrors, GetSandboxStatusResponses, GetScanByDeploymentData, GetScanByDeploymentErrors, GetScanByDeploymentResponses, GetScanData, GetScanErrors, GetScanResponses, GetScanVulnerabilitiesData, GetScanVulnerabilitiesErrors, GetScanVulnerabilitiesResponses, GetServiceBySlugData, GetServiceBySlugErrors, GetServiceBySlugResponses, GetServiceData, GetServiceEnvironmentVariableData, GetServiceEnvironmentVariableErrors, GetServiceEnvironmentVariableResponses, GetServiceEnvironmentVariablesData, GetServiceEnvironmentVariablesErrors, GetServiceEnvironmentVariablesResponses, GetServiceErrors, GetServiceHealthStatusData, GetServiceHealthStatusErrors, GetServiceHealthStatusResponses, GetServicePreviewEnvironmentVariableNamesData, GetServicePreviewEnvironmentVariableNamesErrors, GetServicePreviewEnvironmentVariableNamesResponses, GetServicePreviewEnvironmentVariablesMaskedData, GetServicePreviewEnvironmentVariablesMaskedErrors, GetServicePreviewEnvironmentVariablesMaskedResponses, GetServiceResponses, GetServiceRuntimeData, GetServiceRuntimeErrors, GetServiceRuntimeResponses, GetServiceStatsData, GetServiceStatsErrors, GetServiceStatsResponses, GetServiceTypeParametersData, GetServiceTypeParametersErrors, GetServiceTypeParametersResponses, GetServiceTypesData, GetServiceTypesErrors, GetServiceTypesResponses, GetSessionDetailsData, GetSessionDetailsErrors, GetSessionDetailsResponses, GetSessionEventsData, GetSessionEventsErrors, GetSessionEventsResponses, GetSessionLogsData, GetSessionLogsErrors, GetSessionLogsResponses, GetSessionReplayData, GetSessionReplayErrors, GetSessionReplayEventsData, GetSessionReplayEventsErrors, GetSessionReplayEventsResponses, GetSessionReplayResponses, GetSettingsData, GetSettingsErrors, GetSettingsResponses, GetSkillData, GetSkillErrors, GetSkillResponses, GetSlowQueriesData, GetSlowQueriesErrors, GetSlowQueriesResponses, GetSnapshotData, GetSnapshotErrors, GetSnapshotResponses, GetStaticBundleData, GetStaticBundleErrors, GetStaticBundleResponses, GetStatusOverviewData, GetStatusOverviewErrors, GetStatusOverviewResponses, GetSystemAlarmsSummaryData, GetSystemAlarmsSummaryErrors, GetSystemAlarmsSummaryResponses, GetTagsByRepositoryIdData, GetTagsByRepositoryIdErrors, GetTagsByRepositoryIdResponses, GetTeamData, GetTeamErrors, GetTeamResponses, GetTimeBucketStatsData, GetTimeBucketStatsErrors, GetTimeBucketStatsResponses, GetTodayStatsData, GetTodayStatsErrors, GetTodayStatsResponses, GetTraceData, GetTraceErrors, GetTraceResponses, GetTraefikDiscoveryStatusData, GetTraefikDiscoveryStatusErrors, GetTraefikDiscoveryStatusResponses, GetUnifiedTraceData, GetUnifiedTraceErrors, GetUnifiedTraceResponses, GetUniqueCountsData, GetUniqueCountsErrors, GetUniqueCountsResponses, GetUniqueEventsData, GetUniqueEventsErrors, GetUniqueEventsResponses, GetUpdateCapabilityData, GetUpdateCapabilityErrors, GetUpdateCapabilityResponses, GetUpdateStatusData, GetUpdateStatusErrors, GetUpdateStatusResponses, GetUptimeHistoryData, GetUptimeHistoryErrors, GetUptimeHistoryResponses, GetUsageByProviderData, GetUsageByProviderErrors, GetUsageByProviderResponses, GetUsageRecentData, GetUsageRecentErrors, GetUsageRecentResponses, GetUsageSummaryData, GetUsageSummaryErrors, GetUsageSummaryResponses, GetUsageTimeseriesData, GetUsageTimeseriesErrors, GetUsageTimeseriesResponses, GetUsageTopModelsData, GetUsageTopModelsErrors, GetUsageTopModelsResponses, GetUserConversationAttachmentData, GetUserConversationAttachmentErrors, GetUserConversationAttachmentResponses, GetUserConversationData, GetUserConversationErrors, GetUserConversationResponses, GetUserPendingActionData, GetUserPendingActionErrors, GetUserPendingActionResponses, GetVisibleCustomDomainByHostnameData, GetVisibleCustomDomainByHostnameErrors, GetVisibleCustomDomainByHostnameResponses, GetVisitorByGuidData, GetVisitorByGuidErrors, GetVisitorByGuidResponses, GetVisitorByIdData, GetVisitorByIdErrors, GetVisitorByIdResponses, GetVisitorDetailsData, GetVisitorDetailsErrors, GetVisitorDetailsResponses, GetVisitorFacetsData, GetVisitorFacetsErrors, GetVisitorFacetsResponses, GetVisitorInfoData, GetVisitorInfoErrors, GetVisitorInfoResponses, GetVisitorJourneyData, GetVisitorJourneyErrors, GetVisitorJourneyResponses, GetVisitorsData, GetVisitorsErrors, GetVisitorSessionsData, GetVisitorSessionsErrors, GetVisitorSessionsResponses, GetVisitorsResponses, GetVisitorStatsData, GetVisitorStatsErrors, GetVisitorStatsResponses, GetWebhookData, GetWebhookErrors, GetWebhookResponses, GetWorkspaceFileLimitsData, GetWorkspaceFileLimitsErrors, GetWorkspaceFileLimitsResponses, GrantProjectAccessData, GrantProjectAccessErrors, GrantProjectAccessResponses, HandleGitProviderOauthCallbackData, HandleGitProviderOauthCallbackErrors, HasAnalyticsEventsData, HasAnalyticsEventsErrors, HasAnalyticsEventsResponses, HasErrorGroupsData, HasErrorGroupsErrors, HasErrorGroupsResponses, HasPerformanceMetricsData, HasPerformanceMetricsErrors, HasPerformanceMetricsResponses, HasTracesData, HasTracesErrors, HasTracesResponses, ImportApplicationWorkspaceGitData, ImportApplicationWorkspaceGitErrors, ImportApplicationWorkspaceGitResponses, ImportEmailDomainData, ImportEmailDomainErrors, ImportEmailDomainResponses, ImportExternalServiceData, ImportExternalServiceErrors, ImportExternalServiceResponses, ImportTraefikAcmeJsonData, ImportTraefikAcmeJsonErrors, ImportTraefikAcmeJsonResponses, IngestLogsByPathData, IngestLogsByPathErrors, IngestLogsByPathResponses, IngestLogsData, IngestLogsErrors, IngestLogsResponses, IngestMetricsByPathData, IngestMetricsByPathErrors, IngestMetricsByPathResponses, IngestMetricsData, IngestMetricsErrors, IngestMetricsResponses, IngestSentryEnvelopeData, IngestSentryEnvelopeErrors, IngestSentryEnvelopeResponses, IngestSentryEventData, IngestSentryEventErrors, IngestSentryEventResponses, IngestTracesByPathData, IngestTracesByPathErrors, IngestTracesByPathResponses, IngestTracesData, IngestTracesErrors, IngestTracesResponses, IngestTunneledEnvelopeData, IngestTunneledEnvelopeErrors, IngestTunneledEnvelopeResponses, InitSessionReplayData, InitSessionReplayErrors, InitSessionReplayResponses, InspectDropArchiveData, InspectDropArchiveErrors, InspectDropArchiveResponses, IssueRuntimeCredentialsData, IssueRuntimeCredentialsErrors, IssueRuntimeCredentialsResponses, JobLogsData, JobLogsErrors, JobLogsResponses, JobStatusData, JobStatusErrors, JobStatusResponses, KillJobData, KillJobErrors, KillJobResponses, KvDelData, KvDelErrors, KvDelResponses, KvDisableData, KvDisableErrors, KvDisableResponses, KvEnableData, KvEnableErrors, KvEnableResponses, KvExpireData, KvExpireErrors, KvExpireResponses, KvGetData, KvGetErrors, KvGetResponses, KvIncrData, KvIncrErrors, KvIncrResponses, KvKeysData, KvKeysErrors, KvKeysResponses, KvSetData, KvSetErrors, KvSetResponses, KvStatusData, KvStatusErrors, KvStatusResponses, KvTtlData, KvTtlErrors, KvTtlResponses, KvUpdateData, KvUpdateErrors, KvUpdateResponses, LatestRunForSourceData, LatestRunForSourceErrors, LatestRunForSourceResponses, LinkApplicationProjectData, LinkApplicationProjectErrors, LinkApplicationProjectResponses, LinkCustomDomainToCertificateData, LinkCustomDomainToCertificateErrors, LinkCustomDomainToCertificateResponses, LinkServiceToProjectData, LinkServiceToProjectErrors, LinkServiceToProjectResponses, ListAgentRunsData, ListAgentRunsErrors, ListAgentRunsResponses, ListAgentsData, ListAgentsErrors, ListAgentsResponses, ListAiProvidersData, ListAiProvidersErrors, ListAiProvidersResponses, ListAlertRulesData, ListAlertRulesErrors, ListAlertRulesResponses, ListAlertsData, ListAlertsErrors, ListAlertsResponses, ListAllConversationsData, ListAllConversationsErrors, ListAllConversationsResponses, ListAllRunsData, ListAllRunsErrors, ListAllRunsResponses, ListAnalyticsIngestKeysData, ListAnalyticsIngestKeysErrors, ListAnalyticsIngestKeysResponses, ListApiKeysData, ListApiKeysErrors, ListApiKeysResponses, ListApplicationConversationsData, ListApplicationConversationsErrors, ListApplicationConversationsResponses, ListApplicationsData, ListApplicationsErrors, ListApplicationsResponses, ListAuditLogsData, ListAuditLogsErrors, ListAuditLogsResponses, ListAvailableContainersData, ListAvailableContainersErrors, ListAvailableContainersResponses, ListBackupAlertsData, ListBackupAlertsErrors, ListBackupAlertsResponses, ListBackupChildrenData, ListBackupChildrenErrors, ListBackupChildrenResponses, ListBackupSchedulesData, ListBackupSchedulesErrors, ListBackupSchedulesResponses, ListBackupsForScheduleData, ListBackupsForScheduleErrors, ListBackupsForScheduleResponses, ListCommitsByRepositoryIdData, ListCommitsByRepositoryIdErrors, ListCommitsByRepositoryIdResponses, ListConnectionsData, ListConnectionsErrors, ListConnectionsResponses, ListContainerHistoryData, ListContainerHistoryErrors, ListContainerHistoryResponses, ListContainersAtPathData, ListContainersAtPathErrors, ListContainersAtPathResponses, ListContainersData, ListContainersErrors, ListContainersResponses, ListConversationsData, ListConversationsErrors, ListConversationsResponses, ListCustomDomainsForProjectData, ListCustomDomainsForProjectErrors, ListCustomDomainsForProjectResponses, ListDashboardsData, ListDashboardsErrors, ListDashboardsResponses, ListDeliveriesData, ListDeliveriesErrors, ListDeliveriesResponses, ListDeploymentContainerLogsData, ListDeploymentContainerLogsErrors, ListDeploymentContainerLogsResponses, ListDeploymentTokensData, ListDeploymentTokensErrors, ListDeploymentTokensResponses, ListDiscoverableDomainsData, ListDiscoverableDomainsErrors, ListDiscoverableDomainsResponses, ListDnsProvidersData, ListDnsProvidersErrors, ListDnsProvidersResponses, ListDomainsData, ListDomainsErrors, ListDomainsResponses, ListDsnsData, ListDsnsErrors, ListDsnsResponses, ListEmailDomainProjectsData, ListEmailDomainProjectsErrors, ListEmailDomainProjectsResponses, ListEmailDomainsData, ListEmailDomainsErrors, ListEmailDomainsResponses, ListEmailProvidersData, ListEmailProvidersErrors, ListEmailProvidersResponses, ListEmailsData, ListEmailsErrors, ListEmailsResponses, ListEnrollmentTokensData, ListEnrollmentTokensErrors, ListEnrollmentTokensResponses, ListEntitiesData, ListEntitiesErrors, ListEntitiesResponses, ListErrorEventsData, ListErrorEventsErrors, ListErrorEventsResponses, ListErrorGroupsData, ListErrorGroupsErrors, ListErrorGroupsResponses, ListEventsData, ListEventsResponses, ListEventTypesData, ListEventTypesResponses, ListExternalImagesData, ListExternalImagesErrors, ListExternalImagesResponses, ListExternalPluginsData, ListExternalPluginsErrors, ListExternalPluginsResponses, ListExternalServiceBackupsData, ListExternalServiceBackupsErrors, ListExternalServiceBackupsResponses, ListFacetsData, ListFacetsErrors, ListFacetsResponses, ListFlagsData, ListFlagsErrors, ListFlagsResponses, ListFunnelsData, ListFunnelsErrors, ListFunnelsResponses, ListGitProvidersData, ListGitProvidersErrors, ListGitProvidersResponses, ListGlobalMcpsData, ListGlobalMcpsErrors, ListGlobalMcpsResponses, ListGlobalSkillsData, ListGlobalSkillsErrors, ListGlobalSkillsResponses, ListIncidentsData, ListIncidentsErrors, ListIncidentsResponses, ListInsightsData, ListInsightsErrors, ListInsightsResponses, ListIpAccessControlData, ListIpAccessControlErrors, ListIpAccessControlResponses, ListJobsData, ListJobsErrors, ListJobsResponses, ListKnownAiAgentsData, ListKnownAiAgentsErrors, ListKnownAiAgentsResponses, ListManagedDomainsData, ListManagedDomainsErrors, ListManagedDomainsResponses, ListManagedEnvironmentVariablesData, ListManagedEnvironmentVariablesErrors, ListManagedEnvironmentVariablesResponses, ListMcpsData, ListMcpsErrors, ListMcpsResponses, ListMetricLabelKeysData, ListMetricLabelKeysErrors, ListMetricLabelKeysResponses, ListMetricLabelValuesData, ListMetricLabelValuesErrors, ListMetricLabelValuesResponses, ListMetricNamesData, ListMetricNamesErrors, ListMetricNamesResponses, ListModelsData, ListModelsErrors, ListModelsResponses, ListMonitorsData, ListMonitorsErrors, ListMonitorsResponses, ListNotificationProvidersData, ListNotificationProvidersErrors, ListNotificationProvidersResponses, ListNotificationRoutesData, ListNotificationRoutesErrors, ListNotificationRoutesResponses, ListOidcProvidersData, ListOidcProvidersResponses, ListOidcProviderUsersData, ListOidcProviderUsersErrors, ListOidcProviderUsersResponses, ListOidcRoleMappingsData, ListOidcRoleMappingsResponses, ListOnDemandCertsData, ListOnDemandCertsErrors, ListOnDemandCertsResponses, ListOrdersData, ListOrdersErrors, ListOrdersResponses, ListPeersData, ListPeersErrors, ListPeersResponses, ListPendingActionsData, ListPendingActionsErrors, ListPendingActionsResponses, ListPgUpgradesData, ListPgUpgradesErrors, ListPgUpgradesResponses, ListPresetsData, ListPresetsErrors, ListPresetsResponses, ListProjectAccessData, ListProjectAccessErrors, ListProjectAccessResponses, ListProjectAlarmsData, ListProjectAlarmsErrors, ListProjectAlarmsResponses, ListProjectScansData, ListProjectScansErrors, ListProjectScansResponses, ListProjectSecretsData, ListProjectSecretsErrors, ListProjectSecretsResponses, ListProjectServicesData, ListProjectServicesErrors, ListProjectServicesResponses, ListProjectTemplatesData, ListProjectTemplatesErrors, ListProjectTemplatesResponses, ListProjectTemplateTagsData, ListProjectTemplateTagsErrors, ListProjectTemplateTagsResponses, ListProviderKeysData, ListProviderKeysErrors, ListProviderKeysResponses, ListProviderZonesData, ListProviderZonesErrors, ListProviderZonesResponses, ListPublicProvidersData, ListPublicProvidersResponses, ListReleaseFilesData, ListReleaseFilesErrors, ListReleaseFilesResponses, ListReleasesData, ListReleasesErrors, ListReleasesResponses, ListRemoteExternalImagesData, ListRemoteExternalImagesErrors, ListRemoteExternalImagesResponses, ListRenewalAttemptsData, ListRenewalAttemptsErrors, ListRenewalAttemptsResponses, ListRepositoriesByConnectionData, ListRepositoriesByConnectionErrors, ListRepositoriesByConnectionResponses, ListRepositoriesByProviderData, ListRepositoriesByProviderErrors, ListRepositoriesByProviderResponses, ListRestoreRunsForServiceData, ListRestoreRunsForServiceErrors, ListRestoreRunsForServiceResponses, ListRootContainersData, ListRootContainersErrors, ListRootContainersResponses, ListRoutesData, ListRoutesErrors, ListRoutesResponses, ListS3SourcesData, ListS3SourcesErrors, ListS3SourcesResponses, ListSandboxesData, ListSandboxesErrors, ListSandboxesResponses, ListScheduleRunJobsData, ListScheduleRunJobsErrors, ListScheduleRunJobsResponses, ListScheduleRunsData, ListScheduleRunsErrors, ListScheduleRunsResponses, ListScheduleServicesData, ListScheduleServicesErrors, ListScheduleServicesResponses, ListSecretsData, ListSecretsErrors, ListSecretsResponses, ListServiceHealthStatusesData, ListServiceHealthStatusesErrors, ListServiceHealthStatusesResponses, ListServiceProjectsData, ListServiceProjectsErrors, ListServiceProjectsResponses, ListServiceSchedulesData, ListServiceSchedulesErrors, ListServiceSchedulesResponses, ListServicesData, ListServicesErrors, ListServicesResponses, ListSkillsData, ListSkillsErrors, ListSkillsResponses, ListSnapshotsData, ListSnapshotsErrors, ListSnapshotsResponses, ListSourceBackupsData, ListSourceBackupsErrors, ListSourceBackupsResponses, ListSourceFilesData, ListSourceFilesErrors, ListSourceFilesResponses, ListSourceMapsData, ListSourceMapsErrors, ListSourceMapsResponses, ListSourcesData, ListSourcesErrors, ListSourcesResponses, ListStaticBundlesData, ListStaticBundlesErrors, ListStaticBundlesResponses, ListSyncedRepositoriesData, ListSyncedRepositoriesErrors, ListSyncedRepositoriesResponses, ListSystemAlarmsData, ListSystemAlarmsErrors, ListSystemAlarmsResponses, ListTeamMembersData, ListTeamMembersErrors, ListTeamMembersResponses, ListTeamProjectsData, ListTeamProjectsErrors, ListTeamProjectsResponses, ListTeamsData, ListTeamsErrors, ListTeamsResponses, ListThreadArtifactsData, ListThreadArtifactsErrors, ListThreadArtifactsResponses, ListTraefikDiscoveredRoutesData, ListTraefikDiscoveredRoutesErrors, ListTraefikDiscoveredRoutesResponses, ListUserPendingActionsData, ListUserPendingActionsErrors, ListUserPendingActionsResponses, ListUsersData, ListUsersErrors, ListUsersResponses, ListWebhooksData, ListWebhooksErrors, ListWebhooksResponses, LoginData, LoginErrors, LoginResponses, LogoutData, LogoutErrors, LogoutResponses, LookupDnsARecordsData, LookupDnsARecordsErrors, LookupDnsARecordsResponses, MintEnrollmentTokenData, MintEnrollmentTokenErrors, MintEnrollmentTokenResponses, MkdirData, MkdirErrors, MkdirResponses, NodeHeartbeatData, NodeHeartbeatErrors, NodeHeartbeatResponses, NodeMetricsGetAlertRulesData, NodeMetricsGetAlertRulesErrors, NodeMetricsGetAlertRulesResponses, NodeMetricsGetRangeData, NodeMetricsGetRangeErrors, NodeMetricsGetRangeResponses, NodeMetricsUpdateAlertRuleData, NodeMetricsUpdateAlertRuleErrors, NodeMetricsUpdateAlertRuleResponses, ObservabilityFullEventData, ObservabilityFullEventErrors, ObservabilityFullEventResponses, ObservabilityListEventsData, ObservabilityListEventsErrors, ObservabilityListEventsResponses, OidcCallbackData, PatchAdminGateData, PatchAdminGateErrors, PatchAdminGateResponses, PatchPreviewGatewaySettingsData, PatchPreviewGatewaySettingsErrors, PatchPreviewGatewaySettingsResponses, PauseDeploymentData, PauseDeploymentErrors, PauseDeploymentResponses, PauseSandboxData, PauseSandboxErrors, PauseSandboxResponses, PlanRestoreData, PlanRestoreErrors, PlanRestoreResponses, PostDnsAckData, PostDnsAckErrors, PostDnsAckResponses, PreviewAlertData, PreviewAlertErrors, PreviewAlertResponses, PreviewFunnelMetricsData, PreviewFunnelMetricsErrors, PreviewFunnelMetricsResponses, PreviewHostnameModeData, PreviewHostnameModeErrors, PreviewHostnameModeResponses, PromoteClusterMemberData, PromoteClusterMemberErrors, PromoteClusterMemberResponses, PromoteDeploymentData, PromoteDeploymentErrors, PromoteDeploymentResponses, ProvisionDomainData, ProvisionDomainErrors, ProvisionDomainResponses, PurgeProjectLogsData, PurgeProjectLogsErrors, PurgeProjectLogsResponses, PushExternalImageData, PushExternalImageErrors, PushExternalImageResponses, QueryDataData, QueryDataErrors, QueryDataResponses, QueryGenaiTracesData, QueryGenaiTracesErrors, QueryGenaiTracesResponses, QueryLogsData, QueryLogsErrors, QueryLogsResponses, QueryMetricsData, QueryMetricsErrors, QueryMetricsResponses, QuerySpanStatsData, QuerySpanStatsErrors, QuerySpanStatsResponses, QueryTracesData, QueryTracesErrors, QueryTracesResponses, QueryTraceSummariesData, QueryTraceSummariesErrors, QueryTraceSummariesResponses, ReadEntityRowsData, ReadEntityRowsErrors, ReadEntityRowsResponses, ReadFileData, ReadFileErrors, ReadFileResponses, ReAnalyzeData, ReAnalyzeErrors, ReAnalyzeResponses, ReassignProjectCustomDomainData, ReassignProjectCustomDomainErrors, ReassignProjectCustomDomainResponses, ReconcileCloudBackupSourceData, ReconcileCloudBackupSourceResponses, RecordConsoleEventData, RecordConsoleEventErrors, RecordConsoleEventResponses, RecordEventMetricsData, RecordEventMetricsErrors, RecordEventMetricsResponses, RecordFlagExposureData, RecordFlagExposureErrors, RecordFlagExposureResponses, RecordSpeedMetricsData, RecordSpeedMetricsErrors, RecordSpeedMetricsResponses, RefreshAiProviderStatusData, RefreshAiProviderStatusErrors, RefreshAiProviderStatusResponses, RefreshProviderModelsData, RefreshProviderModelsErrors, RefreshProviderModelsResponses, RefreshRouteTableData, RefreshRouteTableErrors, RefreshRouteTableResponses, RegenerateDsnData, RegenerateDsnErrors, RegenerateDsnResponses, RegisterExternalImageData, RegisterExternalImageErrors, RegisterExternalImageResponses, RegisterNodeData, RegisterNodeErrors, RegisterNodeResponses, ReinstallGitlabWebhookData, ReinstallGitlabWebhookErrors, ReinstallGitlabWebhookResponses, RejectPendingActionData, RejectPendingActionErrors, RejectPendingActionResponses, RejectUserPendingActionData, RejectUserPendingActionErrors, RejectUserPendingActionResponses, ReloadPluginsData, ReloadPluginsErrors, ReloadPluginsResponses, RemoveClusterMemberData, RemoveClusterMemberErrors, RemoveClusterMemberResponses, RemoveManagedDomainData, RemoveManagedDomainErrors, RemoveManagedDomainResponses, RemoveRoleData, RemoveRoleErrors, RemoveRoleResponses, RemoveTeamMemberData, RemoveTeamMemberErrors, RemoveTeamMemberResponses, RenameConversationData, RenameConversationErrors, RenameConversationResponses, RenameUserConversationData, RenameUserConversationErrors, RenameUserConversationResponses, RenewDomainData, RenewDomainErrors, RenewDomainResponses, RepointContinuousArchiveSourceData, RepointContinuousArchiveSourceErrors, RepointContinuousArchiveSourceResponses, RequestDiscoveredRouteCertData, RequestDiscoveredRouteCertErrors, RequestDiscoveredRouteCertResponses, RequestPasswordResetData, RequestPasswordResetErrors, RequestPasswordResetResponses, ResetPasswordData, ResetPasswordErrors, ResetPasswordResponses, ResizeSandboxData, ResizeSandboxErrors, ResizeSandboxResponses, ResolveAlarmData, ResolveAlarmErrors, ResolveAlarmResponses, ResolvePermissionData, ResolvePermissionErrors, ResolvePermissionResponses, ResolveSystemAlarmData, ResolveSystemAlarmErrors, ResolveSystemAlarmResponses, ResolveUserPermissionData, ResolveUserPermissionErrors, ResolveUserPermissionResponses, RestartContainerData, RestartContainerErrors, RestartContainerResponses, RestartPreviewGatewayData, RestartPreviewGatewayErrors, RestartPreviewGatewayResponses, RestartSandboxData, RestartSandboxErrors, RestartSandboxResponses, RestoreApplicationData, RestoreApplicationErrors, RestoreApplicationResponses, RestoreFlagData, RestoreFlagErrors, RestoreFlagResponses, RestoreUserConversationData, RestoreUserConversationErrors, RestoreUserConversationResponses, RestoreUserData, RestoreUserErrors, RestoreUserResponses, ResumeDeploymentData, ResumeDeploymentErrors, ResumeDeploymentResponses, ResumeSandboxData, ResumeSandboxErrors, ResumeSandboxResponses, RetryClusterData, RetryClusterErrors, RetryClusterResponses, RetryDeliveryData, RetryDeliveryErrors, RetryDeliveryResponses, RetryFacetBackfillData, RetryFacetBackfillErrors, RetryFacetBackfillResponses, RetryPgUpgradeData, RetryPgUpgradeErrors, RetryPgUpgradeResponses, RetryRunData, RetryRunErrors, RetryRunResponses, RevealGlobalMcpConfigData, RevealGlobalMcpConfigErrors, RevealGlobalMcpConfigResponses, RevealMcpConfigData, RevealMcpConfigErrors, RevealMcpConfigResponses, RevealNotificationProviderConfigData, RevealNotificationProviderConfigErrors, RevealNotificationProviderConfigResponses, RevealServiceEnvironmentVariablesData, RevealServiceEnvironmentVariablesErrors, RevealServiceEnvironmentVariablesResponses, RevealServiceParameterData, RevealServiceParameterErrors, RevealServiceParameterResponses, RevenueCreateIntegrationData, RevenueCreateIntegrationErrors, RevenueCreateIntegrationResponses, RevenueDeleteIntegrationData, RevenueDeleteIntegrationResponses, RevenueGlobalEventsData, RevenueGlobalEventsResponses, RevenueImportInvoicesCsvData, RevenueImportInvoicesCsvErrors, RevenueImportInvoicesCsvResponses, RevenueImportSubscriptionsCsvData, RevenueImportSubscriptionsCsvErrors, RevenueImportSubscriptionsCsvResponses, RevenueListIntegrationsData, RevenueListIntegrationsResponses, RevenueListProvidersData, RevenueListProvidersResponses, RevenueMetricsCustomersData, RevenueMetricsCustomersResponses, RevenueMetricsGlobalMrrData, RevenueMetricsGlobalMrrResponses, RevenueMetricsGlobalSummaryData, RevenueMetricsGlobalSummaryResponses, RevenueMetricsMrrData, RevenueMetricsMrrResponses, RevenueMetricsSummaryData, RevenueMetricsSummaryResponses, RevenueRecentEventsData, RevenueRecentEventsResponses, RevenueRotateTokenData, RevenueRotateTokenResponses, RevenueUpdateConfigData, RevenueUpdateConfigErrors, RevenueUpdateConfigResponses, RevenueUpdateSecretData, RevenueUpdateSecretErrors, RevenueUpdateSecretResponses, RevokeAnalyticsIngestKeyData, RevokeAnalyticsIngestKeyErrors, RevokeAnalyticsIngestKeyResponses, RevokeDsnData, RevokeDsnErrors, RevokeDsnResponses, RevokeEmailDomainProjectData, RevokeEmailDomainProjectErrors, RevokeEmailDomainProjectResponses, RevokeEnrollmentTokenData, RevokeEnrollmentTokenErrors, RevokeEnrollmentTokenResponses, RevokeJoinTokenData, RevokeJoinTokenErrors, RevokeJoinTokenResponses, RevokeProjectAccessData, RevokeProjectAccessErrors, RevokeProjectAccessResponses, RollbackPgUpgradeData, RollbackPgUpgradeErrors, RollbackPgUpgradeResponses, RollbackToDeploymentData, RollbackToDeploymentErrors, RollbackToDeploymentResponses, RootfsGcData, RootfsGcResponses, RootfsReportData, RootfsReportResponses, RotateAnalyticsIngestKeyData, RotateAnalyticsIngestKeyErrors, RotateAnalyticsIngestKeyResponses, RotateApiKeyData, RotateApiKeyErrors, RotateApiKeyResponses, RotateClusterCaData, RotateClusterCaErrors, RotateClusterCaResponses, RotateDeploymentTokenData, RotateDeploymentTokenErrors, RotateDeploymentTokenResponses, RunBackupForSourceData, RunBackupForSourceErrors, RunBackupForSourceResponses, RunConnectionHealthCheckData, RunConnectionHealthCheckErrors, RunConnectionHealthCheckResponses, RunExternalServiceBackupData, RunExternalServiceBackupErrors, RunExternalServiceBackupResponses, RunScheduleNowData, RunScheduleNowErrors, RunScheduleNowResponses, SandboxCreatePreviewLinkData, SandboxCreatePreviewLinkErrors, SandboxCreatePreviewLinkResponses, SaveAgentTokenData, SaveAgentTokenErrors, SaveAgentTokenResponses, SaveAiProviderCredentialData, SaveAiProviderCredentialErrors, SaveAiProviderCredentialResponses, SearchLogsData, SearchLogsErrors, SearchLogsResponses, SendEmailData, SendEmailErrors, SendEmailResponses, SendFailureReportData, SendFailureReportErrors, SendFailureReportResponses, SendProjectAiMessageData, SendProjectAiMessageErrors, SendProjectAiMessageResponses, SendUserMessageData, SendUserMessageErrors, SendUserMessageResponses, SetAiDataAccessData, SetAiDataAccessErrors, SetAiDataAccessResponses, SetAlternateSourcesData, SetAlternateSourcesErrors, SetAlternateSourcesResponses, SetApplicationPrimaryProjectData, SetApplicationPrimaryProjectErrors, SetApplicationPrimaryProjectResponses, SetDefaultS3SourceData, SetDefaultS3SourceErrors, SetDefaultS3SourceResponses, SetFlagEnvironmentData, SetFlagEnvironmentErrors, SetFlagEnvironmentResponses, SetPreviewPasswordData, SetPreviewPasswordErrors, SetPreviewPasswordResponses, SetTraefikDiscoveredRouteEnabledData, SetTraefikDiscoveredRouteEnabledErrors, SetTraefikDiscoveredRouteEnabledResponses, SetupDnsChallengeData, SetupDnsChallengeErrors, SetupDnsChallengeResponses, SetupDnsData, SetupDnsErrors, SetupDnsResponses, SetupEmailTrackingData, SetupEmailTrackingErrors, SetupEmailTrackingResponses, SetupMfaData, SetupMfaErrors, SetupMfaResponses, SilenceAlarmData, SilenceAlarmErrors, SilenceAlarmResponses, SilenceSystemAlarmData, SilenceSystemAlarmErrors, SilenceSystemAlarmResponses, SleepEnvironmentData, SleepEnvironmentErrors, SleepEnvironmentResponses, SmokeTestAgentData, SmokeTestAgentErrors, SmokeTestAgentResponses, SourceSandboxData, SourceSandboxErrors, SourceSandboxResponses, StartAnalysisData, StartAnalysisErrors, StartAnalysisResponses, StartContainerData, StartContainerErrors, StartContainerResponses, StartFixData, StartFixErrors, StartFixResponses, StartGitProviderOauthData, StartGitProviderOauthErrors, StartOidcLoginBySlugData, StartOidcLoginBySlugErrors, StartPgUpgradeData, StartPgUpgradeErrors, StartPgUpgradeResponses, StartRestoreData, StartRestoreErrors, StartRestoreResponses, StartServiceData, StartServiceErrors, StartServiceResponses, StartUpdateData, StartUpdateErrors, StartUpdateResponses, StatPathData, StatPathErrors, StatPathResponses, StopContainerData, StopContainerErrors, StopContainerResponses, StopSandboxData, StopSandboxErrors, StopSandboxResponses, StopServiceData, StopServiceErrors, StopServiceResponses, StopTurnData, StopTurnErrors, StopTurnResponses, StopUserTurnData, StopUserTurnErrors, StopUserTurnResponses, StorageSummaryData, StorageSummaryErrors, StorageSummaryResponses, StreamContainerMetricsData, StreamContainerMetricsErrors, StreamContainerMetricsResponses, StreamEventsData, StreamEventsErrors, StreamEventsResponses, StreamRunEventsData, StreamRunEventsErrors, StreamRunEventsResponses, SyncRepositoriesData, SyncRepositoriesErrors, SyncRepositoriesResponses, TailDeploymentJobLogsData, TailDeploymentJobLogsErrors, TailLogsData, TailLogsErrors, TailLogsResponses, TeardownDeploymentData, TeardownDeploymentErrors, TeardownDeploymentResponses, TeardownEnvironmentData, TeardownEnvironmentErrors, TeardownEnvironmentResponses, TerminalData, TerminalErrors, TestNotificationProviderData, TestNotificationProviderErrors, TestNotificationProviderResponses, TestOidcProviderData, TestOidcProviderResponses, TestProviderConnectionData, TestProviderConnectionErrors, TestProviderConnectionResponses, TestProviderData, TestProviderErrors, TestProviderKeyByIdData, TestProviderKeyByIdErrors, TestProviderKeyByIdResponses, TestProviderKeyInlineData, TestProviderKeyInlineErrors, TestProviderKeyInlineResponses, TestProviderResponses, TestS3ConnectionPreviewData, TestS3ConnectionPreviewErrors, TestS3ConnectionPreviewResponses, TestS3SourceConnectionData, TestS3SourceConnectionErrors, TestS3SourceConnectionResponses, TrackClickData, TrackClickErrors, TrackOpenData, TrackOpenErrors, TrackOpenResponses, TriggerAgentData, TriggerAgentErrors, TriggerAgentResponses, TriggerProjectPipelineData, TriggerProjectPipelineErrors, TriggerProjectPipelineResponses, TriggerScanData, TriggerScanErrors, TriggerScanResponses, TriggerServiceHealthCheckData, TriggerServiceHealthCheckErrors, TriggerServiceHealthCheckResponses, TriggerWeeklyDigestData, TriggerWeeklyDigestErrors, TriggerWeeklyDigestResponses, UnlinkApplicationProjectData, UnlinkApplicationProjectErrors, UnlinkApplicationProjectResponses, UnlinkServiceFromProjectData, UnlinkServiceFromProjectErrors, UnlinkServiceFromProjectResponses, UpdateAgentData, UpdateAgentErrors, UpdateAgentResponses, UpdateAiProviderData, UpdateAiProviderErrors, UpdateAiProviderPreferenceData, UpdateAiProviderPreferenceErrors, UpdateAiProviderPreferenceResponses, UpdateAiProviderResponses, UpdateAiSummaryPreferenceData, UpdateAiSummaryPreferenceErrors, UpdateAiSummaryPreferenceResponses, UpdateAlertData, UpdateAlertErrors, UpdateAlertResponses, UpdateAlertRuleData, UpdateAlertRuleErrors, UpdateAlertRuleResponses, UpdateAnalyticsIngestKeyData, UpdateAnalyticsIngestKeyErrors, UpdateAnalyticsIngestKeyResponses, UpdateApiKeyData, UpdateApiKeyErrors, UpdateApiKeyResponses, UpdateApplicationWorkspaceData, UpdateApplicationWorkspaceErrors, UpdateApplicationWorkspaceResponses, UpdateAutomaticDeployData, UpdateAutomaticDeployErrors, UpdateAutomaticDeployResponses, UpdateBackupScheduleData, UpdateBackupScheduleErrors, UpdateBackupScheduleResponses, UpdateCloudFeaturesData, UpdateCloudFeaturesResponses, UpdateCloudflareProviderData, UpdateCloudflareProviderErrors, UpdateCloudflareProviderResponses, UpdateConnectionTokenData, UpdateConnectionTokenErrors, UpdateConnectionTokenResponses, UpdateCustomDomainData, UpdateCustomDomainErrors, UpdateCustomDomainResponses, UpdateDashboardData, UpdateDashboardErrors, UpdateDashboardResponses, UpdateDeploymentTokenData, UpdateDeploymentTokenErrors, UpdateDeploymentTokenResponses, UpdateEmailProviderData, UpdateEmailProviderErrors, UpdateEmailProviderResponses, UpdateEnvironmentSettingsData, UpdateEnvironmentSettingsErrors, UpdateEnvironmentSettingsResponses, UpdateEnvironmentSubdomainData, UpdateEnvironmentSubdomainErrors, UpdateEnvironmentSubdomainResponses, UpdateEnvironmentVariableData, UpdateEnvironmentVariableErrors, UpdateEnvironmentVariableResponses, UpdateErrorGroupData, UpdateErrorGroupErrors, UpdateErrorGroupResponses, UpdateFlagData, UpdateFlagErrors, UpdateFlagResponses, UpdateFunnelData, UpdateFunnelErrors, UpdateFunnelResponses, UpdateGitProviderCredentialsData, UpdateGitProviderCredentialsErrors, UpdateGitProviderCredentialsResponses, UpdateGitSettingsData, UpdateGitSettingsErrors, UpdateGitSettingsResponses, UpdateGlobalMcpData, UpdateGlobalMcpErrors, UpdateGlobalMcpResponses, UpdateGlobalSkillData, UpdateGlobalSkillErrors, UpdateGlobalSkillResponses, UpdateIncidentStatusData, UpdateIncidentStatusErrors, UpdateIncidentStatusResponses, UpdateIpAccessControlData, UpdateIpAccessControlErrors, UpdateIpAccessControlResponses, UpdateManagedDomainData, UpdateManagedDomainErrors, UpdateManagedDomainResponses, UpdateMcpData, UpdateMcpErrors, UpdateMcpResponses, UpdateNotificationEmailProviderData, UpdateNotificationEmailProviderErrors, UpdateNotificationEmailProviderResponses, UpdateNotificationProviderData, UpdateNotificationProviderErrors, UpdateNotificationProviderResponses, UpdateNotificationRouteData, UpdateNotificationRouteErrors, UpdateNotificationRouteResponses, UpdateOidcProviderData, UpdateOidcProviderResponses, UpdatePermissionModeData, UpdatePermissionModeErrors, UpdatePermissionModeResponses, UpdatePreferencesData, UpdatePreferencesErrors, UpdatePreferencesResponses, UpdateProjectCloudTelemetryData, UpdateProjectCloudTelemetryErrors, UpdateProjectCloudTelemetryResponses, UpdateProjectData, UpdateProjectDeploymentConfigData, UpdateProjectDeploymentConfigErrors, UpdateProjectDeploymentConfigResponses, UpdateProjectErrors, UpdateProjectResponses, UpdateProjectSecretData, UpdateProjectSecretErrors, UpdateProjectSecretResponses, UpdateProjectSettingsData, UpdateProjectSettingsErrors, UpdateProjectSettingsResponses, UpdateProviderData, UpdateProviderErrors, UpdateProviderKeyData, UpdateProviderKeyErrors, UpdateProviderKeyResponses, UpdateProviderModelData, UpdateProviderModelErrors, UpdateProviderModelResponses, UpdateProviderResponses, UpdateRouteData, UpdateRouteErrors, UpdateRouteResponses, UpdateS3SourceData, UpdateS3SourceErrors, UpdateS3SourceResponses, UpdateSelfData, UpdateSelfErrors, UpdateSelfResponses, UpdateServiceData, UpdateServiceErrors, UpdateServiceResourcesData, UpdateServiceResourcesErrors, UpdateServiceResourcesResponses, UpdateServiceResponses, UpdateServiceTemplateRuntimeData, UpdateServiceTemplateRuntimeErrors, UpdateServiceTemplateRuntimeResponses, UpdateSessionDurationData, UpdateSessionDurationErrors, UpdateSessionDurationResponses, UpdateSettingsData, UpdateSettingsErrors, UpdateSettingsResponses, UpdateSkillData, UpdateSkillErrors, UpdateSkillResponses, UpdateSlackProviderData, UpdateSlackProviderErrors, UpdateSlackProviderResponses, UpdateSpeedMetricsData, UpdateSpeedMetricsErrors, UpdateSpeedMetricsResponses, UpdateTeamData, UpdateTeamErrors, UpdateTeamMemberRoleData, UpdateTeamMemberRoleErrors, UpdateTeamMemberRoleResponses, UpdateTeamResponses, UpdateUserData, UpdateUserErrors, UpdateUserPermissionModeData, UpdateUserPermissionModeErrors, UpdateUserPermissionModeResponses, UpdateUserResponses, UpdateWebhookData, UpdateWebhookErrors, UpdateWebhookProviderData, UpdateWebhookProviderErrors, UpdateWebhookProviderResponses, UpdateWebhookResponses, UpgradePreviewGatewayData, UpgradePreviewGatewayErrors, UpgradePreviewGatewayResponses, UpgradeProjectServiceTemplateData, UpgradeProjectServiceTemplateErrors, UpgradeProjectServiceTemplateResponses, UpgradeServiceData, UpgradeServiceErrors, UpgradeServiceResponses, UploadApplicationWorkspaceFilesData, UploadApplicationWorkspaceFilesErrors, UploadApplicationWorkspaceFilesResponses, UploadGlobalSkillData, UploadGlobalSkillErrors, UploadGlobalSkillResponses, UploadGlobalWorkspaceFilesData, UploadGlobalWorkspaceFilesErrors, UploadGlobalWorkspaceFilesResponses, UploadReleaseFileData, UploadReleaseFileErrors, UploadReleaseFileResponses, UploadSkillData, UploadSkillErrors, UploadSkillResponses, UploadSourceFileData, UploadSourceFileErrors, UploadSourceFileResponses, UploadSourceMapData, UploadSourceMapErrors, UploadSourceMapResponses, UploadStaticBundleData, UploadStaticBundleErrors, UploadStaticBundleResponses, UploadUserConversationAttachmentData, UploadUserConversationAttachmentErrors, UploadUserConversationAttachmentResponses, UpsertSecretData, UpsertSecretErrors, UpsertSecretResponses, ValidateConnectionData, ValidateConnectionErrors, ValidateConnectionResponses, ValidateEmailData, ValidateEmailErrors, ValidateEmailResponses, VerifyAndEnableMfaData, VerifyAndEnableMfaErrors, VerifyAndEnableMfaResponses, VerifyDomainData, VerifyDomainErrors, VerifyDomainResponses, VerifyEmailData, VerifyEmailErrors, VerifyEmailResponses, VerifyManagedDomainData, VerifyManagedDomainErrors, VerifyManagedDomainResponses, VerifyMfaChallengeData, VerifyMfaChallengeErrors, VerifyMfaChallengeResponses, VerifyStepUpData, VerifyStepUpErrors, VerifyStepUpResponses, WakeEnvironmentData, WakeEnvironmentErrors, WakeEnvironmentResponses, WebhookTriggerData, WebhookTriggerErrors, WebhookTriggerResponses, WorkflowDryRunData, WorkflowDryRunErrors, WorkflowDryRunResponses, WriteApplicationWorkspaceFilesData, WriteApplicationWorkspaceFilesErrors, WriteApplicationWorkspaceFilesResponses, WriteFileData, WriteFileErrors, WriteFileResponses, WriteFilesData, WriteFilesErrors, WriteFilesResponses } from './types.gen'; +import type { GetPluginStatusData, GetPluginStatusErrors, GetPluginStatusResponses, InstallPluginData, InstallPluginErrors, InstallPluginResponses, ListPluginCatalogData, ListPluginCatalogErrors, ListPluginCatalogResponses } from './types.gen'; + export type Options = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of @@ -9731,6 +9733,22 @@ export const listExternalPlugins = (option ...options }); +export const listPluginCatalog = (options?: Options): RequestResult => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/x/plugins/catalog', + ...options +}); + +export const installPlugin = (options: Options): RequestResult => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/x/plugins/install', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + /** * Reload all external plugins. * @@ -9746,6 +9764,12 @@ export const reloadPlugins = (options?: Op ...options }); +export const getPluginStatus = (options: Options): RequestResult => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/x/plugins/{name}/status', + ...options +}); + /** * Ingest a Sentry envelope (binary payload) */ diff --git a/web/src/api/client/types.gen.ts b/web/src/api/client/types.gen.ts index b11b86a46..a3a1b78ce 100644 --- a/web/src/api/client/types.gen.ts +++ b/web/src/api/client/types.gen.ts @@ -11026,6 +11026,22 @@ export type InsightsResponse = { data: Array; }; +export type InstallPluginRequest = { + /** + * Validated registry name only. URLs, paths, versions, and hashes are not + * accepted from HTTP callers. + */ + name: string; +}; + +export type InstallPluginResponse = { + message: string; + name: string; + platform: string; + sha256: string; + version: string; +}; + export type IntegrationResponse = { config?: null | ProviderConfig; created_at: string; @@ -14359,6 +14375,11 @@ export type PlatformInfo = { platforms: Array; }; +export type PlatformRelease = { + sha256: string; + url: string; +}; + /** * What a plugin is allowed to do with the platform API over the channel. * @@ -14369,6 +14390,13 @@ export type PlatformInfo = { */ export type PluginCapability = 'api_read' | 'api_write'; +export type PluginCatalogResponse = { + available: boolean; + plugins: Array; + reason?: string | null; + source: string; +}; + /** * The complete plugin manifest — the handshake contract. */ @@ -14470,6 +14498,12 @@ export type PluginManifest = { version: string; }; +export type PluginStatusResponse = { + configured: boolean; + reason?: string | null; + setup_path: string; +}; + /** * Port mapping */ @@ -16323,6 +16357,40 @@ export type RegenerateDsnRequest = { base_url?: string | null; }; +/** + * The outer envelope signs the decoded bytes in `payload`. Encoding the + * payload instead of reserializing a JSON object avoids ambiguous map order, + * whitespace, and number representations. + */ +export type RegistryEnvelope = { + key_id: string; + /** + * Standard-base64 encoded JSON [`RegistryDocument`]. + */ + payload: string; + /** + * Standard-base64 encoded 64-byte Ed25519 signature over payload bytes. + */ + signature: string; +}; + +export type RegistryPlugin = { + author: string; + category: string; + description: string; + docs_url?: string | null; + keywords?: Array; + logo_url?: string | null; + name: string; + platforms: { + [key: string]: PlatformRelease; + }; + repository?: string | null; + summary: string; + title: string; + version: string; +}; + export type RegisterImageRequest = { /** * Image digest (sha256:...) @@ -16477,10 +16545,19 @@ export type ReleaseListResponse = { releases: Array; }; +export type ReloadFailureResponse = { + plugin?: string | null; + reason: string; +}; + /** * Response from the reload endpoint. */ export type ReloadResponse = { + /** + * Activated installs that could not be verified or started. + */ + failures: Array; /** * Number of plugins successfully loaded after reload */ @@ -60466,6 +60543,100 @@ export type ListExternalPluginsResponses = { export type ListExternalPluginsResponse = ListExternalPluginsResponses[keyof ListExternalPluginsResponses]; +export type ListPluginCatalogData = { + body?: never; + path?: never; + query?: never; + url: '/x/plugins/catalog'; +}; + +export type ListPluginCatalogErrors = { + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; +}; + +export type ListPluginCatalogError = ListPluginCatalogErrors[keyof ListPluginCatalogErrors]; + +export type ListPluginCatalogResponses = { + /** + * Signed plugin catalogue, or an unavailable state when registry trust is not configured + */ + 200: PluginCatalogResponse; +}; + +export type ListPluginCatalogResponse = ListPluginCatalogResponses[keyof ListPluginCatalogResponses]; + +export type InstallPluginData = { + body: InstallPluginRequest; + path?: never; + query?: never; + url: '/x/plugins/install'; +}; + +export type InstallPluginErrors = { + /** + * Invalid plugin name or registry release + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; + /** + * Registry rollback refused + */ + 409: ProblemDetails; + /** + * Request body exceeds the configured limit + */ + 413: ProblemDetails; + /** + * Request content type is not application/json + */ + 415: ProblemDetails; + /** + * Request JSON does not match the install schema + */ + 422: ProblemDetails; + /** + * Recent sensitive-action verification required + */ + 428: ProblemDetails; + /** + * Local plugin installation failed + */ + 500: ProblemDetails; + /** + * Registry, artifact, or plugin startup verification failed + */ + 502: ProblemDetails; + /** + * Registry trust, plugin service, or security audit unavailable + */ + 503: ProblemDetails; +}; + +export type InstallPluginError = InstallPluginErrors[keyof InstallPluginErrors]; + +export type InstallPluginResponses = { + /** + * Plugin verified, installed, and started + */ + 200: InstallPluginResponse; +}; + +export type InstallPluginResponse2 = InstallPluginResponses[keyof InstallPluginResponses]; + export type ReloadPluginsData = { body?: never; path?: never; @@ -60482,17 +60653,62 @@ export type ReloadPluginsErrors = { * Insufficient permissions */ 403: unknown; + /** + * No activated plugin could be reloaded + */ + 502: ReloadResponse; }; +export type ReloadPluginsError = ReloadPluginsErrors[keyof ReloadPluginsErrors]; + export type ReloadPluginsResponses = { /** - * Plugins reloaded successfully + * All plugins reloaded successfully */ 200: ReloadResponse; + /** + * Some plugins reloaded and some failed + */ + 207: ReloadResponse; }; export type ReloadPluginsResponse = ReloadPluginsResponses[keyof ReloadPluginsResponses]; +export type GetPluginStatusData = { + body?: never; + path: { + name: string; + }; + query?: never; + url: '/x/plugins/{name}/status'; +}; + +export type GetPluginStatusErrors = { + /** + * Invalid plugin name + */ + 400: ProblemDetails; + /** + * Unauthorized + */ + 401: ProblemDetails; + /** + * Insufficient permissions + */ + 403: ProblemDetails; +}; + +export type GetPluginStatusError = GetPluginStatusErrors[keyof GetPluginStatusErrors]; + +export type GetPluginStatusResponses = { + /** + * Verified active plugin status + */ + 200: PluginStatusResponse; +}; + +export type GetPluginStatusResponse = GetPluginStatusResponses[keyof GetPluginStatusResponses]; + export type IngestSentryEnvelopeData = { /** * Sentry envelope as binary data diff --git a/web/src/hooks/usePlugins.ts b/web/src/hooks/usePlugins.ts index 93e72882d..2663352bc 100644 --- a/web/src/hooks/usePlugins.ts +++ b/web/src/hooks/usePlugins.ts @@ -1,40 +1,40 @@ // SPDX-FileCopyrightText: 2024-2026 Temps Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -import { client } from '@/api/client/client.gen' +import { + installPlugin, + listExternalPlugins, + listPluginCatalog, + reloadPlugins, +} from '@/api/client/sdk.gen' +import type { + InstallPluginResponse, + PluginCatalogResponse, + ReloadResponse, +} from '@/api/client/types.gen' import type { PluginManifest } from '@/types/plugins' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' export const PLUGINS_QUERY_KEY = ['external-plugins'] +export const PLUGIN_CATALOG_QUERY_KEY = ['external-plugins', 'catalog'] /** - * Fetch the list of external plugin manifests from /api/x/plugins. - * Returns an empty array if the endpoint is unavailable (e.g., no plugins loaded). + * Fetch the list of running external plugin manifests. + * The endpoint is optional, so startup without a plugin host degrades to an + * empty list instead of breaking navigation throughout the dashboard. */ async function fetchPluginManifests(): Promise { try { - const response = await client.get({ - url: '/x/plugins', - }) - return response.data ?? [] + const response = await listExternalPlugins({ throwOnError: true }) + return (response.data ?? []) as PluginManifest[] } catch { - // Endpoint may not exist if no external plugins are configured. - // Degrade gracefully — no plugins is the default. return [] } } -/** Response from POST /x/plugins/reload */ -export interface ReloadPluginsResponse { - loaded: number - plugins: string[] - message: string -} - /** - * React Query hook to get the list of external plugins. + * React Query hook to get the list of running external plugins. * Caches for 5 minutes since plugins rarely change at runtime. - * Never throws — returns an empty list on failure. */ export function usePlugins() { return useQuery({ @@ -46,22 +46,52 @@ export function usePlugins() { }) } -/** - * Mutation hook to reload all external plugins. - * On success, invalidates the plugins query so the UI refreshes. - */ -export function useReloadPlugins() { +/** Fetch the signed registry catalog exposed by the backend. */ +export function usePluginCatalog(enabled = true) { + return useQuery({ + queryKey: PLUGIN_CATALOG_QUERY_KEY, + queryFn: async (): Promise => { + const response = await listPluginCatalog({ throwOnError: true }) + return response.data + }, + staleTime: 5 * 60 * 1000, + retry: false, + enabled, + }) +} + +/** Install a named plugin release selected and verified by the backend. */ +export function useInstallPlugin() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async (): Promise => { - const response = await client.post({ - url: '/x/plugins/reload', + mutationFn: async (name: string): Promise => { + const response = await installPlugin({ + body: { name }, + throwOnError: true, }) - return response.data! + return response.data + }, + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY }), + queryClient.invalidateQueries({ queryKey: PLUGIN_CATALOG_QUERY_KEY }), + ]) + }, + }) +} + +/** Reload all verified plugin installations from disk. */ +export function useReloadPlugins() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (): Promise => { + const response = await reloadPlugins({ throwOnError: true }) + return response.data }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY }) + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: PLUGINS_QUERY_KEY }) }, }) } diff --git a/web/src/lib/plugin-registry.test.ts b/web/src/lib/plugin-registry.test.ts new file mode 100644 index 000000000..0515b8781 --- /dev/null +++ b/web/src/lib/plugin-registry.test.ts @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { describe, expect, test } from 'bun:test' +import { + canManageExternalPlugins, + pluginInstallAction, + safeRegistryNavigationUrl, +} from './plugin-registry' + +describe('safeRegistryNavigationUrl', () => { + test('accepts absolute HTTP and HTTPS registry metadata', () => { + expect( + safeRegistryNavigationUrl('https://github.com/gotempsh/plugins') + ).toBe('https://github.com/gotempsh/plugins') + }) + + test('rejects script, data, credential, and relative targets', () => { + expect( + safeRegistryNavigationUrl('javascript:alert(document.cookie)') + ).toBeUndefined() + expect( + safeRegistryNavigationUrl('data:image/svg+xml,') + ).toBeUndefined() + expect( + safeRegistryNavigationUrl('https://token@example.com/plugin') + ).toBeUndefined() + expect(safeRegistryNavigationUrl('//evil.example/plugin')).toBeUndefined() + expect(safeRegistryNavigationUrl('/relative/plugin')).toBeUndefined() + }) +}) + +describe('pluginInstallAction', () => { + test('offers installation, recognizes the active release, and permits upgrades', () => { + expect(pluginInstallAction(undefined, '1.0.0')).toBe('install') + expect(pluginInstallAction('1.0.0', '1.0.0')).toBe('installed') + expect(pluginInstallAction('1.0.0', '1.1.0')).toBe('upgrade') + }) +}) + +describe('canManageExternalPlugins', () => { + test('matches the roles that carry the backend SystemAdmin permission', () => { + expect(canManageExternalPlugins('admin')).toBe(true) + expect(canManageExternalPlugins('platform_admin')).toBe(true) + expect(canManageExternalPlugins('user')).toBe(false) + expect(canManageExternalPlugins('reader')).toBe(false) + expect(canManageExternalPlugins(null)).toBe(false) + }) +}) diff --git a/web/src/lib/plugin-registry.ts b/web/src/lib/plugin-registry.ts new file mode 100644 index 000000000..1d323f73d --- /dev/null +++ b/web/src/lib/plugin-registry.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +/** + * Registry metadata is signed but remains untrusted display input. Only + * absolute HTTP(S) URLs may become browser navigation or image targets. + */ +export function safeRegistryNavigationUrl( + value?: string | null +): string | undefined { + if (!value) return undefined + + try { + const url = new URL(value) + if (url.username || url.password) return undefined + return url.protocol === 'https:' || url.protocol === 'http:' + ? url.toString() + : undefined + } catch { + return undefined + } +} + +export type PluginInstallAction = 'install' | 'upgrade' | 'installed' + +const PLUGIN_ADMIN_ROLES = new Set(['admin', 'platform_admin']) + +/** Mirrors the roles that carry `Permission::SystemAdmin` in temps-auth. */ +export function canManageExternalPlugins(role?: string | null): boolean { + return role !== undefined && role !== null && PLUGIN_ADMIN_ROLES.has(role) +} + +export function pluginInstallAction( + installedVersion: string | undefined, + registryVersion: string +): PluginInstallAction { + if (installedVersion === undefined) return 'install' + return installedVersion === registryVersion ? 'installed' : 'upgrade' +} diff --git a/web/src/pages/settings/PluginsPage.test.tsx b/web/src/pages/settings/PluginsPage.test.tsx new file mode 100644 index 000000000..80b95f288 --- /dev/null +++ b/web/src/pages/settings/PluginsPage.test.tsx @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2024-2026 Temps Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +import { beforeEach, describe, expect, mock, test } from 'bun:test' +import { renderToStaticMarkup } from 'react-dom/server' + +let role = 'reader' +let catalogEnabledValues: boolean[] = [] + +mock.module('@/contexts/AuthContext', () => ({ + useAuth: () => ({ user: { role } }), +})) + +mock.module('@/contexts/BreadcrumbContext', () => ({ + useBreadcrumbs: () => ({ setBreadcrumbs: () => undefined }), +})) + +mock.module('@/hooks/usePageTitle', () => ({ + usePageTitle: () => undefined, +})) + +mock.module('@/hooks/useSensitiveActionVerification', () => ({ + useSensitiveActionVerification: () => ({ + handleSensitiveActionError: () => false, + verificationDialog: null, + }), +})) + +mock.module('@/hooks/usePlugins', () => ({ + usePlugins: () => ({ data: [], isLoading: false }), + usePluginCatalog: (enabled = true) => { + catalogEnabledValues.push(enabled) + return { + data: { + available: true, + plugins: [ + { + author: 'Temps', + category: 'Observability', + description: 'Checks deployment health.', + name: 'deployment-health', + platforms: {}, + summary: 'Monitor recent deployments.', + title: 'Deployment Health', + version: '1.0.0', + }, + ], + }, + isLoading: false, + error: null, + } + }, + useInstallPlugin: () => ({ + isPending: false, + mutateAsync: () => Promise.resolve(), + }), + useReloadPlugins: () => ({ + isPending: false, + mutateAsync: () => Promise.resolve(), + }), +})) + +const { PluginsPage } = await import('./PluginsPage') + +describe('PluginsPage management permissions', () => { + beforeEach(() => { + catalogEnabledValues = [] + }) + + test('keeps plugin management and its catalog request disabled for readers', () => { + role = 'reader' + + const markup = renderToStaticMarkup() + + expect(catalogEnabledValues).toEqual([false]) + expect(markup).toContain('Verified plugins currently loaded by Temps.') + expect(markup).toContain('Ask a system administrator to install one.') + expect(markup).not.toContain('Reload Plugins') + expect(markup).not.toContain('>Registry<') + expect(markup).not.toContain('>Install<') + }) + + test('enables the catalog and management controls for system administrators', () => { + role = 'admin' + + const markup = renderToStaticMarkup() + + expect(catalogEnabledValues).toEqual([true]) + expect(markup).toContain('Reload Plugins') + expect(markup).toContain('>Registry<') + expect(markup).toContain('>Install<') + }) +}) diff --git a/web/src/pages/settings/PluginsPage.tsx b/web/src/pages/settings/PluginsPage.tsx index 510e635c8..f2946d370 100644 --- a/web/src/pages/settings/PluginsPage.tsx +++ b/web/src/pages/settings/PluginsPage.tsx @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: 2024-2026 Temps Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 +import type { RegistryPlugin } from '@/api/client/types.gen' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { @@ -10,12 +12,25 @@ import { CardHeader, CardTitle, } from '@/components/ui/card' +import { Skeleton } from '@/components/ui/skeleton' +import { useAuth } from '@/contexts/AuthContext' import { useBreadcrumbs } from '@/contexts/BreadcrumbContext' import { usePageTitle } from '@/hooks/usePageTitle' -import { usePlugins, useReloadPlugins } from '@/hooks/usePlugins' +import { + useInstallPlugin, + usePluginCatalog, + usePlugins, + useReloadPlugins, +} from '@/hooks/usePlugins' +import { useSensitiveActionVerification } from '@/hooks/useSensitiveActionVerification' +import { + canManageExternalPlugins, + pluginInstallAction, + safeRegistryNavigationUrl, +} from '@/lib/plugin-registry' +import { sensitiveActionErrorMessage } from '@/lib/sensitiveActionProblem' import { AlertCircle, - Copy, ExternalLink, Loader2, Puzzle, @@ -24,12 +39,21 @@ import { import { useEffect } from 'react' import { Link } from 'react-router' import { toast } from 'sonner' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' export function PluginsPage() { const { setBreadcrumbs } = useBreadcrumbs() - const { data: plugins = [], isLoading, error } = usePlugins() + const { user } = useAuth() + const canManagePlugins = canManageExternalPlugins(user?.role) + const { data: plugins = [], isLoading: pluginsLoading } = usePlugins() + const { + data: catalog, + isLoading: catalogLoading, + error: catalogError, + } = usePluginCatalog(canManagePlugins) + const installPlugin = useInstallPlugin() const reloadPlugins = useReloadPlugins() + const { handleSensitiveActionError, verificationDialog } = + useSensitiveActionVerification() useEffect(() => { setBreadcrumbs([ @@ -43,305 +67,352 @@ export function PluginsPage() { const handleReload = async () => { try { const result = await reloadPlugins.mutateAsync() - toast.success(result.message) - } catch { - toast.error('Failed to reload plugins') + if (result.failures.length > 0) { + toast.warning(result.message) + } else { + toast.success(result.message) + } + } catch (error) { + if (handleSensitiveActionError(error, () => void handleReload())) return + toast.error( + sensitiveActionErrorMessage(error, 'Failed to reload plugins.') + ) } } - if (isLoading) { - return ( -
- -
- ) - } - - if (error) { - return ( - - - Error - Failed to load plugins. - - ) + const handleInstall = async (name: string) => { + try { + const result = await installPlugin.mutateAsync(name) + toast.success(result.message) + } catch (error) { + if (handleSensitiveActionError(error, () => void handleInstall(name))) { + return + } + toast.error( + sensitiveActionErrorMessage(error, `Failed to install ${name}.`) + ) + } } return (
+ {canManagePlugins && verificationDialog}
External Plugins - Manage external plugin binaries. Plugins are discovered from the - plugins directory on startup or reload. + {canManagePlugins + ? 'Install signed, platform-specific releases from the trusted Temps registry.' + : 'View verified external plugins currently running in Temps.'}
- + {canManagePlugins && ( + + )}
- - - {plugins.length === 0 ? ( -
- -

No plugins installed

-

- Place plugin binaries in the plugins directory and click Reload. -

+ + {canManagePlugins && ( + [plugin.name, plugin.version])) + } + installingName={ + installPlugin.isPending ? installPlugin.variables : undefined + } + onInstall={(name) => void handleInstall(name)} + /> + )} + +
+
+
+

+ Running +

+

+ Verified plugins currently loaded by Temps. +

+
+ + {plugins.length} {plugins.length === 1 ? 'plugin' : 'plugins'} +
- ) : ( -
- {plugins.map((plugin) => ( -
-
-
- -
+ + {pluginsLoading ? ( + + ) : plugins.length === 0 ? ( +
+ +

+ No verified plugins are running. +

+

+ {canManagePlugins + ? 'Install a registry release above to add one.' + : 'Ask a system administrator to install one.'} +

+
+ ) : ( +
+ {plugins.map((plugin) => ( +
-
-

+

+

{plugin.display_name || plugin.name}

- - v{plugin.version} + v{plugin.version} + + Running
{plugin.description && ( -

+

{plugin.description}

)}
-
-
- {plugin.ui && ( - - UI - - )} - {plugin.requires_db && ( - - DB - - )} - - Running - - {/* A plugin listed here with no way to reach it sends the - user hunting through the sidebar. - - Gated on the nav entry, not on `plugin.ui`: that field - describes a *declared* bundle, and a plugin can serve - its UI from `/ui/` without one (some plugins do, and - reports `ui: null`). What actually makes a plugin - reachable is a platform/settings nav entry — those are - what `/plugins/:pluginName` routes to. Project-scoped - entries live under a project and have no address from - here. */} - {plugin.nav.some((e) => e.section !== 'project') && ( + {plugin.nav.some( + (entry) => entry.section !== 'project' + ) && ( )}
-
- ))} -
- )} + ))} +
+ )} +
- -
) } -const PLUGINS_REPO_URL = 'https://github.com/gotempsh/plugins' - -const EXAMPLE_PLUGINS: Array<{ - name: string - description: string - path: string -}> = [ - { - name: 'example-plugin', - description: - 'Minimal "hello world" plugin — the shortest path to understanding the plugin protocol and UI bundle layout.', - path: 'example-plugin', - }, - { - name: 'lighthouse-plugin', - description: - 'Runs Lighthouse audits after deployments and tracks Core Web Vitals over time.', - path: 'lighthouse-plugin', - }, - { - name: 'indexnow-plugin', - description: - 'Automatically submits deployed URLs to Bing, Yandex, and other IndexNow-supporting search engines.', - path: 'indexnow-plugin', - }, - { - name: 'google-indexing-plugin', - description: - 'Notifies the Google Indexing API when pages are published or removed.', - path: 'google-indexing-plugin', - }, -] +interface RegistryCatalogProps { + catalog?: { + available: boolean + plugins: RegistryPlugin[] + reason?: string | null + source: string + } + error: Error | null + installedVersions: Map + installingName?: string + isLoading: boolean + onInstall: (name: string) => void +} -function PluginExamples() { +function RegistryCatalog({ + catalog, + error, + installedVersions, + installingName, + isLoading, + onInstall, +}: RegistryCatalogProps) { return ( - - -
-
- Example Plugins - - Official plugins maintained in{' '} - - gotempsh/plugins - - . Clone the repo, run cargo build --release, and - copy the binary into your plugins directory. - -
- - Prebuilt binaries - - +
+
+
+

+ Registry +

+

+ Releases are selected for this server, hash-verified, and installed + atomically. +

- - - + + {isLoading ? ( + + ) : error ? ( + + + Could not load the plugin registry + + {sensitiveActionErrorMessage(error, 'Try again in a moment.')} + + + ) : catalog?.available === false ? ( + + + Plugin registry is not configured + + {catalog.reason || + 'Configure the registry URL and trusted signing key, then restart Temps.'} + + + ) : catalog?.plugins.length === 0 ? ( +
+ +

The registry has no plugins yet.

+

+ Published releases will appear here automatically. +

+
+ ) : ( +
+
+ {catalog?.plugins.map((plugin) => ( + + ))} +
-
- + )} +
) } -function PluginSetupHelp() { - const pluginsDir = '~/.temps/plugins' +interface RegistryPluginCardProps { + installDisabled: boolean + installedVersion?: string + installing: boolean + onInstall: (name: string) => void + plugin: RegistryPlugin +} - const handleCopy = (value: string) => { - navigator.clipboard.writeText(value) - toast.success('Copied to clipboard') - } +function RegistryPluginCard({ + installDisabled, + installedVersion, + installing, + onInstall, + plugin, +}: RegistryPluginCardProps) { + const repositoryUrl = safeRegistryNavigationUrl(plugin.repository) + const action = pluginInstallAction(installedVersion, plugin.version) + const installed = action === 'installed' + let actionLabel = action === 'upgrade' ? 'Upgrade' : 'Install' + if (installed) actionLabel = 'Installed' + if (installing) + actionLabel = action === 'upgrade' ? 'Upgrading' : 'Installing' return ( -
-
-
- -
-
-
-

How to install a plugin

-

- Temps loads executable binaries from the plugins directory over - stdin/stdout. Drop a binary in, click Reload, and it shows up - below. -

+
+
+ +
+
+

{plugin.title}

+ v{plugin.version} + {plugin.category}
+

+ {plugin.name} +

+
+
-
    -
  1. - 1. -
    -

    - Place the plugin binary in the plugins directory (override - with TEMPS_DATA_DIR): -

    -
    - {pluginsDir} - -
    -
    -
  2. -
  3. - 2. -

    - Ensure the file is executable ( - chmod +x ./my-plugin). -

    -
  4. -
  5. - 3. -

    - Click Reload Plugins above - to discover and start it. -

    -
  6. -
+

+ {plugin.summary} +

- + + By {plugin.author} + +
+ {repositoryUrl && ( + + )} +
+ + ) +} + +function CatalogSkeleton() { + return ( + + ) +} + +function RunningPluginsSkeleton() { + return ( + ) }