From f8e048502920c948886e71d150c106fcd0f057a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 10:33:49 +0200 Subject: [PATCH 01/70] Add validation guards for template cycles, duplicate IFS targets, and duplicate component matches --- cli/golem-cli/src/command_handler/app/mod.rs | 88 ++++++++++++++++++- .../src/command_handler/component/ifs.rs | 4 +- cli/golem-cli/src/model/app.rs | 54 ++++++++++++ cli/golem-cli/src/model/cascade/error.rs | 2 + cli/golem-cli/src/model/cascade/store.rs | 72 +++++++++++++-- 5 files changed, 208 insertions(+), 12 deletions(-) diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 0b8ccdbf4e..2db3516f42 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -34,7 +34,7 @@ use crate::context::Context; use crate::error::service::{MapServiceError, ServiceError}; use crate::error::{HintError, NonSuccessfulExit}; use crate::fs; -use crate::fuzzy::{Error, FuzzySearch}; +use crate::fuzzy::{Error, FuzzySearch, Match}; use crate::log::{ LogColorize, LogIndent, LogOutput, Output, log_action, log_error, log_failed_to, log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, @@ -2598,7 +2598,6 @@ impl AppCommandHandler { .await } - // TODO: forbid matching the same component multiple times // Returns false if there is no app async fn opt_select_components_internal( &self, @@ -2664,6 +2663,29 @@ impl AppCommandHandler { bail!(NonSuccessfulExit); } + // Forbid distinct patterns from resolving to the same component, which would + // otherwise select (build/deploy) that component more than once. + let collisions = duplicate_component_matches(&found); + if !collisions.is_empty() { + logln(""); + log_error(format!( + "The following component names were matched by multiple patterns:\n{}", + collisions + .iter() + .map(|(option, patterns)| { + format!( + " - {} matched by {}", + option.bold(), + patterns.iter().map(|p| p.as_str().bold().to_string()).join(", ") + ) + }) + .join("\n") + )); + logln(""); + + bail!(NonSuccessfulExit); + } + log_fuzzy_matches(&found); let _log_output = silent_selection.then(|| LogOutput::new(Output::TracingDebug)); @@ -2846,3 +2868,65 @@ fn materialize_agent_secret_defaults( (materialized_defaults, unused_paths) } + +/// Returns the components (sorted) that were matched by more than one fuzzy pattern, +/// each paired with the patterns that matched it. Empty when there are no collisions. +fn duplicate_component_matches(found: &[Match]) -> Vec<(String, Vec)> { + let mut duplicate_options = found + .iter() + .map(|m| m.option.as_str()) + .counts() + .into_iter() + .filter(|&(_, count)| count > 1) + .map(|(option, _)| option.to_string()) + .collect::>(); + duplicate_options.sort(); + + duplicate_options + .into_iter() + .map(|option| { + let patterns = found + .iter() + .filter(|m| m.option == option) + .map(|m| m.pattern.clone()) + .collect::>(); + (option, patterns) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::duplicate_component_matches; + use crate::fuzzy::Match; + use test_r::test; + + fn matched(option: &str, pattern: &str) -> Match { + Match { + option: option.to_string(), + pattern: pattern.to_string(), + exact_match: option == pattern, + } + } + + #[test] + fn detects_two_patterns_matching_the_same_component() { + let found = vec![ + matched("payment-service", "payment-service"), + matched("payment-service", "payment"), + ]; + assert_eq!( + duplicate_component_matches(&found), + vec![( + "payment-service".to_string(), + vec!["payment-service".to_string(), "payment".to_string()] + )] + ); + } + + #[test] + fn no_collision_for_distinct_components() { + let found = vec![matched("a", "a"), matched("b", "b")]; + assert!(duplicate_component_matches(&found).is_empty()); + } +} diff --git a/cli/golem-cli/src/command_handler/component/ifs.rs b/cli/golem-cli/src/command_handler/component/ifs.rs index 5cf2879eeb..dd4cedf594 100644 --- a/cli/golem-cli/src/command_handler/component/ifs.rs +++ b/cli/golem-cli/src/command_handler/component/ifs.rs @@ -580,7 +580,9 @@ impl FileProcessor for FileHasher { } } -// TODO: add this to manifest validation (too or instead of doing it here)? +// Backstop for duplicate IFS targets that only appear after directory sources are +// expanded at build time. Literal duplicate targets are also caught earlier during +// manifest validation (see `AppBuilder::validate_unique_file_targets` in `model/app.rs`). fn validate_unique_targets(component_files: &[InitialComponentFile]) -> anyhow::Result<()> { let non_unique_target_paths = component_files .iter() diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index b50f704035..b46f5db06f 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -3747,6 +3747,7 @@ mod app_builder { component_dir, &component_layer_properties, ); + Self::validate_unique_file_targets(validation, &component_properties); self.validate_component_dependencies( validation, &component_name, @@ -3761,6 +3762,32 @@ mod app_builder { } } + // Manifest-time check for duplicate IFS target paths. This catches obvious + // collisions between literal file entries early (before deploy). Directory sources + // are only expanded at build time, so `ifs::validate_unique_targets` remains the + // backstop for collisions that only appear after expansion. + fn validate_unique_file_targets( + validation: &mut ValidationBuilder, + component_properties: &ComponentProperties, + ) { + let duplicate_targets = component_properties + .files + .iter() + .map(|file| &file.target.path) + .counts() + .into_iter() + .filter(|&(_, count)| count > 1) + .map(|(path, _)| path.to_string()) + .collect::>(); + + if !duplicate_targets.is_empty() { + validation.add_error(format!( + "Multiple initial component files map to the same target path: {}", + duplicate_targets.into_iter().join(", ") + )); + } + } + fn validate_component_dependencies( &self, validation: &mut ValidationBuilder, @@ -4584,6 +4611,33 @@ mod test { ); } + #[test] + fn component_files_reject_duplicate_target_paths() { + let errors = load_app_errors(indoc! { r#" + app: hello-app + + environments: + local: + server: local + + components: + app:main: + componentWasm: dummy-component.wasm + files: + - sourcePath: a.txt + targetPath: /data/shared.txt + - sourcePath: b.txt + targetPath: /data/shared.txt + "# }); + + assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}"); + assert!( + errors[0].contains("same target path") && errors[0].contains("/data/shared.txt"), + "unexpected error: {}", + errors[0] + ); + } + #[test] fn non_rust_guest_bridge_mode_is_rejected() { let source = indoc! { r#" diff --git a/cli/golem-cli/src/model/cascade/error.rs b/cli/golem-cli/src/model/cascade/error.rs index 9b62613084..25b119d5f8 100644 --- a/cli/golem-cli/src/model/cascade/error.rs +++ b/cli/golem-cli/src/model/cascade/error.rs @@ -20,6 +20,8 @@ pub enum StoreGetValueError { LayerNotFound(L::Id), #[error("layer ({0}) apply error: {1}")] LayerApplyError(L::Id, L::ApplyError), + #[error("circular parent layers detected: {0:?}")] + CircularParents(Vec), } #[derive(Debug, thiserror::Error)] diff --git a/cli/golem-cli/src/model/cascade/store.rs b/cli/golem-cli/src/model/cascade/store.rs index 2081d78880..6a7d054a43 100644 --- a/cli/golem-cli/src/model/cascade/store.rs +++ b/cli/golem-cli/src/model/cascade/store.rs @@ -27,7 +27,6 @@ impl Default for Store { } } -// TODO: check for circular parents (either on add or on get, or have a separate validation step) impl Store { pub fn new() -> Store { Self { @@ -62,26 +61,36 @@ impl Store { return Err(StoreGetValueError::LayerNotFound(id.clone())); }; - fn apply_layer( - store: &Store, + fn apply_layer<'a, L: Layer>( + store: &'a Store, ctx: &L::ApplyContext, selector: &L::Selector, - layer: &L, + layer: &'a L, value: &mut L::Value, + path: &mut Vec<&'a L::Id>, ) -> Result<(), StoreGetValueError> { - for layer_id in layer.parent_layers() { - let Some(layer) = store.layers.get(layer_id) else { - return Err(StoreGetValueError::LayerNotFound(layer_id.clone())); + let layer_id = layer.id(); + if path.contains(&layer_id) { + let mut chain = path.iter().map(|id| (*id).clone()).collect::>(); + chain.push(layer_id.clone()); + return Err(StoreGetValueError::CircularParents(chain)); + } + path.push(layer_id); + for parent_id in layer.parent_layers() { + let Some(parent) = store.layers.get(parent_id) else { + return Err(StoreGetValueError::LayerNotFound(parent_id.clone())); }; - apply_layer(store, ctx, selector, layer, value)?; + apply_layer(store, ctx, selector, parent, value, path)?; } if let Some(err) = layer.apply_onto_parent(ctx, selector, value).err() { return Err(StoreGetValueError::LayerApplyError(layer.id().clone(), err)); }; + path.pop(); Ok(()) } let mut value = L::Value::default(); - apply_layer(self, ctx, selector, layer, &mut value)?; + let mut path = Vec::new(); + apply_layer(self, ctx, selector, layer, &mut value, &mut path)?; Ok(value) } } @@ -89,6 +98,7 @@ impl Store { #[cfg(test)] mod test { use super::Store; + use crate::model::cascade::error::StoreGetValueError; use crate::model::cascade::layer::Layer; use test_r::test; @@ -150,4 +160,48 @@ mod test { let value = store.value(&"leaf".to_string(), &(), &()).unwrap(); assert_eq!(value, vec!["base", "mid", "leaf"]); } + + fn add(store: &mut Store, id: &str, parents: &[&str]) { + store + .add_layer(TestLayer { + id: id.to_string(), + parents: parents.iter().map(|p| p.to_string()).collect(), + }) + .unwrap(); + } + + #[test] + fn value_detects_circular_parents_instead_of_overflowing() { + let mut store = Store::::new(); + add(&mut store, "a", &["b"]); + add(&mut store, "b", &["a"]); + + let err = store.value(&"a".to_string(), &(), &()).unwrap_err(); + assert!( + matches!(err, StoreGetValueError::CircularParents(_)), + "expected CircularParents, got {err:?}" + ); + } + + #[test] + fn value_detects_self_referencing_parent() { + let mut store = Store::::new(); + add(&mut store, "a", &["a"]); + + let err = store.value(&"a".to_string(), &(), &()).unwrap_err(); + assert!(matches!(err, StoreGetValueError::CircularParents(_))); + } + + #[test] + fn value_allows_diamond_shaped_parents() { + // a -> {b, c} -> d : d is reachable via two paths but is not a cycle. + let mut store = Store::::new(); + add(&mut store, "d", &[]); + add(&mut store, "b", &["d"]); + add(&mut store, "c", &["d"]); + add(&mut store, "a", &["b", "c"]); + + let value = store.value(&"a".to_string(), &(), &()).unwrap(); + assert_eq!(value, vec!["d", "b", "d", "c", "a"]); + } } From 6f4bb7ef0391d624633ed214662128f39fd87b68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 11:01:08 +0200 Subject: [PATCH 02/70] Add configurable agent stream ping interval --- cli/golem-cli/src/command.rs | 19 +++++++++++++++++++ .../src/command_handler/worker/mod.rs | 3 +++ .../src/command_handler/worker/stream.rs | 9 +++++++-- cli/golem-cli/src/context.rs | 7 +++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 69b61b3e8e..73f880ce19 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -55,6 +55,7 @@ use lenient_bool::LenientBool; use std::collections::{BTreeSet, HashMap}; use std::ffi::OsString; use std::path::PathBuf; +use std::time::Duration; /// Golem Command Line Interface #[derive(Debug, Parser)] @@ -221,6 +222,9 @@ pub struct GolemCliGlobalFlags { #[arg(skip)] http_parallelism: Option, + #[arg(skip)] + agent_stream_ping_interval: Option, + #[arg(skip)] pub auth_token: Option, @@ -294,6 +298,16 @@ impl GolemCliGlobalFlags { })?) } + if let Ok(interval) = std::env::var("GOLEM_AGENT_STREAM_PING_INTERVAL") { + self.agent_stream_ping_interval = Some( + iso8601::duration(&interval) + .map_err(|err| { + anyhow!("Failed to parse GOLEM_AGENT_STREAM_PING_INTERVAL ({interval}): {err}") + })? + .into(), + ); + } + if let Ok(auth_token) = std::env::var("GOLEM_AUTH_TOKEN") { self.auth_token = Some( auth_token @@ -333,6 +347,11 @@ impl GolemCliGlobalFlags { self.http_parallelism.unwrap_or(4) } + pub fn agent_stream_ping_interval(&self) -> Duration { + self.agent_stream_ping_interval + .unwrap_or_else(|| Duration::from_secs(1)) + } + pub fn verbosity(&self) -> clap_verbosity_flag::Verbosity { self.verbosity.as_clap_verbosity_flag() } diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 6c644f21a1..dd36f087ff 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -501,6 +501,7 @@ impl WorkerCommandHandler { stream_args.into(), self.ctx.allow_insecure(), self.ctx.format(), + self.ctx.agent_stream_ping_interval(), if trigger { None } else { @@ -589,6 +590,7 @@ impl WorkerCommandHandler { stream_args.into(), self.ctx.allow_insecure(), self.ctx.format(), + self.ctx.agent_stream_ping_interval(), None, ) .await?; @@ -646,6 +648,7 @@ impl WorkerCommandHandler { stream_args.into(), self.ctx.allow_insecure(), self.ctx.format(), + self.ctx.agent_stream_ping_interval(), Some(idempotency_key), ) .await?; diff --git a/cli/golem-cli/src/command_handler/worker/stream.rs b/cli/golem-cli/src/command_handler/worker/stream.rs index cdf036b336..fcce4f2af8 100644 --- a/cli/golem-cli/src/command_handler/worker/stream.rs +++ b/cli/golem-cli/src/command_handler/worker/stream.rs @@ -48,6 +48,7 @@ pub struct WorkerConnection { idempotency_key: Option, last_seen_idempotency_key: Arc>>, goal_reached: Arc, + ping_interval: Duration, } impl WorkerConnection { @@ -60,6 +61,7 @@ impl WorkerConnection { connect_options: AgentLogStreamOptions, allow_insecure: bool, format: Format, + ping_interval: Duration, idempotency_key: Option, ) -> anyhow::Result { let (request, connector) = Self::create_request( @@ -81,6 +83,7 @@ impl WorkerConnection { idempotency_key, last_seen_idempotency_key, goal_reached, + ping_interval, }) } @@ -117,7 +120,8 @@ impl WorkerConnection { let (write, read) = ws_stream.split(); - let pings = task::spawn(async move { Self::ping_loop(write).await }); + let ping_interval = self.ping_interval; + let pings = task::spawn(async move { Self::ping_loop(write, ping_interval).await }); let output = self.output.clone(); let last_seen_idempotency_key = self.last_seen_idempotency_key.clone(); @@ -195,8 +199,9 @@ impl WorkerConnection { async fn ping_loop( mut write: SplitSink>, Message>, + ping_interval: Duration, ) -> anyhow::Error { - let mut interval = time::interval(Duration::from_secs(1)); // TODO configure + let mut interval = time::interval(ping_interval); let mut cnt: i64 = 1; loop { diff --git a/cli/golem-cli/src/context.rs b/cli/golem-cli/src/context.rs index 1913ed4763..0e83221c96 100644 --- a/cli/golem-cli/src/context.rs +++ b/cli/golem-cli/src/context.rs @@ -54,6 +54,7 @@ use golem_common::model::http_api_deployment::{ use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use std::sync::Arc; +use std::time::Duration; use tracing::{Level, debug, enabled}; use url::Url; @@ -74,6 +75,7 @@ pub struct Context { app_context_config: Option, http_batch_size: u64, http_parallelism: usize, + agent_stream_ping_interval: Duration, auth_token_override: Option, client_config: ClientConfig, yes: bool, @@ -338,6 +340,7 @@ impl Context { app_context_config, http_batch_size: global_flags.http_batch_size(), http_parallelism: global_flags.http_parallelism(), + agent_stream_ping_interval: global_flags.agent_stream_ping_interval(), auth_token_override: global_flags.auth_token, environment_reference, manifest_environment, @@ -453,6 +456,10 @@ impl Context { self.http_parallelism } + pub fn agent_stream_ping_interval(&self) -> Duration { + self.agent_stream_ping_interval + } + pub async fn golem_clients(&self) -> anyhow::Result<&GolemClients> { self.golem_clients .get_or_try_init(|| async { From 5b8c5ee6e66c91e9c272b7b4bade6fa85a513be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 11:01:18 +0200 Subject: [PATCH 03/70] Update debug comments for task result marker fields to clarify usage --- cli/golem-cli/src/app/build/task_result_marker.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/golem-cli/src/app/build/task_result_marker.rs b/cli/golem-cli/src/app/build/task_result_marker.rs index e8c318b096..7ed9abfb1a 100644 --- a/cli/golem-cli/src/app/build/task_result_marker.rs +++ b/cli/golem-cli/src/app/build/task_result_marker.rs @@ -506,7 +506,9 @@ impl TaskResultMarker { fs::write_str( &self.marker_file_path, &serde_json::to_string_pretty(&TaskResult { - // TODO: setting kind, id and hash_input could be driven by a debug flag, env or build + // kind/id/hash_input are debug-only, but cheap to write: they are already + // held in memory, small in practice, and written once per task off the build + // hot path. We keep them to aid debugging of stale/incremental rebuilds. kind: Some(self.kind.to_string()), id: Some(self.id), hash_input: Some(self.hash_input), From 7a02b3fa2051f4e2524c4eea2828ed660c8a6572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 11:58:38 +0200 Subject: [PATCH 04/70] cli healthcheck cleanups --- cli/golem-cli/src/client.rs | 14 +------------- cli/golem-cli/src/config.rs | 13 ------------- cli/golem/src/router.rs | 11 +++++++++-- 3 files changed, 10 insertions(+), 28 deletions(-) diff --git a/cli/golem-cli/src/client.rs b/cli/golem-cli/src/client.rs index 4757a0c18b..65c922832e 100644 --- a/cli/golem-cli/src/client.rs +++ b/cli/golem-cli/src/client.rs @@ -19,7 +19,7 @@ use golem_client::api::{ AccountClientLive, AccountSummaryClientLive, AgentClientLive, AgentSecretsClientLive, AgentTypesClientLive, ApiDeploymentClientLive, ApiDomainClientLive, ApiSecurityClientLive, ApplicationClientLive, CardClientLive, ComponentClientLive, DeploymentClientLive, - EnvironmentClientLive, HealthCheckClientLive, LoginClientLive, McpDeploymentClientLive, + EnvironmentClientLive, LoginClientLive, McpDeploymentClientLive, MeClientLive, PermissionSharesClientLive, PluginClientLive, ResourcesClientLive, RetryPoliciesClientLive, TokenClientLive, WorkerClientLive, }; @@ -265,7 +265,6 @@ pub struct GolemClients { pub application: ApplicationClientLive, pub card: CardClientLive, pub component: ComponentClientLive, - pub component_healthcheck: HealthCheckClientLive, pub deployment: DeploymentClientLive, pub environment: EnvironmentClientLive, pub login: LoginClientLive, @@ -285,12 +284,9 @@ impl GolemClients { auth_config: &AuthenticationConfigWithSource, config_dir: &Path, ) -> anyhow::Result { - let without_retry = MiddlewareConfig::without_retry(); let with_service_retry = MiddlewareConfig::with_service_retry(); let with_invoke_retry = MiddlewareConfig::with_invoke_retry(); - let healthcheck_http_client = - new_reqwest_client(&config.health_check_http_client_config, &without_retry)?; let service_http_client = new_reqwest_client(&config.service_http_client_config, &with_service_retry)?; let invoke_http_client = @@ -316,11 +312,6 @@ impl GolemClients { security_token: security_token.clone(), }; - let registry_healthcheck_context = || ClientContext { - client: healthcheck_http_client, - base_url: config.registry_url.clone(), - security_token: Security::Empty, - }; let worker_context = || ClientContext { client: service_http_client.clone(), @@ -381,9 +372,6 @@ impl GolemClients { component: ComponentClientLive { context: registry_context(), }, - component_healthcheck: HealthCheckClientLive { - context: registry_healthcheck_context(), - }, deployment: DeploymentClientLive { context: registry_context(), }, diff --git a/cli/golem-cli/src/config.rs b/cli/golem-cli/src/config.rs index af784a31d4..e6d1f1afc2 100644 --- a/cli/golem-cli/src/config.rs +++ b/cli/golem-cli/src/config.rs @@ -333,7 +333,6 @@ pub struct ClientConfig { pub worker_url: Url, pub service_http_client_config: HttpClientConfig, pub invoke_http_client_config: HttpClientConfig, - pub health_check_http_client_config: HttpClientConfig, pub file_download_http_client_config: HttpClientConfig, } @@ -353,7 +352,6 @@ impl From<&Profile> for ClientConfig { worker_url, service_http_client_config: HttpClientConfig::new_for_service_calls(allow_insecure), invoke_http_client_config: HttpClientConfig::new_for_invoke(allow_insecure), - health_check_http_client_config: HttpClientConfig::new_for_health_check(allow_insecure), file_download_http_client_config: HttpClientConfig::new_for_file_download( allow_insecure, ), @@ -405,7 +403,6 @@ impl From<&Server> for ClientConfig { worker_url, service_http_client_config: HttpClientConfig::new_for_service_calls(allow_insecure), invoke_http_client_config: HttpClientConfig::new_for_invoke(allow_insecure), - health_check_http_client_config: HttpClientConfig::new_for_health_check(allow_insecure), file_download_http_client_config: HttpClientConfig::new_for_file_download( allow_insecure, ), @@ -442,16 +439,6 @@ impl HttpClientConfig { .with_env_overrides("GOLEM_HTTP_INVOKE") } - pub fn new_for_health_check(allow_insecure: bool) -> Self { - Self { - allow_insecure, - timeout: Some(Duration::from_secs(2)), - connect_timeout: Some(Duration::from_secs(1)), - read_timeout: Some(Duration::from_secs(1)), - } - .with_env_overrides("GOLEM_HTTP_HEALTHCHECK") - } - pub fn new_for_file_download(allow_insecure: bool) -> Self { Self { allow_insecure, diff --git a/cli/golem/src/router.rs b/cli/golem/src/router.rs index 633925158e..96810028de 100644 --- a/cli/golem/src/router.rs +++ b/cli/golem/src/router.rs @@ -150,8 +150,15 @@ pub async fn start_router( .with(OpenTelemetryMetrics::new()) .with(Tracing); - // TODO: have proper healtchchecks, pass them through the different services and expose here -- similar to metrics - // for now just use the one from component service. + // `/healthcheck` is intentionally served by the registry service's static endpoint (via the `*` + // fallback above) rather than an aggregated per-service check. The router only starts once every + // service has started and bound its ports, so this is a "server process is up" signal for the local + // tooling that polls it. It deliberately does NOT reflect deep readiness: worker executors register + // with an empty shard set and receive assignments in the background, the shard manager reconciles its + // ring in the background, and the worker service connects to the registry lazily. Since no service + // gates its own health on that readiness, an aggregated check here could not report it either. The CLI + // does not consume healthchecks, and deployed servers use each service's own per-service probe, so + // aggregating here would add nothing. join_set.spawn( async move { From b7a75bd56def6743c1ecba90df7a1e39fc8e6dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 12:33:18 +0200 Subject: [PATCH 05/70] Add context to staging approval prompts with operation details, todo cleanup --- cli/golem-cli/src/command_handler/app/mod.rs | 22 ++++++++++++++----- .../src/command_handler/interactive.rs | 12 ++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 2db3516f42..b2cdb0c246 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -1932,16 +1932,17 @@ impl AppCommandHandler { let http_api_deployment_handler = self.ctx.api_deployment_handler(); let interactive_handler = self.ctx.interactive_handler(); - let approve = || { - if approve_staging_steps && !interactive_handler.confirm_staging_next_step()? { + let approve = |operation: &str, name: &str| { + if approve_staging_steps + && !interactive_handler.confirm_staging_next_step(operation, name)? + { bail!("Aborted staging"); } Ok(()) }; - // TODO for (component_name, component_diff) in &diff_stage.components { - approve()?; + approve(staging_operation(component_diff), component_name)?; let component_name = ComponentName(component_name.to_string()); @@ -2013,7 +2014,7 @@ impl AppCommandHandler { } for (domain, http_api_deployment_diff) in &diff_stage.http_api_deployments { - approve()?; + approve(staging_operation(http_api_deployment_diff), domain)?; let domain = Domain(domain.to_string()); @@ -2047,7 +2048,7 @@ impl AppCommandHandler { } for (domain, mcp_deployment_diff) in &diff_stage.mcp_deployments { - approve()?; + approve(staging_operation(mcp_deployment_diff), domain)?; let domain = Domain(domain.to_string()); @@ -2869,6 +2870,15 @@ fn materialize_agent_secret_defaults( (materialized_defaults, unused_paths) } +/// The staging operation verb for a diff entry, used to add context to staging approval prompts. +fn staging_operation(diff: &diff::BTreeMapDiffValue) -> &'static str { + match diff { + diff::BTreeMapDiffValue::Create => "create", + diff::BTreeMapDiffValue::Delete => "delete", + diff::BTreeMapDiffValue::Update(_) => "update", + } +} + /// Returns the components (sorted) that were matched by more than one fuzzy pattern, /// each paired with the patterns that matched it. Empty when there are no collisions. fn duplicate_component_matches(found: &[Match]) -> Vec<(String, Vec)> { diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index cb3f2eaa6b..92045e631f 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -128,8 +128,16 @@ impl InteractiveHandler { ) } - pub fn confirm_staging_next_step(&self) -> anyhow::Result { - self.confirm(true, "Continue with the next staging step?", None) + pub fn confirm_staging_next_step( + &self, + operation: &str, + name: impl AsRef, + ) -> anyhow::Result { + self.confirm( + true, + format!("Continue with staging step ({operation} {})?", name.as_ref()), + None, + ) } pub fn confirm_auto_deploy_component( From 0e77f0b30a83d642b8bf970243ad503eb31fbbc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 15:12:53 +0200 Subject: [PATCH 06/70] remove unused `OverwriteSafeAction` implementation and associated logic --- cli/golem-cli/src/fs.rs | 185 --------------------------------------- cli/golem-cli/src/log.rs | 87 ------------------ 2 files changed, 272 deletions(-) diff --git a/cli/golem-cli/src/fs.rs b/cli/golem-cli/src/fs.rs index ac1fe35104..a5d070d681 100644 --- a/cli/golem-cli/src/fs.rs +++ b/cli/golem-cli/src/fs.rs @@ -312,191 +312,6 @@ pub fn metadata>(path: P) -> anyhow::Result { .with_context(|| anyhow!("Failed to get metadata for {}", path.log_color_highlight())) } -// TODO: we most probably do not need this anymore -pub enum OverwriteSafeAction { - CopyFile { - source: PathBuf, - target: PathBuf, - }, - CopyFileTransformed { - source: PathBuf, - source_content_transformed: String, - target: PathBuf, - }, - WriteFile { - content: String, - target: PathBuf, - }, -} - -impl OverwriteSafeAction { - pub fn copy_file_transformed( - source: PathBuf, - target: PathBuf, - transform: F, - ) -> anyhow::Result - where - F: FnOnce(String) -> anyhow::Result, - { - let content = std::fs::read_to_string(&source).with_context(|| { - anyhow!( - "Failed to read file as string, path: {}", - source.log_color_highlight() - ) - })?; - - let source_transformed = transform(content).with_context(|| { - anyhow!( - "Failed to transform file, path: {}", - source.log_color_highlight() - ) - })?; - - Ok(OverwriteSafeAction::CopyFileTransformed { - source, - source_content_transformed: source_transformed, - target, - }) - } - - pub fn target(&self) -> &Path { - match self { - OverwriteSafeAction::CopyFile { target, .. } => target, - OverwriteSafeAction::CopyFileTransformed { target, .. } => target, - OverwriteSafeAction::WriteFile { target, .. } => target, - } - } -} - -#[derive(Copy, Clone, PartialEq)] -pub enum OverwriteSafeActionPlan { - Create, - Overwrite, - SkipSameContent, -} - -pub struct OverwriteSafeActions(Vec); - -impl Default for OverwriteSafeActions { - fn default() -> Self { - Self::new() - } -} - -impl OverwriteSafeActions { - pub fn new() -> Self { - OverwriteSafeActions(Vec::new()) - } - - pub fn add(&mut self, action: OverwriteSafeAction) -> &mut Self { - self.0.push(action); - self - } - - pub fn targets(&self) -> Vec<&Path> { - self.0.iter().map(|a| a.target()).collect() - } - - pub fn run( - self, - allow_overwrite: bool, - allow_skip_by_content: bool, - log_action: F, - ) -> anyhow::Result> - where - F: Fn(&OverwriteSafeAction, OverwriteSafeActionPlan), - { - let actions_with_plan = { - let mut actions_with_plan = - Vec::<(OverwriteSafeAction, OverwriteSafeActionPlan)>::new(); - let mut forbidden_overwrites = Vec::::new(); - - for action in self.0 { - let plan = match &action { - OverwriteSafeAction::CopyFile { source, target } => Self::plan_for_action( - allow_overwrite, - allow_skip_by_content, - target, - || has_same_string_content(source, target), - )?, - OverwriteSafeAction::CopyFileTransformed { - source_content_transformed: source_transformed, - target, - .. - } => Self::plan_for_action( - allow_overwrite, - allow_skip_by_content, - target, - || has_str_content(target, source_transformed), - )?, - OverwriteSafeAction::WriteFile { content, target } => Self::plan_for_action( - allow_overwrite, - allow_skip_by_content, - target, - || has_str_content(target, content), - )?, - }; - match plan { - Some(plan) => actions_with_plan.push((action, plan)), - None => forbidden_overwrites.push(action), - } - } - - if !forbidden_overwrites.is_empty() { - return Ok(forbidden_overwrites); - } - - actions_with_plan - }; - - for (action, plan) in actions_with_plan { - log_action(&action, plan); - if plan == OverwriteSafeActionPlan::SkipSameContent { - continue; - } - - match action { - OverwriteSafeAction::CopyFile { source, target } => { - copy(source, target)?; - } - OverwriteSafeAction::CopyFileTransformed { - source_content_transformed, - target, - .. - } => { - write_str(target, &source_content_transformed)?; - } - OverwriteSafeAction::WriteFile { content, target } => { - write_str(target, &content)?; - } - } - } - - Ok(Vec::new()) - } - - fn plan_for_action( - allow_overwrite: bool, - allow_skip_by_content: bool, - target: P, - skip_by_content: F, - ) -> anyhow::Result> - where - P: AsRef, - F: FnOnce() -> anyhow::Result, - { - if !target.as_ref().exists() { - Ok(Some(OverwriteSafeActionPlan::Create)) - } else if allow_skip_by_content && skip_by_content()? { - Ok(Some(OverwriteSafeActionPlan::SkipSameContent)) - } else if allow_overwrite { - Ok(Some(OverwriteSafeActionPlan::Overwrite)) - } else { - Ok(None) - } - } -} - pub fn resolve_relative_glob, S: AsRef>( base_dir: P, glob: S, diff --git a/cli/golem-cli/src/log.rs b/cli/golem-cli/src/log.rs index cd9c2ab4a7..f92a7b979e 100644 --- a/cli/golem-cli/src/log.rs +++ b/cli/golem-cli/src/log.rs @@ -14,7 +14,6 @@ use crate::app::error::AppValidationError; use crate::error::{HintError, NonSuccessfulExit, PipedExitCode}; -use crate::fs::{OverwriteSafeAction, OverwriteSafeActionPlan}; use anyhow::anyhow; use camino::{Utf8Path, Utf8PathBuf}; use colored::{ColoredString, Colorize}; @@ -382,92 +381,6 @@ pub fn log_skipping_up_to_date(subject: impl AsRef) { ); } -pub fn log_action_plan(action: &OverwriteSafeAction, plan: OverwriteSafeActionPlan) { - match plan { - OverwriteSafeActionPlan::Create => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_action( - "Copying", - format!( - "{} to {}", - source.log_color_highlight(), - target.log_color_highlight() - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_action( - "Copying", - format!( - "{} to {} transformed", - source.log_color_highlight(), - target.log_color_highlight() - ), - ); - } - OverwriteSafeAction::WriteFile { target, .. } => { - log_action("Creating", format!("{}", target.log_color_highlight())); - } - }, - OverwriteSafeActionPlan::Overwrite => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_warn_action( - "Overwriting", - format!( - "{} with {}", - target.log_color_highlight(), - source.log_color_highlight() - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_warn_action( - "Overwriting", - format!( - "{} with {} transformed", - target.log_color_highlight(), - source.log_color_highlight() - ), - ); - } - OverwriteSafeAction::WriteFile { content: _, target } => { - log_warn_action("Overwriting", format!("{}", target.log_color_highlight())); - } - }, - OverwriteSafeActionPlan::SkipSameContent => match action { - OverwriteSafeAction::CopyFile { source, target } => { - log_warn_action( - "Skipping", - format!( - "copying {} to {}, content already up-to-date", - source.log_color_highlight(), - target.log_color_highlight(), - ), - ); - } - OverwriteSafeAction::CopyFileTransformed { source, target, .. } => { - log_warn_action( - "Skipping", - format!( - "copying {} to {} transformed, content already up-to-date", - source.log_color_highlight(), - target.log_color_highlight() - ), - ); - } - OverwriteSafeAction::WriteFile { content: _, target } => { - log_warn_action( - "Skipping", - format!( - "generating {}, content already up-to-date", - target.log_color_highlight() - ), - ); - } - }, - } -} - pub trait LogColorize { fn as_str(&self) -> impl Colorize; From ae6cb56c5f84e335045af4d3bb6cc255624d7a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 15:13:06 +0200 Subject: [PATCH 07/70] refactor `guess_language` to streamline logic using `GuestLanguage::from_id_string` --- cli/golem-cli/src/model/app.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 7c61e4b1bd..3bfcdd82f9 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -1731,18 +1731,12 @@ impl<'a> Component<'a> { self.component_name } - // TODO: FCL: cleanup this, and make lang ids reserved for template names + // Guesses the language from the applied language templates, which are named after the + // language id they provide (see `GuestLanguage::from_id_string`). pub fn guess_language(&self) -> Option { - self.applied_layers().iter().find_map(|(id, _)| { - id.template_name() - .and_then(|template_name| match template_name { - "ts" => Some(GuestLanguage::TypeScript), - "rust" => Some(GuestLanguage::Rust), - "scala" => Some(GuestLanguage::Scala), - "moonbit" => Some(GuestLanguage::MoonBit), - _ => None, - }) - }) + self.applied_layers() + .iter() + .find_map(|(id, _)| id.template_name().and_then(GuestLanguage::from_id_string)) } pub fn source(&self) -> &Path { From 4ed590b39b573974a683af9886d8d39ad8a1b4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 17:21:39 +0200 Subject: [PATCH 08/70] make `agent_name` non-optional in `WorkerCreateView` and update related logic --- .../command-output.schema.json | 5 +- .../src/command_handler/worker/mod.rs | 2 +- cli/golem-cli/src/model/cli_output.rs | 4 +- cli/golem-cli/src/model/text/account.rs | 19 ------ cli/golem-cli/src/model/text/plugin.rs | 61 ------------------- cli/golem-cli/src/model/text/worker.rs | 21 ++----- 6 files changed, 12 insertions(+), 100 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 9af354b7ef..993e6c5d28 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -1369,7 +1369,8 @@ "x-golem-command": "agent new", "required": [ "$type", - "componentName" + "componentName", + "agentName" ], "properties": { "$type": { @@ -1379,7 +1380,7 @@ "type": "string" }, "agentName": { - "type": ["string", "null"] + "type": "string" } }, "additionalProperties": false diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index dd36f087ff..27974b5092 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -325,7 +325,7 @@ impl WorkerCommandHandler { logln(""); self.ctx.log_handler().log_output(WorkerCreateView { component_name: agent_name_match.component_name, - agent_name: Some(display_agent_name), + agent_name: display_agent_name, })?; Ok(()) diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index 43f5e82955..98e0ddd9da 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -2923,10 +2923,10 @@ mod tests { fn arb_agent_new_result() -> OutputDocumentStrategy { serialized_output( - (arb_small_string(), proptest::option::of(arb_small_string())).prop_map( + (arb_small_string(), arb_small_string()).prop_map( |(component_name, agent_name)| crate::model::text::worker::WorkerCreateView { component_name: golem_common::model::component::ComponentName(component_name), - agent_name: agent_name.map(crate::model::worker::RawAgentId), + agent_name: crate::model::worker::RawAgentId(agent_name), }, ), ) diff --git a/cli/golem-cli/src/model/text/account.rs b/cli/golem-cli/src/model/text/account.rs index cd112adcb8..23e215d4e1 100644 --- a/cli/golem-cli/src/model/text/account.rs +++ b/cli/golem-cli/src/model/text/account.rs @@ -248,22 +248,3 @@ impl TextOutput for PermissionShareListView { impl StructuredOutput for PermissionShareListView { const KIND: &'static str = "account.permission-share.list"; } - -// TODO: atomic -/* -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct GrantGetView(pub Vec); - -impl TextRender for GrantGetView { - fn log(&self) { - if self.0.is_empty() { - logln("No roles granted") - } else { - logln("Granted roles:"); - for role in &self.0 { - logln(format!(" - {role}")); - } - } - } -} -*/ diff --git a/cli/golem-cli/src/model/text/plugin.rs b/cli/golem-cli/src/model/text/plugin.rs index 9f8f4bd650..cf729fc7a6 100644 --- a/cli/golem-cli/src/model/text/plugin.rs +++ b/cli/golem-cli/src/model/text/plugin.rs @@ -192,64 +192,3 @@ fn plugin_registration_fields(plugin: &PluginRegistrationDto) -> Vec<(String, St fields.build() } - -// TODO: atomic -/*impl MessageWithFields for PluginInstallation { - fn message(&self) -> String { - format!( - "Installed plugin {} version {}", - format_message_highlight(&self.environment_plugin_grant_id), - format_message_highlight(&self.plugin_version), - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("ID", &self.id, format_main_id) - .fmt_field("Plugin name", &self.plugin_version, format_id) - .fmt_field("Plugin version", &self.plugin_version, format_id) - .fmt_field("Priority", &self.priority, format_id); - - for (k, v) in &self.parameters { - fields.fmt_field(k, v, format_id); - } - - fields.build() - } -} - -// TODO: add component name to help with "multi-install" -#[derive(Table)] -struct PluginInstallationTableView { - #[table(title = "Installation ID")] - pub id: String, - #[table(title = "Plugin name")] - pub name: String, - #[table(title = "Plugin version")] - pub version: String, - #[table(title = "Parameters")] - pub parameters: String, -} - -impl From<&PluginRegistrationDto> for PluginInstallationTableView { - fn from(value: &PluginRegistrationDto) -> Self { - Self { - id: value.id.to_string(), - name: value.name.clone(), - version: value.version.clone(), - parameters: value - .parameters - .iter() - .map(|(k, v)| format!("{k}: {v}")) - .join(", "), - } - } -} - -impl TextRender for Vec { - fn log(&self) { - log_table::<_, PluginInstallationTableView>(self.as_slice()) - } -}*/ diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index a7757886e8..1b9dff7f9f 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -52,26 +52,17 @@ use std::fmt::Write; #[serde(rename_all = "camelCase")] pub struct WorkerCreateView { pub component_name: ComponentName, - pub agent_name: Option, + pub agent_name: RawAgentId, } impl Masked for WorkerCreateView {} impl MessageWithFields for WorkerCreateView { fn message(&self) -> String { - if let Some(agent_name) = &self.agent_name { - format!( - "Created new agent {}", - format_message_highlight(&agent_name) - ) - } else { - // TODO: review: do we really want to hide the worker name? it is provided now - // in "worker new" - format!( - "Created new agent with a {}", - format_message_highlight("random generated name") - ) - } + format!( + "Created new agent {}", + format_message_highlight(&self.agent_name) + ) } fn fields(&self) -> Vec<(String, String)> { @@ -79,7 +70,7 @@ impl MessageWithFields for WorkerCreateView { fields .fmt_field("Component name", &self.component_name, format_id) - .fmt_field_option("Agent name", &self.agent_name, format_agent_name); + .fmt_field("Agent name", &self.agent_name, format_agent_name); fields.build() } From c77ec0daa87b9b6a07ba1a8bd481e61172f098b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 19:42:22 +0200 Subject: [PATCH 09/70] use bail instead of unimplemented --- cli/golem-cli/src/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cli/golem-cli/src/main.rs b/cli/golem-cli/src/main.rs index b834acf8f2..bdcd1395c9 100644 --- a/cli/golem-cli/src/main.rs +++ b/cli/golem-cli/src/main.rs @@ -38,12 +38,18 @@ mod hooks { _ctx: Arc, _subcommand: ServerSubcommand, ) -> anyhow::Result<()> { - unimplemented!() + anyhow::bail!( + "This build of golem-cli does not include a bundled local server. \ + Use the 'golem' binary to run 'server' commands." + ) } #[cfg(feature = "server-commands")] async fn run_server() -> anyhow::Result<()> { - unimplemented!() + anyhow::bail!( + "This build of golem-cli does not include a bundled local server. \ + Use the 'golem' binary to start a local server." + ) } #[cfg(feature = "server-commands")] From 1891da6c0beba6f3ea60a8ee388aa719c9bb1db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 19:42:40 +0200 Subject: [PATCH 10/70] improve error context in `parse_cursor` with detailed validation for format and values --- cli/golem-cli/src/args.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/cli/golem-cli/src/args.rs b/cli/golem-cli/src/args.rs index 8f8cc0f0ee..337580cfb7 100644 --- a/cli/golem-cli/src/args.rs +++ b/cli/golem-cli/src/args.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::log::LogColorize; -use anyhow::{anyhow, bail}; +use anyhow::{Context, anyhow, bail}; use chrono::{DateTime, Utc}; use golem_client::model::ScanCursor; use golem_common::model::agent_secret::AgentSecretPath; @@ -130,17 +130,20 @@ fn push_agent_config_path_segment(keys: &mut Vec, buf: &mut String) -> a Ok(()) } -// TODO: better error context and messages pub fn parse_cursor(cursor: &str) -> anyhow::Result { let parts = cursor.split('/').collect::>(); if parts.len() != 2 { - bail!("Invalid cursor format: {}", cursor); + bail!("Invalid cursor {cursor:?}, expected the format / (e.g. 0/123)"); } Ok(ScanCursor { - layer: parts[0].parse()?, - cursor: parts[1].parse()?, + layer: parts[0].parse().with_context(|| { + format!("Invalid scan cursor layer {:?}, expected a non-negative integer", parts[0]) + })?, + cursor: parts[1].parse().with_context(|| { + format!("Invalid scan cursor position {:?}, expected a non-negative integer", parts[1]) + })?, }) } From ecca8be01a209dc8d5ef316cec45a137965da6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 20:27:16 +0200 Subject: [PATCH 11/70] help cleanups --- .../src/command_handler/component/mod.rs | 1 - .../src/command_handler/partial_match.rs | 37 ++++++++++++------- .../src/command_handler/profile/config.rs | 10 ++++- cli/golem-cli/src/model/text/help.rs | 29 ++++++++++++++- 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 6659c78861..c39e38c459 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -667,7 +667,6 @@ impl ComponentCommandHandler { "Component {} not found, and not part of the current application", component_name.0.log_color_highlight() )); - // TODO: fuzzy match from service to list components? let app_ctx = self.ctx.app_context_lock().await; if let Some(app_ctx) = app_ctx.opt()? { diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index 5124876988..7c6b04268e 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -27,7 +27,8 @@ use crate::model::environment::EnvironmentResolveMode; use crate::model::format::Format; use crate::model::text::fmt::{DecoratedIndent, log_text_view}; use crate::model::text::help::{ - AgentNameHelp, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, EnvironmentNameHelp, + AgentNameHelp, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, + AvailableProfileNamesHelp, EnvironmentNameHelp, }; use colored::Colorize; use indoc::indoc; @@ -112,7 +113,22 @@ impl ErrorHandler { Ok(()) } GolemCliCommandPartialMatch::AgentHelp => { - // TODO: show agents + self.ctx.silence_app_context_init().await; + + if let Ok(environment) = self + .ctx + .environment_handler() + .resolve_environment(EnvironmentResolveMode::Any) + .await + && let Ok(agent_types) = + self.ctx.app_handler().list_agent_types(&environment).await + { + logln(""); + log_text_view(&AvailableAgentConstructorsHelp::for_deployed_agent_types( + &agent_types, + )); + } + Ok(()) } GolemCliCommandPartialMatch::AgentInvokeMissingFunctionName { agent_name } => { @@ -209,8 +225,10 @@ impl ErrorHandler { Ok(()) } GolemCliCommandPartialMatch::ProfileSwitchMissingProfileName => { - // TODO: atomic: show available profiles - + logln(""); + log_text_view(&AvailableProfileNamesHelp::from_config_dir( + self.ctx.config_dir(), + )?); Ok(()) } } @@ -352,16 +370,7 @@ impl ErrorHandler { "Profile '{}' not found!", profile_name.0.log_color_highlight() )); - - logln( - "Available profile names:" - .log_color_help_group() - .to_string(), - ); - for environment_name in available_profile_names { - logln(format!("- {}", environment_name.0)); - } - + log_text_view(&AvailableProfileNamesHelp(available_profile_names.clone())); Ok(()) } } diff --git a/cli/golem-cli/src/command_handler/profile/config.rs b/cli/golem-cli/src/command_handler/profile/config.rs index 23ee4013a1..874938c169 100644 --- a/cli/golem-cli/src/command_handler/profile/config.rs +++ b/cli/golem-cli/src/command_handler/profile/config.rs @@ -17,9 +17,12 @@ use crate::command_handler::Handlers; use crate::config::{Config, ProfileName}; use crate::context::Context; use crate::error::NonSuccessfulExit; -use crate::log::log_action; use crate::log::log_error; +use crate::log::logln; +use crate::log::log_action; use crate::model::format::Format; +use crate::model::text::fmt::log_text_view; +use crate::model::text::help::AvailableProfileNamesHelp; use crate::model::text::profile::ProfileConfigSetFormatResult; use anyhow::bail; use std::sync::Arc; @@ -72,7 +75,10 @@ impl ProfileConfigCommandHandler { } None => { log_error(format!("Profile {profile_name} not found")); - // TODO: show available profiles + logln(""); + log_text_view(&AvailableProfileNamesHelp::from_config_dir( + self.ctx.config_dir(), + )?); bail!(NonSuccessfulExit); } } diff --git a/cli/golem-cli/src/model/text/help.rs b/cli/golem-cli/src/model/text/help.rs index d4857c56f3..f04f312c23 100644 --- a/cli/golem-cli/src/model/text/help.rs +++ b/cli/golem-cli/src/model/text/help.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::agent_id_display::{SourceLanguage, render_type_for_language}; +use crate::config::{Config, ProfileName}; use crate::log::{LogColorize, LogIndent, logln}; use crate::model::component::show_exported_agent_constructors; use crate::model::masking::Masked; @@ -28,7 +29,7 @@ use golem_common::model::component::ComponentName; use golem_common::schema::agent::AgentTypeSchema; use golem_common::schema::{SchemaGraph, SchemaType}; use indoc::indoc; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; pub struct AgentNameHelp; @@ -186,6 +187,32 @@ impl TextOutput for AvailableComponentNamesHelp { } } +pub struct AvailableProfileNamesHelp(pub Vec); + +impl AvailableProfileNamesHelp { + pub fn from_config_dir(config_dir: &Path) -> anyhow::Result { + Ok(Self( + Config::from_dir(config_dir)?.profiles.into_keys().collect(), + )) + } +} + +impl TextOutput for AvailableProfileNamesHelp { + fn log(&self) { + let mut names = self.0.iter().map(|name| name.0.as_str()).collect::>(); + names.sort(); + + logln( + "Available profile names:" + .log_color_help_group() + .to_string(), + ); + for name in names { + logln(format!("- {name}")); + } + } +} + pub struct AvailableFunctionNamesHelp { pub component_name: String, pub agent_name: Option, From 96ebfdf7a8c082966952ac8726afc8707f558020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 21 Jul 2026 21:13:58 +0200 Subject: [PATCH 12/70] cleanup WAVE :wave: comments --- cli/golem-cli/src/command.rs | 1 - cli/golem-cli/tests/app/agents.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 73f880ce19..b1c7be9617 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -1307,7 +1307,6 @@ pub mod worker { #[arg(short, long, value_parser = parse_agent_config, verbatim_doc_comment)] config: Vec, }, - // TODO: json args /// Invoke (or enqueue invocation for) agent #[command(after_help = crate::command_examples::AGENT_INVOKE)] Invoke { diff --git a/cli/golem-cli/tests/app/agents.rs b/cli/golem-cli/tests/app/agents.rs index fd2226dd33..58c7b64349 100644 --- a/cli/golem-cli/tests/app/agents.rs +++ b/cli/golem-cli/tests/app/agents.rs @@ -621,7 +621,7 @@ async fn test_rust_code_first_with_rpc_and_all_types() { run_and_assert(&ctx, "fun_enum_with_only_literals", &["A"]).await; - // TODO: Re-enable once CLI WAVE argument parsing supports multimodal/unstructured types + // TODO: Re-enable once the CLI's argument parsing supports multimodal/unstructured types // run_and_assert( // &ctx, // "fun_multi_modal", @@ -1707,7 +1707,7 @@ async fn test_ts_code_first_with_rpc_and_all_types() { // Union that has only literals run_and_assert(&ctx, "funUnionWithOnlyLiterals", &[r#""foo""#]).await; - // TODO: Re-enable once CLI WAVE argument parsing supports multimodal/unstructured types + // TODO: Re-enable once the CLI's argument parsing supports multimodal/unstructured types // // Unstructured text type // run_and_assert(&ctx, "funUnstructuredText", &["url(\"foo\")"]).await; // From e7da5e170bc7f21c38b9be5cfbfecead1ead58f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Mon, 27 Jul 2026 12:15:38 +0200 Subject: [PATCH 13/70] add structural agent ID formatting for terminal display and integrate with table rendering system --- Cargo.lock | 36 +- Cargo.toml | 4 +- .../src/agent_id_display/highlight.rs | 369 +++++++++++++++++ cli/golem-cli/src/agent_id_display/mod.rs | 16 + .../src/command_handler/interactive.rs | 11 +- .../src/command_handler/worker/mod.rs | 40 +- cli/golem-cli/src/log.rs | 12 +- cli/golem-cli/src/model/text/fmt.rs | 381 ++++++++++++++++-- cli/golem-cli/src/model/text/worker.rs | 215 +++++++--- 9 files changed, 950 insertions(+), 134 deletions(-) create mode 100644 cli/golem-cli/src/agent_id_display/highlight.rs diff --git a/Cargo.lock b/Cargo.lock index ae4227013f..b8dac87329 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,25 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "ansi-str" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060de1453b69f46304b28274f382132f4e72c55637cf362920926a70d090890d" +dependencies = [ + "ansitok", +] + +[[package]] +name = "ansitok" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a8acea8c2f1c60f0a92a8cd26bf96ca97db56f10bbcab238bbe0cceba659ee" +dependencies = [ + "nom 7.1.3", + "vte", +] + [[package]] name = "anstream" version = "0.6.15" @@ -1681,6 +1700,8 @@ version = "7.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47" dependencies = [ + "ansi-str", + "console 0.16.4", "crossterm 0.29.0", "unicode-segmentation", "unicode-width 0.2.2", @@ -1745,6 +1766,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + [[package]] name = "console-api" version = "0.9.0" @@ -9257,7 +9290,7 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b441962c817e33508847a22bd82f03a30cff43642dc2fae8b050566121eb9a" dependencies = [ - "console", + "console 0.15.11", "similar", ] @@ -11143,6 +11176,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" dependencies = [ + "arrayvec", "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index a0b732642d..297ddea744 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,7 +86,9 @@ clap-verbosity-flag = { version = "3.0.4", features = ["tracing"] } clap_complete = "4.5.66" colored = "3.0.0" colored-diff = "0.2.3" -comfy-table = "7.2.2" +# `custom_styling` makes comfy-table measure cell content with ANSI escapes +# stripped, which is required for cells that carry their own coloring. +comfy-table = { version = "7.2.2", features = ["custom_styling"] } combine = "4.6.7" conditional-trait-gen = "0.4.1" console-subscriber = "0.5.0" diff --git a/cli/golem-cli/src/agent_id_display/highlight.rs b/cli/golem-cli/src/agent_id_display/highlight.rs new file mode 100644 index 0000000000..164592b813 --- /dev/null +++ b/cli/golem-cli/src/agent_id_display/highlight.rs @@ -0,0 +1,369 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Terminal-friendly formatting of already-rendered agent ids: breaks long ids +//! across lines with structural indentation and optionally colors them. +//! +//! Done as a post-pass over the rendered text, re-tokenized with the shared +//! [`Lexer`]. Working on the rendered string (not the parsed value) keeps this +//! independent of the per-language renderers and of whatever is serialized; +//! since the lexer treats string literals as single tokens, brackets and commas +//! inside string values never affect the layout. + +use super::lexer::{Lexer, Token}; +use colored::Colorize; + +const INDENT: &str = " "; +const MIN_WIDTH: usize = 20; + +/// Lexical class of a token, used for both layout and coloring. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Kind { + Open, + Close, + Comma, + Punct, + Str, + Num, + Lit, + Ident, +} + +/// A token as a byte range into the original rendered string. +struct Span { + kind: Kind, + start: usize, + end: usize, +} + +/// Formats an already-rendered agent id for terminal display. +/// +/// `width` is what the caller can give the id (only the call site knows: a field +/// value is indented, a table cell bounded by its column); `None` disables line +/// breaking, leaving only coloring. Groups that fit stay inline; a single token +/// wider than `width` (a long string, a uuid) still overflows. Returned +/// unchanged if it cannot be tokenized. +pub fn format_agent_id_for_terminal( + rendered: &str, + colorize: bool, + width: Option, +) -> String { + let Some(spans) = tokenize(rendered) else { + return rendered.to_string(); + }; + if spans.is_empty() { + return rendered.to_string(); + } + + // Below the minimum every group explodes onto its own line, worse than + // overflowing a very narrow target. + let width = width + .map(|width| width.max(MIN_WIDTH)) + .unwrap_or(usize::MAX); + + let close_of = match_brackets(&spans); + layout(rendered, &spans, &close_of, colorize, width) +} + +fn kind_of(token: &Token) -> Kind { + match token { + Token::LBrace | Token::LBrack | Token::LParen => Kind::Open, + Token::RBrace | Token::RBrack | Token::RParen => Kind::Close, + Token::Comma => Kind::Comma, + Token::Colon | Token::DoubleColon | Token::Dot | Token::Eq | Token::Star | Token::At => { + Kind::Punct + } + Token::StringLit(_) | Token::CharLit(_) => Kind::Str, + Token::IntLit(_) | Token::UintLit(_) | Token::FloatLit(_) => Kind::Num, + Token::BoolLit(_) | Token::Null | Token::Undefined => Kind::Lit, + Token::Ident(_) => Kind::Ident, + Token::Eof => Kind::Punct, + } +} + +/// Tokenizes the rendered id, recovering any character the lexer does not model +/// (map arrows `=>`, `` fallbacks, the hyphens of a phantom uuid) as a +/// punctuation span instead of giving up — bailing out would drop both the line +/// breaking and the coloring for the whole id. `None` only if it cannot advance. +fn tokenize(input: &str) -> Option> { + let mut spans: Vec = Vec::new(); + let mut base = 0usize; + + loop { + let rest = &input[base..]; + let mut lexer = Lexer::new(rest); + + // Restarts after each recovered character. + loop { + match lexer.next_token() { + Ok((Token::Eof, _, _)) => return Some(spans), + Ok((token, start, end)) => spans.push(Span { + kind: kind_of(&token), + start: base + start, + end: base + end, + }), + Err(err) => { + let at = base + err.position; + let ch = input[at..].chars().next()?; + + // Merge `=` + `>` into a single `=>` punctuation span. + let merged = ch == '>' + && spans.last().is_some_and(|last| { + last.end == at && &input[last.start..last.end] == "=" + }); + if merged { + spans.last_mut().expect("checked above").end = at + 1; + } else { + spans.push(Span { + kind: Kind::Punct, + start: at, + end: at + ch.len_utf8(), + }); + } + + base = at + ch.len_utf8(); + break; + } + } + } + } +} + +/// Maps each opening bracket span index to its matching closing span index. +fn match_brackets(spans: &[Span]) -> Vec> { + let mut close_of = vec![None; spans.len()]; + let mut open_stack = Vec::new(); + + for (index, span) in spans.iter().enumerate() { + match span.kind { + Kind::Open => open_stack.push(index), + Kind::Close => { + if let Some(open) = open_stack.pop() { + close_of[open] = Some(index); + } + } + _ => {} + } + } + + close_of +} + +fn layout( + input: &str, + spans: &[Span], + close_of: &[Option], + colorize: bool, + width: usize, +) -> String { + let mut out = String::new(); + let mut col = 0usize; + // One entry per open group: whether that group is broken across lines. + let mut expanded: Vec = Vec::new(); + let mut prev_end: Option = None; + let mut at_line_start = true; + + for (index, span) in spans.iter().enumerate() { + let closing_expanded = + span.kind == Kind::Close && expanded.last().copied().unwrap_or(false); + + // Keep the renderer's own spacing whenever we stay on the same line. + if !at_line_start + && !closing_expanded + && let Some(end) = prev_end + { + let gap = &input[end..span.start]; + out.push_str(gap); + col += gap.chars().count(); + } + at_line_start = false; + + let text = &input[span.start..span.end]; + + match span.kind { + Kind::Open => { + let fits = close_of[index] + .map(|close| col + input[span.start..spans[close].end].chars().count() <= width) + .unwrap_or(true); + push_token(&mut out, &mut col, text, span.kind, colorize); + expanded.push(!fits); + + if !fits { + newline_indent(&mut out, &mut col, indent_level(&expanded)); + at_line_start = true; + } + } + Kind::Close => { + let was_expanded = expanded.pop().unwrap_or(false); + if was_expanded { + newline_indent(&mut out, &mut col, indent_level(&expanded)); + } + push_token(&mut out, &mut col, text, span.kind, colorize); + } + Kind::Comma => { + push_token(&mut out, &mut col, text, span.kind, colorize); + if expanded.last().copied().unwrap_or(false) { + newline_indent(&mut out, &mut col, indent_level(&expanded)); + at_line_start = true; + } + } + _ => push_token(&mut out, &mut col, text, span.kind, colorize), + } + + prev_end = Some(span.end); + } + + out +} + +fn indent_level(expanded: &[bool]) -> usize { + expanded.iter().filter(|is_expanded| **is_expanded).count() +} + +fn newline_indent(out: &mut String, col: &mut usize, level: usize) { + out.push('\n'); + out.push_str(&INDENT.repeat(level)); + *col = level * INDENT.chars().count(); +} + +fn push_token(out: &mut String, col: &mut usize, text: &str, kind: Kind, colorize: bool) { + if colorize { + let colored = match kind { + Kind::Str => text.green().to_string(), + Kind::Num => text.cyan().to_string(), + Kind::Lit => text.yellow().to_string(), + Kind::Open | Kind::Close | Kind::Comma | Kind::Punct => text.dimmed().to_string(), + Kind::Ident => text.to_string(), + }; + out.push_str(&colored); + } else { + out.push_str(text); + } + *col += text.chars().count(); +} + +#[cfg(test)] +mod tests { + use super::format_agent_id_for_terminal; + use test_r::test; + + #[test] + fn short_id_stays_on_one_line() { + let id = r#"Counter("main")"#; + assert_eq!(format_agent_id_for_terminal(id, false, Some(80)), id); + } + + #[test] + fn plain_output_is_unchanged_when_it_fits() { + let id = r#"Cart { user: "ann", items: [1, 2, 3] }"#; + assert_eq!(format_agent_id_for_terminal(id, false, Some(80)), id); + } + + #[test] + fn long_id_is_broken_with_indentation() { + let id = r#"ShoppingCart(user: "a-fairly-long-user-identifier", items: ["one", "two", "three"])"#; + let formatted = format_agent_id_for_terminal(id, false, Some(40)); + + // Fields go one per line, indented; the closing bracket returns to the + // opener's level. The short inner list still fits, so it stays inline. + assert_eq!( + formatted, + concat!( + "ShoppingCart(\n", + " user: \"a-fairly-long-user-identifier\",\n", + " items: [\"one\", \"two\", \"three\"]\n", + ")" + ) + ); + } + + #[test] + fn groups_that_still_fit_stay_inline() { + let id = r#"Outer(first: "a-quite-long-value-here-indeed", inner: [1, 2])"#; + let formatted = format_agent_id_for_terminal(id, false, Some(40)); + + assert!(formatted.contains('\n')); + // The short inner list is not exploded. + assert!( + formatted.contains("[1, 2]"), + "inner list broken:\n{formatted}" + ); + } + + #[test] + fn braces_and_commas_inside_strings_do_not_affect_layout() { + let id = r#"Weird(text: "a, b {c} [d]", n: 1)"#; + let formatted = format_agent_id_for_terminal(id, false, Some(80)); + + assert_eq!(formatted, id); + } + + #[test] + fn untokenizable_input_is_returned_unchanged() { + let id = "definitely ~not~ an agent id"; + assert_eq!(format_agent_id_for_terminal(id, false, Some(80)), id); + } + + /// A phantom id ends in `[uuid]`, whose hyphens the lexer does not model. + /// Recovering from them matters: bailing out would drop the layout and the + /// coloring for the entire id, not just the suffix. + #[test] + fn phantom_uuid_suffix_is_still_formatted() { + let id = r#"Probe("health-check-probe-with-a-longer-label", 3)[81db7b03-f3ff-456b-af38-a0fa0ce795b3]"#; + let formatted = format_agent_id_for_terminal(id, false, Some(40)); + + // The arguments break, the uuid is left intact on the closing line. + assert_eq!( + formatted, + concat!( + "Probe(\n", + " \"health-check-probe-with-a-longer-label\",\n", + " 3\n", + ")[81db7b03-f3ff-456b-af38-a0fa0ce795b3]" + ) + ); + } + + /// A token wider than the target cannot be broken, but moving it onto its + /// own indented line still recovers the width taken by the prefix. + #[test] + fn oversized_token_is_moved_to_its_own_line() { + let id = r#"Probe("a-single-argument-that-is-really-quite-long")"#; + let formatted = format_agent_id_for_terminal(id, false, Some(30)); + + assert_eq!( + formatted, + concat!( + "Probe(\n", + " \"a-single-argument-that-is-really-quite-long\"\n", + ")" + ) + ); + } + + #[test] + fn no_width_never_breaks_lines() { + let id = r#"ShoppingCart(user: "a-fairly-long-user-identifier", items: ["one", "two", "three"])"#; + + assert_eq!(format_agent_id_for_terminal(id, false, None), id); + } + + #[test] + fn map_arrows_are_tolerated() { + let id = r#"Lookup({ "a" => 1, "b" => 2 })"#; + // Must not panic and must preserve the arrows. + let formatted = format_agent_id_for_terminal(id, false, Some(80)); + assert!(formatted.contains("=>"), "arrows lost: {formatted}"); + } +} diff --git a/cli/golem-cli/src/agent_id_display/mod.rs b/cli/golem-cli/src/agent_id_display/mod.rs index 08524b6f02..46e0a5b739 100644 --- a/cli/golem-cli/src/agent_id_display/mod.rs +++ b/cli/golem-cli/src/agent_id_display/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod highlight; mod lexer; mod parse_common; mod parse_moonbit; @@ -36,6 +37,7 @@ use golem_common::schema::graph::{SchemaGraph, TypedSchemaValue, reachable_defs} use golem_common::schema::schema_type::SchemaType; use golem_common::schema::schema_value::SchemaValue; +pub use highlight::format_agent_id_for_terminal; pub use parse_common::ParseError; /// Represents the source language of an agent component, used to select @@ -135,6 +137,20 @@ pub fn render_schema_value( } } +/// Renders an agent ID in its source-language notation when a parsed form and a +/// known source language are both available, falling back to the raw canonical +/// id otherwise. +pub fn render_agent_id_or_raw( + parsed: Option<&ParsedAgentId>, + source_language: &SourceLanguage, + raw: &str, +) -> String { + match parsed { + Some(parsed) if source_language.is_known() => render_agent_id(parsed, source_language), + _ => raw.to_string(), + } +} + /// Render a full agent ID string in the form `TypeName(params)[phantom]`. /// /// The parameters are rendered using [`render_schema_value`] over each diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index 92045e631f..11a9943453 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -290,12 +290,11 @@ impl InteractiveHandler { source_language: &crate::agent_id_display::SourceLanguage, target_revision: ComponentRevision, ) -> anyhow::Result { - let rendered_agent_name = match parsed_agent_id { - Some(parsed) if source_language.is_known() => { - crate::agent_id_display::render_agent_id(parsed, source_language) - } - _ => agent_name.0.clone(), - }; + let rendered_agent_name = crate::agent_id_display::render_agent_id_or_raw( + parsed_agent_id, + source_language, + &agent_name.0, + ); self.confirm( true, format!("Agent {}/{} will be updated to the current component revision: {}. Do you want to continue?", diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 99946128f5..44e2187620 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -314,13 +314,12 @@ impl WorkerCommandHandler { ) .await?; - let display_agent_name: RawAgentId = match &agent_name_match.parsed_agent_id { - Some(parsed) if agent_name_match.source_language.is_known() => { - crate::agent_id_display::render_agent_id(parsed, &agent_name_match.source_language) - .into() - } - _ => agent_name, - }; + let display_agent_name: RawAgentId = crate::agent_id_display::render_agent_id_or_raw( + agent_name_match.parsed_agent_id.as_ref(), + &agent_name_match.source_language, + &agent_name.0, + ) + .into(); logln(""); self.ctx.log_handler().log_output(WorkerCreateView { @@ -1156,13 +1155,13 @@ impl WorkerCommandHandler { .with_defaults(defaults) .with_secret_config_paths(secret_config_paths); - if source_language.is_known() - && let Ok(parsed) = - ParsedAgentId::parse(&raw_agent_name, &worker_component.metadata) - { - agent_view.agent_name = - crate::agent_id_display::render_agent_id(&parsed, &source_language).into(); - } + let parsed = ParsedAgentId::parse(&raw_agent_name, &worker_component.metadata).ok(); + agent_view.agent_name = crate::agent_id_display::render_agent_id_or_raw( + parsed.as_ref(), + &source_language, + &raw_agent_name, + ) + .into(); view.agents .push(agent_view.with_source_language(source_language)); @@ -1375,13 +1374,12 @@ impl WorkerCommandHandler { .with_defaults(defaults) .with_secret_config_paths(secret_config_paths) .with_source_language(agent_name_match.source_language.clone()); - if let Some(parsed) = &agent_name_match.parsed_agent_id - && agent_name_match.source_language.is_known() - { - metadata_view.agent_name = - crate::agent_id_display::render_agent_id(parsed, &agent_name_match.source_language) - .into(); - } + metadata_view.agent_name = crate::agent_id_display::render_agent_id_or_raw( + agent_name_match.parsed_agent_id.as_ref(), + &agent_name_match.source_language, + &metadata_view.agent_name.0, + ) + .into(); self.ctx .log_handler() diff --git a/cli/golem-cli/src/log.rs b/cli/golem-cli/src/log.rs index f92a7b979e..6bd79f126a 100644 --- a/cli/golem-cli/src/log.rs +++ b/cli/golem-cli/src/log.rs @@ -28,7 +28,13 @@ use tracing::debug; static LOG_STATE: LazyLock> = LazyLock::new(RwLock::default); static LOG_STATE_BUFFER: LazyLock>> = LazyLock::new(RwLock::default); static TERMINAL_WIDTH: OnceLock> = OnceLock::new(); -static WRAP_PADDING: usize = 2; +/// Columns kept free at the right edge when wrapping logged text. Callers that +/// pre-format multi-line output must reserve it too, else their lines re-wrap. +pub static WRAP_PADDING: usize = 2; + +/// One level of log indentation. Callers that pre-format multi-line output reuse +/// it to reproduce the same geometry (see `text::fmt::field_value_width`). +pub const INDENT: &str = " "; /// Returns the terminal width as `Some(width)` or `None` if not detectable. /// Cached via `OnceLock` — read once at startup for use in `LogState` text-wrapping. @@ -93,10 +99,10 @@ impl LogState { } fn regen_indent_prefix(&mut self) { - self.calculated_indent = String::with_capacity(self.indents.len() * 2); + self.calculated_indent = String::with_capacity(self.indents.len() * INDENT.len()); for indent in &self.indents { self.calculated_indent - .push_str(indent.as_ref().map(|s| s.as_str()).unwrap_or(" ")) + .push_str(indent.as_ref().map(|s| s.as_str()).unwrap_or(INDENT)) } self.max_width = terminal_width_opt().map(|w| w - WRAP_PADDING - self.calculated_indent.len()); diff --git a/cli/golem-cli/src/model/text/fmt.rs b/cli/golem-cli/src/model/text/fmt.rs index 15288cdf93..07398bc4a3 100644 --- a/cli/golem-cli/src/model/text/fmt.rs +++ b/cli/golem-cli/src/model/text/fmt.rs @@ -16,7 +16,9 @@ use crate::fuzzy::Match; pub use crate::log::log_table; pub use crate::log::logln; pub use crate::log::terminal_width; -use crate::log::{LogColorize, LogIndent, current_indent_width, log_warn_action}; +use crate::log::{ + INDENT, LogColorize, LogIndent, WRAP_PADDING, current_indent_width, log_warn_action, +}; use crate::model::app::ComponentLayerId; use crate::model::format::Format; use crate::model::masking::{Masked, MaskingConfig}; @@ -24,7 +26,9 @@ use anyhow::anyhow; use colored::Colorize; use colored::control::SHOULD_COLORIZE; pub use comfy_table::Table as ComfyTable; -use comfy_table::{Cell, CellAlignment, ColumnConstraint, ContentArrangement, Width}; +use comfy_table::{ + Cell, CellAlignment, Color as ComfyColor, ColumnConstraint, ContentArrangement, Width, +}; use golem_common::model::AgentStatus; use golem_common::model::component::{InitialAgentFile, InstalledPlugin}; use golem_common::model::worker::TypedAgentConfigEntry; @@ -123,6 +127,22 @@ impl TextOutput for T { } } +/// Columns a multi-line field value can use. `fields()` builds values before the +/// indents they print inside exist, so all are subtracted here: the ambient +/// indent, the view's own indent, the per-line indent of a multi-line value, and +/// [`WRAP_PADDING`] (reaching into which gets the value wrapped again). +pub fn field_value_width() -> usize { + let view_indent = match T::indent_mode() { + MessageWithFieldsIndentMode::None => 0, + MessageWithFieldsIndentMode::IdentFields | MessageWithFieldsIndentMode::NestedIdentAll => { + INDENT.len() + } + }; + + (terminal_width() as usize) + .saturating_sub(current_indent_width() + view_indent + INDENT.len() + WRAP_PADDING) +} + fn log_message_with_fields(message: String, fields: Vec<(String, String)>) { let _ident = match T::indent_mode() { MessageWithFieldsIndentMode::None => None, @@ -154,7 +174,7 @@ fn log_message_with_fields(message: String, fields: Vec<(S } else { logln(format!("{}:", T::format_field_name(name))); for line in lines { - logln(format!(" {line}")) + logln(format!("{INDENT}{line}")) } } } @@ -533,7 +553,7 @@ impl Column { &self.title } - fn total_width_for_content_width(content_width: u16) -> u16 { + pub fn total_width_for_content_width(content_width: u16) -> u16 { content_width.saturating_add(2) } } @@ -557,23 +577,12 @@ pub enum TablePreset { /// The terminal width is automatically reduced by the current log indent width so that /// tables render correctly when called inside an indented context. pub fn new_table(preset: TablePreset, headers: Vec) -> ComfyTable { - use comfy_table::presets::{ASCII_FULL, ASCII_FULL_CONDENSED, UTF8_FULL, UTF8_FULL_CONDENSED}; let colorize = SHOULD_COLORIZE.should_colorize(); let indent_width = current_indent_width(); let term_width = (terminal_width() as usize).saturating_sub(indent_width) as u16; let mut table = ComfyTable::new(); table - .load_preset(if colorize { - match preset { - TablePreset::Full => UTF8_FULL, - TablePreset::FullCondensed => UTF8_FULL_CONDENSED, - } - } else { - match preset { - TablePreset::Full => ASCII_FULL, - TablePreset::FullCondensed => ASCII_FULL_CONDENSED, - } - }) + .load_preset(preset_str(preset, colorize)) .set_content_arrangement(ContentArrangement::Dynamic) .set_width(term_width) .set_header( @@ -584,29 +593,7 @@ pub fn new_table(preset: TablePreset, headers: Vec) -> ComfyTable { ); for (i, col) in headers.iter().enumerate() { let column = table.column_mut(i).unwrap(); - match col.width { - ColumnWidth::Auto => {} - ColumnWidth::Content => { - column.set_constraint(ColumnConstraint::ContentWidth); - } - ColumnWidth::Exact(width) => { - column.set_constraint(ColumnConstraint::Absolute(Width::Fixed( - Column::total_width_for_content_width(width), - ))); - } - ColumnWidth::Min(min_width) => { - column.set_constraint(ColumnConstraint::LowerBoundary(Width::Fixed(min_width))); - } - ColumnWidth::Max(max_width) => { - column.set_constraint(ColumnConstraint::UpperBoundary(Width::Fixed(max_width))); - } - ColumnWidth::Range { min, max } => { - column.set_constraint(ColumnConstraint::Boundaries { - lower: Width::Fixed(Column::total_width_for_content_width(min)), - upper: Width::Fixed(Column::total_width_for_content_width(max)), - }); - } - } + apply_column_width(column, col.width); if col.right_aligned { column.set_cell_alignment(CellAlignment::Right); } @@ -614,6 +601,32 @@ pub fn new_table(preset: TablePreset, headers: Vec) -> ComfyTable { table } +fn apply_column_width(column: &mut comfy_table::Column, width: ColumnWidth) { + match width { + ColumnWidth::Auto => {} + ColumnWidth::Content => { + column.set_constraint(ColumnConstraint::ContentWidth); + } + ColumnWidth::Exact(width) => { + column.set_constraint(ColumnConstraint::Absolute(Width::Fixed( + Column::total_width_for_content_width(width), + ))); + } + ColumnWidth::Min(min_width) => { + column.set_constraint(ColumnConstraint::LowerBoundary(Width::Fixed(min_width))); + } + ColumnWidth::Max(max_width) => { + column.set_constraint(ColumnConstraint::UpperBoundary(Width::Fixed(max_width))); + } + ColumnWidth::Range { min, max } => { + column.set_constraint(ColumnConstraint::Boundaries { + lower: Width::Fixed(Column::total_width_for_content_width(min)), + upper: Width::Fixed(Column::total_width_for_content_width(max)), + }); + } + } +} + pub fn new_table_full(headers: Vec) -> ComfyTable { new_table(TablePreset::Full, headers) } @@ -622,6 +635,218 @@ pub fn new_table_full_condensed(headers: Vec) -> ComfyTable { new_table(TablePreset::FullCondensed, headers) } +/// Space a comfy-table cell reserves around its content: one column on each +/// side. Kept in sync with [`Column::total_width_for_content_width`]. +const CELL_PADDING: usize = 2; + +/// One cell of a [`self_formatting_table`] row. The cell in the flex column +/// ([`FlexColumn::index`]) carries the raw value and is reformatted to the +/// budgeted width; every other cell is rendered from its text as-is. +pub struct TableCell { + text: String, + align_right: bool, + color: Option, +} + +impl TableCell { + /// A cell holding the given text. + pub fn new(text: impl Into) -> Self { + Self { + text: text.into(), + align_right: false, + color: None, + } + } + + pub fn right(mut self) -> Self { + self.align_right = true; + self + } + + pub fn color(mut self, color: ComfyColor) -> Self { + self.color = Some(color); + self + } + + fn content_width(&self) -> usize { + self.text.chars().count() + } +} + +/// The self-formatting column of a [`self_formatting_table`]. +pub struct FlexColumn<'a> { + /// Index of the column among `headers`. + pub index: usize, + /// Smallest content width worth formatting to; below it the formatter is + /// called with `None` and the value rendered as-is for the engine to wrap. + pub min_width: usize, + /// Formats a raw cell value to the budgeted content width (`None` = as-is). + pub format: &'a dyn Fn(&str, Option) -> String, +} + +/// Inputs for [`self_formatting_table`]. +pub struct SelfFormattingTableSpec<'a> { + pub preset: TablePreset, + pub term_width: u16, + pub full_width: bool, + pub headers: Vec, + pub flex: FlexColumn<'a>, + pub rows: Vec>, +} + +/// Builds a table where one column's cells are formatted to a width the builder +/// computes, instead of one the engine derives from the content. +/// +/// The normal flow is content → width: cells go in, comfy-table sizes columns +/// after. That breaks for a cell already laid out to a width (a structurally +/// broken agent id) — the engine re-wraps it mid-token. So the flow is inverted: +/// measure the fixed columns, budget the leftover to the flex column, format its +/// cells to exactly that, and pin the column so the engine cannot resize it. The +/// flex column is never wider than its longest cell needs, so it wraps only when +/// the terminal cannot fit it and otherwise uses the full width. +/// +/// A non-flex [`ColumnWidth::Range`] column is *soft*: capped at its upper bound, +/// with the lower bound only the budget's shrink-to floor (no hard lower +/// constraint, so the engine frees space for the pinned flex column). Other +/// width kinds keep their usual meaning. +pub fn self_formatting_table(spec: SelfFormattingTableSpec) -> ComfyTable { + let flex_width = flex_content_width(&spec); + + let colorize = SHOULD_COLORIZE.should_colorize(); + let mut table = ComfyTable::new(); + table + .load_preset(preset_str(spec.preset, colorize)) + .set_content_arrangement(if spec.full_width { + ContentArrangement::DynamicFullWidth + } else { + ContentArrangement::Dynamic + }) + .set_width(spec.term_width) + .set_header( + spec.headers + .iter() + .map(|header| Cell::new(&header.title)) + .collect::>(), + ); + + for (index, header) in spec.headers.iter().enumerate() { + let column = table.column_mut(index).unwrap(); + if index == spec.flex.index { + match flex_width { + // Pin it so the engine cannot shrink it and re-wrap the cells. + Some(width) => { + column.set_constraint(ColumnConstraint::Absolute(Width::Fixed( + Column::total_width_for_content_width(width as u16), + ))); + } + None => apply_column_width(column, header.width), + } + } else if let ColumnWidth::Range { max, .. } = header.width { + // Soft column: upper bound only, no hard lower one, so the engine + // shrinks it freely to fit the pinned flex column. + column.set_constraint(ColumnConstraint::UpperBoundary(Width::Fixed( + Column::total_width_for_content_width(max), + ))); + } else { + apply_column_width(column, header.width); + } + if header.right_aligned { + column.set_cell_alignment(CellAlignment::Right); + } + } + + for row in &spec.rows { + table.add_row( + row.iter() + .enumerate() + .map(|(index, cell)| { + // `flex.index` is the single source of truth for which cell is + // reformatted, shared with the constraint above. + let text = if index == spec.flex.index { + (spec.flex.format)(&cell.text, flex_width) + } else { + cell.text.clone() + }; + let mut comfy = Cell::new(text); + if cell.align_right { + comfy = comfy.set_alignment(CellAlignment::Right); + } + if let Some(color) = cell.color { + comfy = comfy.fg(color); + } + comfy + }) + .collect::>(), + ); + } + + table +} + +/// Budgeted content width for the flex column, or `None` when too little room is +/// left to format structurally. +/// +/// Other columns are subtracted at their content width (capped by any +/// `Max`/`Range` upper bound); when that starves the flex column, `Range` columns +/// are assumed to shrink to their lower bound. This assumes non-flex columns +/// render at their content width — true for `Content`/`Exact`/`Range` but not +/// `Min`/`Auto`, which can grow past content and overshoot. +fn flex_content_width(spec: &SelfFormattingTableSpec) -> Option { + let borders = spec.headers.len() + 1; + let flex_min = spec.flex.min_width + CELL_PADDING; + + let mut fixed_total = 0usize; + let mut reclaimable = 0usize; + for (index, header) in spec.headers.iter().enumerate() { + if index == spec.flex.index { + continue; + } + let content = column_content_width(spec, index); + let effective = match header.width { + ColumnWidth::Exact(width) => width as usize, + ColumnWidth::Max(max) | ColumnWidth::Range { max, .. } => content.min(max as usize), + _ => content, + } + CELL_PADDING; + if let ColumnWidth::Range { min, .. } = header.width { + reclaimable += effective.saturating_sub(min as usize + CELL_PADDING); + } + fixed_total += effective; + } + + let mut budget = (spec.term_width as usize).checked_sub(fixed_total + borders)?; + if budget < flex_min { + budget += reclaimable.min(flex_min - budget); + } + + // Never wider than the longest cell needs: claiming more would only pad the + // table out with empty space. + let needed = column_content_width(spec, spec.flex.index) + CELL_PADDING; + let budget = budget.min(needed); + + (budget >= flex_min).then(|| budget - CELL_PADDING) +} + +/// Widest content in a column, including its header title. +fn column_content_width(spec: &SelfFormattingTableSpec, index: usize) -> usize { + spec.rows + .iter() + .filter_map(|row| row.get(index)) + .map(TableCell::content_width) + .chain(std::iter::once(spec.headers[index].title.chars().count())) + .max() + .unwrap_or(0) +} + +fn preset_str(preset: TablePreset, colorize: bool) -> &'static str { + use comfy_table::presets::{ASCII_FULL, ASCII_FULL_CONDENSED, UTF8_FULL, UTF8_FULL_CONDENSED}; + match (preset, colorize) { + (TablePreset::Full, true) => UTF8_FULL, + (TablePreset::FullCondensed, true) => UTF8_FULL_CONDENSED, + (TablePreset::Full, false) => ASCII_FULL, + (TablePreset::FullCondensed, false) => ASCII_FULL_CONDENSED, + } +} + pub fn log_text_view(view: &View) { view.log(); } @@ -791,3 +1016,81 @@ pub fn format_component_applied_layers( }) .join(", ") } + +#[cfg(test)] +mod tests { + use super::*; + use test_r::test; + + fn flex_table( + term_width: u16, + component_names: &[&str], + ids: &[&str], + ) -> SelfFormattingTableSpec<'static> { + let headers = vec![ + Column::new("Component name").width_range(12, 28), + Column::new("Agent name"), + Column::new("Status").content_right(), + ]; + let rows = component_names + .iter() + .zip(ids) + .map(|(component, id)| { + vec![ + TableCell::new(component.to_string()), + TableCell::new(id.to_string()), + TableCell::new("Idle"), + ] + }) + .collect(); + SelfFormattingTableSpec { + preset: TablePreset::FullCondensed, + term_width, + full_width: false, + headers, + flex: FlexColumn { + index: 1, + min_width: 24, + format: &|raw, _| raw.to_string(), + }, + rows, + } + } + + /// A wide terminal gives the flex column all the leftover room, but never + /// more than its longest cell needs. + #[test] + fn flex_budget_fills_leftover_but_not_beyond_content() { + let long = "ShoppingCart(\"a-fairly-long-user-identifier\", [1, 2, 3])"; + // Plenty of room: the column takes exactly what the longest id needs. + assert_eq!( + flex_content_width(&flex_table( + 200, + &["comp:one", "comp:two"], + &[long, "Counter(\"x\")"] + )), + Some(long.chars().count()) + ); + } + + /// A tight terminal squeezes the `Range` component column before the flex + /// column drops below its minimum. + #[test] + fn flex_budget_squeezes_range_column_when_tight() { + let ids = ["ShoppingCart(\"a-long-id-that-needs-wrapping-here\")"; 2]; + let width = flex_content_width(&flex_table(70, &["a-long-component-name-x", "b"], &ids)) + .expect("should still format"); + assert!(width >= 24, "flex dropped below its minimum: {width}"); + } + + /// Too narrow for both columns at their floor: the flex column opts out and + /// the ids are left for the engine to wrap. + #[test] + fn flex_budget_gives_up_when_no_room() { + let ids = ["ShoppingCart(\"x\")"; 2]; + assert_eq!( + flex_content_width(&flex_table(30, &["component-name-here", "b"], &ids)), + None + ); + } +} diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index 1b9dff7f9f..1841cce1a9 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -28,10 +28,7 @@ use base64::prelude::BASE64_STANDARD; use chrono::DateTime; use colored::Colorize; -use comfy_table::{ - Cell, CellAlignment, Color as ComfyColor, ColumnConstraint, ContentArrangement, - Table as ComfyTable, -}; +use comfy_table::Color as ComfyColor; use golem_common::model::component::ComponentName; use golem_common::model::oplog::{ MultipartPartData, PluginInstallationDescription, PublicAgentInvocation, @@ -70,7 +67,13 @@ impl MessageWithFields for WorkerCreateView { fields .fmt_field("Component name", &self.component_name, format_id) - .fmt_field("Agent name", &self.agent_name, format_agent_name); + .fmt_field("Agent name", &self.agent_name, |agent_name| { + format_agent_id_in( + &agent_name.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }); fields.build() } @@ -181,7 +184,13 @@ impl MessageWithFields for WorkerGetView { &self.metadata.component_revision, format_id, ) - .fmt_field("Agent name", &self.metadata.agent_name, format_agent_name) + .fmt_field("Agent name", &self.metadata.agent_name, |agent_name| { + format_agent_id_in( + &agent_name.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }) .field("Created at", &self.metadata.created_at) .fmt_field( "Component size", @@ -306,6 +315,14 @@ impl TextOutput for AgentsMetadataResponseView { } } +/// Agent-list component-name column: capped at `MAX` so it cannot eat the agent +/// id budget, squeezable to `MIN` when ids need the room. +const MAX_COMPONENT_NAME_WIDTH: usize = 28; +const MIN_COMPONENT_NAME_WIDTH: usize = 12; + +/// Below this the agent id column is left unformatted for the table to wrap. +const MIN_AGENT_NAME_WIDTH: usize = 24; + impl AgentsMetadataResponseView { fn status_color(status: &AgentStatus, colorize: bool) -> ComfyColor { if colorize { @@ -329,57 +346,54 @@ impl AgentsMetadataResponseView { colorize: bool, full_width: bool, ) -> String { - use comfy_table::presets::{ASCII_FULL_CONDENSED, UTF8_FULL_CONDENSED}; - - let preset = if colorize { - UTF8_FULL_CONDENSED - } else { - ASCII_FULL_CONDENSED - }; + // Agent ids are self-formatted (broken at their own structure), so the + // column width must be known before the cells are built; + // `self_formatting_table` budgets it. `Range` marks the component name as + // the squeezable column. + let headers = vec![ + Column::new("Component name") + .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), + Column::new("Agent name"), + Column::new("Revision").content_right(), + Column::new("Status").content_right(), + Column::new("Pending").content_right(), + Column::new("Created at").content(), + ]; - let arrangement = if full_width { - ContentArrangement::DynamicFullWidth - } else { - ContentArrangement::Dynamic + let format_agent_id = |raw: &str, width: Option| match width { + Some(width) => format_agent_id_in(raw, colorize, width), + None => raw.to_string(), }; - let mut table = ComfyTable::new(); - table - .load_preset(preset) - .set_content_arrangement(arrangement) - .set_width(term_width) - .set_header(vec![ - "Component name", - "Agent name", - "Revision", - "Status", - "Pending", - "Created at", - ]); - - // Pin fixed-width columns so component_name (0) and agent_name (1) absorb surplus width - for col_idx in 2..=5usize { - table - .column_mut(col_idx) - .unwrap() - .set_constraint(ColumnConstraint::ContentWidth); - } - - for agent in agents { - table.add_row(vec![ - Cell::new(agent.component_name.to_string()), - Cell::new(agent.agent_name.0.clone()), - Cell::new(agent.component_revision.to_string()).set_alignment(CellAlignment::Right), - Cell::new(agent.status.to_string()) - .set_alignment(CellAlignment::Right) - .fg(Self::status_color(&agent.status, colorize)), - Cell::new(agent.pending_invocation_count.to_string()) - .set_alignment(CellAlignment::Right), - Cell::new(agent.created_at.to_string()), - ]); - } - - table.to_string() + let rows = agents + .iter() + .map(|agent| { + vec![ + TableCell::new(agent.component_name.to_string()), + TableCell::new(agent.agent_name.0.clone()), + TableCell::new(agent.component_revision.to_string()).right(), + TableCell::new(agent.status.to_string()) + .right() + .color(Self::status_color(&agent.status, colorize)), + TableCell::new(agent.pending_invocation_count.to_string()).right(), + TableCell::new(agent.created_at.to_string()), + ] + }) + .collect(); + + self_formatting_table(SelfFormattingTableSpec { + preset: TablePreset::FullCondensed, + term_width, + full_width, + headers, + flex: FlexColumn { + index: 1, + min_width: MIN_AGENT_NAME_WIDTH, + format: &format_agent_id, + }, + rows, + }) + .to_string() } } @@ -1317,9 +1331,9 @@ fn render_typed_schema_value_line( format!("{pad} {rendered}") } -// TODO: pretty print -fn format_agent_name(agent_name: &RawAgentId) -> String { - textwrap::wrap(&agent_name.to_string(), 80).join("\n") +/// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). +fn format_agent_id_in(agent_name: &str, colorize: bool, width: usize) -> String { + crate::agent_id_display::format_agent_id_for_terminal(agent_name, colorize, Some(width)) } fn log_optional_error(pad: &str, error: &Option) { @@ -1375,6 +1389,7 @@ fn render_snapshot_data_lines(pad: &str, snapshot: &PublicSnapshotData) -> Vec AgentMetadataView { + AgentMetadataView { + component_name: ComponentName("shop:cart".to_string()), + agent_name: RawAgentId(agent_name.to_string()), + created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), + environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), + env: HashMap::new(), + default_env: HashMap::new(), + config: Vec::new(), + default_config: Vec::new(), + status: AgentStatus::Running, + component_revision: ComponentRevision::new(1).expect("valid revision"), + retry_count: 0, + pending_invocation_count: 0, + updates: Vec::new(), + created_at: timestamp(), + last_error: None, + component_size: 0, + total_linear_memory_size: 0, + exported_resource_instances: HashMap::new(), + source_language: SourceLanguage::Rust, + secret_config_paths: std::collections::BTreeSet::new(), + } + } + + /// Long agent ids are pre-formatted, so they have to break at their own + /// structure inside the cell instead of being wrapped mid-token, without + /// disturbing the table layout. + #[test] + fn table_breaks_long_agent_ids_structurally() { + let agents = vec![ + agent_metadata( + r#"ShoppingCart(user: "a-fairly-long-user-identifier", items: ["one", "two"])"#, + ), + agent_metadata(r#"Counter("short")"#), + ]; + + let table = AgentsMetadataResponseView::format_table_wide(&agents, 120, false, false); + + // The indent only appears if we broke the id ourselves; the table's own + // wrapping would split it mid-token and would not indent. + assert!( + table.contains(r#" user: "a-fairly-long-user-identifier","#), + "long id was not broken at its structure:\n{table}" + ); + assert_rows_aligned(&table); + } + + /// Agent id cells carry their own coloring, which only lines up if + /// comfy-table measures cell content with the ANSI escapes stripped. That + /// needs its `custom_styling` feature, so this fails if the feature is + /// dropped. The escapes are written out here because the `colored` crate + /// emits none while colors are globally off, as they are under tests. + #[test] + fn table_measures_cells_with_ansi_escapes_stripped() { + let mut table = ComfyTable::new(); + table + .set_header(vec!["Agent name"]) + .add_row(vec![Cell::new("\u{1b}[32mColored(\"id\")\u{1b}[0m")]); + + assert_rows_aligned(&table.to_string()); + } + + fn assert_rows_aligned(table: &str) { + let widths = table + .lines() + .map(|line| strip_ansi_escapes::strip_str(line).chars().count()) + .collect::>(); + + assert!( + widths.iter().all(|width| *width == widths[0]), + "misaligned rows, widths {widths:?}:\n{table}" + ); + } + fn typed_string_value(value: &str) -> TypedSchemaValue { TypedSchemaValue::new( SchemaGraph::anonymous(SchemaType::string()), @@ -1643,12 +1733,11 @@ pub fn format_timestamp(timestamp: u64) -> String { } pub fn format_agent_name_match(agent_name_match: &AgentNameMatch) -> String { - let rendered_agent_name = match &agent_name_match.parsed_agent_id { - Some(parsed) if agent_name_match.source_language.is_known() => { - crate::agent_id_display::render_agent_id(parsed, &agent_name_match.source_language) - } - _ => agent_name_match.agent_name.0.clone(), - }; + let rendered_agent_name = crate::agent_id_display::render_agent_id_or_raw( + agent_name_match.parsed_agent_id.as_ref(), + &agent_name_match.source_language, + &agent_name_match.agent_name.0, + ); format!( "{}{}/{}", From 6f2194cd7d1ad0444abbd4489f7184066583108a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Mon, 27 Jul 2026 14:07:33 +0200 Subject: [PATCH 14/70] validate component preset references and ensure proper error handling during environment and runtime selection --- cli/golem-cli/src/model/app.rs | 227 ++++++++++++++++++++++++++++++--- 1 file changed, 208 insertions(+), 19 deletions(-) diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 3bfcdd82f9..0dfc1b057e 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -2545,10 +2545,10 @@ mod app_builder { use crate::fuzzy::FuzzySearch; use crate::log::LogColorize; use crate::model::app::{ - Application, ApplicationPreload, ComponentDependency, ComponentLayer, - ComponentLayerApplyContext, ComponentLayerId, ComponentLayerProperties, - ComponentLayerPropertiesKind, ComponentPresetName, ComponentPresetSelector, - ComponentProperties, PartitionedComponentPresets, TEMP_DIR, WithSource, + APP_ENV_PRESET_PREFIX, Application, ApplicationPreload, ComponentDependency, + ComponentLayer, ComponentLayerApplyContext, ComponentLayerId, ComponentLayerProperties, + ComponentLayerPropertiesKind, ComponentPresetSelector, ComponentProperties, + PartitionedComponentPresets, TEMP_DIR, WithSource, }; use crate::model::app_raw; use crate::model::cascade::store::Store; @@ -2870,7 +2870,11 @@ mod app_builder { raw_component_names: HashSet, component_names_to_source_and_dir: BTreeMap)>, - component_custom_presets: BTreeSet, + // Every custom preset name defined by any component, component template, + // or agent (env-scoped `app-env:` presets excluded), so environment preset + // references can be validated. Populated as those parts are processed, via + // `record_selectable_presets`. + custom_preset_names: BTreeSet, component_layer_store: Store, components: @@ -2947,8 +2951,8 @@ mod app_builder { builder.add_raw_app(&mut validation, app); } - // TODO: atomic: validate presets used in envs and template references - // before component resolve, and skip if they are not valid + builder.validate_environment_preset_references(&mut validation); + builder.validate_selected_preset_references(&mut validation, &component_presets); builder.resolve_and_validate_components(&mut validation, &component_presets); builder.validate_unique_sources(&mut validation); builder.validate_http_api_deployments(&mut validation, &environments); @@ -3096,6 +3100,7 @@ mod app_builder { // agent templates/presets and flattened component fallback layers. let unique_key = UniqueSourceCheckedEntityKey::Agent(agent_type_name.clone()); if self.add_entity_source(unique_key, &app.source) { + self.record_selectable_presets(agent_properties.presets.keys()); self.agents.insert( agent_type_name, WithSource::new(app.source.clone(), agent_properties), @@ -3391,12 +3396,6 @@ mod app_builder { self.environments .insert(environment_name.clone(), environment.clone()); - validation.with_context( - vec![("environment", environment_name.0)], - |_validation| { - // TODO: atomic: validate environment - }, - ); } } @@ -3511,6 +3510,7 @@ mod app_builder { validation.add_error(err.to_string()) } + self.record_selectable_presets(template.presets.keys()); let presets = PartitionedComponentPresets::new(template.presets); if let Some(err) = self @@ -3586,13 +3586,9 @@ mod app_builder { validation.add_error(err.to_string()) } + self.record_selectable_presets(component.presets.keys()); let presets = PartitionedComponentPresets::new(component.presets); - presets.custom_presets.keys().for_each(|preset_name| { - self.component_custom_presets - .insert(ComponentPresetName(preset_name.clone())); - }); - if let Some(err) = self .component_layer_store .add_layer(ComponentLayer { @@ -3663,6 +3659,91 @@ mod app_builder { }) } + /// Records preset names (from a component, template, or agent) that an + /// environment can select into [`Self::custom_preset_names`]. Env-scoped + /// `app-env:` presets are applied automatically rather than selected by + /// name, so they are excluded. + fn record_selectable_presets<'a>(&mut self, names: impl Iterator) { + for name in names { + if !name.starts_with(APP_ENV_PRESET_PREFIX) { + self.custom_preset_names.insert(name.clone()); + } + } + } + + /// Validates that every preset an environment declares is defined somewhere. + /// Preset selection (see `ComponentLayer::apply`) silently drops names it + /// does not recognise, so without this an environment typo would apply no + /// preset with no warning. The `--preset` flag is checked separately, in + /// [`Self::validate_selected_preset_references`]. + fn validate_environment_preset_references(&self, validation: &mut ValidationBuilder) { + for (environment_name, environment) in &self.environments { + let referenced = environment.component_presets.clone().into_vec(); + if referenced.is_empty() { + continue; + } + validation.with_context( + vec![("environment", environment_name.0.clone())], + |validation| { + for preset in referenced { + self.validate_preset_defined(validation, &preset, None); + } + }, + ); + } + } + + /// Validates the presets actually selected for this run. Environment presets + /// are covered by [`Self::validate_environment_preset_references`]; this also + /// catches presets given with `--preset`, which replace the environment's + /// list and would otherwise be silently dropped. + fn validate_selected_preset_references( + &self, + validation: &mut ValidationBuilder, + selector: &ComponentPresetSelector, + ) { + let environment_refs = self + .environments + .get(&selector.environment) + .map(|environment| environment.component_presets.clone().into_set()) + .unwrap_or_default(); + + for preset in &selector.presets { + // Skip presets that came from the environment; those are already + // reported by validate_environment_preset_references. + if environment_refs.contains(&preset.0) { + continue; + } + self.validate_preset_defined(validation, &preset.0, Some("--preset")); + } + } + + /// Adds an error if `preset` is not a defined selectable preset name. + /// `selected_with` names the flag it came from, when not an environment. + fn validate_preset_defined( + &self, + validation: &mut ValidationBuilder, + preset: &str, + selected_with: Option<&str>, + ) { + if self.custom_preset_names.contains(preset) { + return; + } + let source = selected_with + .map(|flag| format!(" selected with {flag}")) + .unwrap_or_default(); + validation.add_error(format!( + "Unknown preset {}{source}.\n{}", + preset.log_color_error_highlight(), + self.available_options_help( + "presets", + "preset names", + preset, + self.custom_preset_names.iter().map(String::as_str), + ), + )); + } + fn resolve_and_validate_components( &mut self, validation: &mut ValidationBuilder, @@ -3993,6 +4074,15 @@ mod test { componentWasm: dummy-component.wasm components: + # Defines debug/release so they are known preset names; app:main + # below deliberately does not define them, to exercise the fallback. + app:other: + componentWasm: other.wasm + presets: + debug: + componentWasm: other-debug.wasm + release: + componentWasm: other-release.wasm app:main: templates: malbogle presets: @@ -4669,6 +4759,93 @@ mod test { ); } + #[test] + fn environment_unknown_component_preset_is_rejected() { + let errors = load_app_errors(indoc! { r#" + app: hello-app + + environments: + local: + server: local + componentPresets: slow + + components: + app:main: + componentWasm: dummy-component.wasm + presets: + fast: + env: + MODE: fast + "# }); + + assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}"); + assert!( + errors[0].contains("Unknown preset") && errors[0].contains("slow"), + "unexpected error: {}", + errors[0] + ); + // The available presets are listed so the user can find the right name. + assert!( + errors[0].contains("fast"), + "error should list available presets: {}", + errors[0] + ); + } + + #[test] + fn environment_known_component_preset_is_accepted() { + let errors = load_app_errors(indoc! { r#" + app: hello-app + + environments: + local: + server: local + componentPresets: fast + + components: + app:main: + componentWasm: dummy-component.wasm + presets: + fast: + env: + MODE: fast + "# }); + + assert!(errors.is_empty(), "unexpected errors: {errors:?}"); + } + + #[test] + fn unknown_preset_selected_with_flag_is_rejected() { + // A `--preset` value replaces the environment's list and would otherwise + // be silently dropped during selection. + let source = indoc! { r#" + app: hello-app + + environments: + local: + server: local + + components: + app:main: + componentWasm: dummy-component.wasm + presets: + fast: + env: + MODE: fast + "# }; + + let errors = load_app_errors_with_selector(source, &selector("local", &["slow"])); + + assert_eq!(errors.len(), 1, "unexpected errors: {errors:?}"); + assert!( + errors[0].contains("Unknown preset") + && errors[0].contains("slow") + && errors[0].contains("--preset"), + "unexpected error: {}", + errors[0] + ); + } + #[test] fn non_rust_guest_bridge_mode_is_rejected() { let source = indoc! { r#" @@ -5001,6 +5178,11 @@ mod test { components: app:main: componentWasm: dummy-component.wasm + # 'missing' is a known preset name (so selecting it is valid), but + # test-agent below does not define it, exercising the fallback. + presets: + missing: + componentWasm: dummy-component.wasm agents: test-agent: @@ -5709,6 +5891,13 @@ mod test { } fn load_app_errors(source: &str) -> Vec { + load_app_errors_with_selector(source, &selector("local", &[])) + } + + fn load_app_errors_with_selector( + source: &str, + selector: &ComponentPresetSelector, + ) -> Vec { let tmp_dir = tempfile::tempdir().unwrap(); let golem_yaml_path = tmp_dir.path().join("golem.yaml"); @@ -5736,7 +5925,7 @@ mod test { application_name, environments, local_server, - selector("local", &[]), + selector.clone(), raw_apps, ) .into_product(); From 604f17db21b32354e564ce9fae71c2f33d0af911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Mon, 27 Jul 2026 14:42:36 +0200 Subject: [PATCH 15/70] clanups --- cli/golem-cli/src/command_handler/component/mod.rs | 1 - cli/golem-cli/src/model/app.rs | 13 ------------- 2 files changed, 14 deletions(-) diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index c39e38c459..8cf200f88c 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -943,7 +943,6 @@ impl ComponentCommandHandler { component_name: &ComponentName, properties: &ComponentDeployProperties, ) -> anyhow::Result { - // TODO: atomic: cache it with a TaskResultMarker? let component_binary_hash = { log_action( "Calculating hash", diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 0dfc1b057e..75cee69326 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -3932,19 +3932,6 @@ mod app_builder { self.available_options_help("profiles", "profile names", unknown, available_profiles) } - // TODO: atomic - #[allow(unused)] - fn available_templates(&self, _unknown: &str) -> String { - // TODO: atomic - /*self.available_options_help( - "templates", - "template names", - unknown, - self.templates.keys().map(|name| name.as_str()), - )*/ - todo!() - } - fn available_options_help<'a, I: IntoIterator>( &self, entity_plural: &str, From c7b5b9bf151f6bc45659fe3491fdda109d4b80e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Mon, 27 Jul 2026 15:45:11 +0200 Subject: [PATCH 16/70] cleanup --- cli/golem-cli/src/config.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cli/golem-cli/src/config.rs b/cli/golem-cli/src/config.rs index e6d1f1afc2..9da13314b9 100644 --- a/cli/golem-cli/src/config.rs +++ b/cli/golem-cli/src/config.rs @@ -205,7 +205,11 @@ impl Config { ) })?; - // TODO: atomic: check and override urls, if necessary (and save) + // Built-in `local`/`cloud` URLs should be fixed (`builtin_local_url()` / + // `DEFAULT_CLOUD_URL`), but a `custom_url` hand-edited into their config + // entry is still honoured in non-manifest mode — a migration leftover. + // Making them authoritative and rewriting stale stored values is a deferred + // behaviour change; for now the stored value is left as-is. Ok(config.with_local_and_cloud_profiles()) } From ab6164cf3356f2a06abaf64e12c7defe736ec8d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 28 Jul 2026 10:15:56 +0200 Subject: [PATCH 17/70] add explicit `--auth` mode and improve static token handling in profile commands --- cli/golem-cli/src/command.rs | 23 +++++- .../src/command_handler/interactive.rs | 76 ++++++++++--------- .../src/command_handler/profile/mod.rs | 41 ++++++++-- .../common/golem-cloud-account-setup.mdx | 8 +- .../golem-profiles-and-environments.mdx | 9 ++- .../common/golem-cloud-account-setup/SKILL.md | 8 +- .../golem-profiles-and-environments/SKILL.md | 9 ++- 7 files changed, 125 insertions(+), 49 deletions(-) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index b1c7be9617..09b0732456 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -302,7 +302,9 @@ impl GolemCliGlobalFlags { self.agent_stream_ping_interval = Some( iso8601::duration(&interval) .map_err(|err| { - anyhow!("Failed to parse GOLEM_AGENT_STREAM_PING_INTERVAL ({interval}): {err}") + anyhow!( + "Failed to parse GOLEM_AGENT_STREAM_PING_INTERVAL ({interval}): {err}" + ) })? .into(), ); @@ -2092,9 +2094,16 @@ pub mod profile { use crate::command::profile::config::ProfileConfigSubcommand; use crate::config::ProfileName; use crate::model::format::Format; - use clap::Subcommand; + use clap::{Subcommand, ValueEnum}; use url::Url; + #[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] + #[clap(rename_all = "kebab-case")] + pub enum ProfileAuthMode { + Oauth2, + Static, + } + #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum ProfileSubcommand { @@ -2115,8 +2124,14 @@ pub mod profile { /// Default output format for this profile #[arg(long, default_value_t = Format::Text)] default_format: Format, - /// Token to use for authenticating against Golem. If not provided an OAuth2 flow will be performed when authentication is needed for the first time. - #[arg(long)] + /// How the profile authenticates: `oauth2` (interactive browser login, + /// the default) or `static` (a pre-issued token). Defaults to `static` + /// when `--static-token` is given. + #[arg(long, value_enum, verbatim_doc_comment)] + auth: Option, + /// Static authentication token (implies `--auth static`). With + /// `--auth static` and no token given, you are prompted for it. + #[arg(long, verbatim_doc_comment)] static_token: Option, /// Accept invalid certificates. /// diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index 11a9943453..214eb2767a 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::app::template::AppTemplateName; -use crate::config::{AuthSecret, AuthenticationConfig, Profile, ProfileConfig, ProfileName}; +use crate::config::{AuthenticationConfig, Profile, ProfileConfig, ProfileName}; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::log::{LogColorize, log_error, log_warn, log_warn_action, logln}; @@ -32,7 +32,7 @@ use golem_common::model::environment::EnvironmentName; use indoc::formatdoc; use inquire::error::InquireResult; use inquire::validator::{ErrorMessage, Validation}; -use inquire::{Confirm, CustomType, InquireError, MultiSelect, Select, Text}; +use inquire::{Confirm, CustomType, InquireError, MultiSelect, Password, PasswordDisplayMode, Select, Text}; use itertools::Itertools; use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; @@ -400,16 +400,16 @@ impl InteractiveHandler { .with_starting_cursor(2) .prompt()?; - let static_token = CustomType::::new( - "Static token for authentication (empty to use interactive authentication via OAuth2):", - ) - .prompt()? - .0; + let static_token = Password::new("Static token for authentication (leave empty for OAuth2):") + .with_display_mode(PasswordDisplayMode::Masked) + .without_confirmation() + .with_help_message("Mainly for testing or custom servers") + .prompt()?; - let auth = if let Some(static_token) = static_token { - AuthenticationConfig::static_token(static_token.0) - } else { + let auth = if static_token.is_empty() { AuthenticationConfig::empty_oauth2() + } else { + AuthenticationConfig::static_token(static_token) }; let profile = Profile { @@ -427,6 +427,38 @@ impl InteractiveHandler { Ok((profile_name.into(), profile, set_as_active)) } + /// Prompts (masked) for a required static authentication token, used when a + /// profile is created with `--auth static` but no `--static-token`. In a + /// non-interactive environment there is no way to ask, so it fails with a + /// hint to pass the token directly. + pub fn prompt_static_token(&self) -> anyhow::Result { + let token = Password::new("Static authentication token:") + .with_display_mode(PasswordDisplayMode::Masked) + .without_confirmation() + .with_help_message("Mainly for testing or custom servers") + .with_validator(|value: &str| { + if value.trim().is_empty() { + Ok(Validation::Invalid(ErrorMessage::from( + "A static token is required with --auth static", + ))) + } else { + Ok(Validation::Valid) + } + }) + .prompt() + .none_if_not_interactive()?; + + match token { + Some(token) => Ok(token), + None => { + log_error( + "Cannot prompt for a static token in a non-interactive environment. Pass --static-token instead.", + ); + bail!(NonSuccessfulExit) + } + } + } + pub fn select_repl_language( &self, repl_languages: Vec, @@ -650,30 +682,6 @@ impl FromStr for OptionalUrl { } } -#[derive(Clone)] -pub struct OptionalAuthSecret(Option); - -impl Display for OptionalAuthSecret { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match &self.0 { - None => Ok(()), - Some(value) => write!(f, "{value}"), - } - } -} - -impl FromStr for OptionalAuthSecret { - type Err = uuid::Error; - - fn from_str(s: &str) -> Result { - if s.trim().is_empty() { - Ok(Self(None)) - } else { - Ok(Self(Some(AuthSecret(s.to_string())))) - } - } -} - trait InquireResultExtensions { fn none_if_not_interactive(self) -> anyhow::Result>; diff --git a/cli/golem-cli/src/command_handler/profile/mod.rs b/cli/golem-cli/src/command_handler/profile/mod.rs index f22e153f2f..29ee79d124 100644 --- a/cli/golem-cli/src/command_handler/profile/mod.rs +++ b/cli/golem-cli/src/command_handler/profile/mod.rs @@ -14,7 +14,7 @@ pub mod config; -use crate::command::profile::ProfileSubcommand; +use crate::command::profile::{ProfileAuthMode, ProfileSubcommand}; use crate::command_handler::Handlers; use crate::config::{ AuthenticationConfig, Config, NamedProfile, Profile, ProfileConfig, ProfileName, @@ -51,6 +51,7 @@ impl ProfileCommandHandler { worker_url, default_format, allow_insecure, + auth, static_token, } => self.cmd_new( name, @@ -59,6 +60,7 @@ impl ProfileCommandHandler { worker_url, default_format, allow_insecure, + auth, static_token, ), ProfileSubcommand::List => self.cmd_list(), @@ -85,6 +87,7 @@ impl ProfileCommandHandler { worker_url: Option, default_format: Format, allow_insecure: bool, + auth: Option, static_token: Option, ) -> anyhow::Result<()> { let (name, profile, set_active) = match name { @@ -96,12 +99,7 @@ impl ProfileCommandHandler { bail!(NonSuccessfulExit); } - let auth = if let Some(static_token) = static_token { - // TODO: we may want to read from prompt instead of reading parameter - AuthenticationConfig::static_token(static_token) - } else { - AuthenticationConfig::empty_oauth2() - }; + let auth = self.resolve_auth_config(auth, static_token)?; let profile = Profile { custom_url, @@ -142,6 +140,35 @@ impl ProfileCommandHandler { Ok(()) } + /// Resolves the profile's authentication from the explicit `--auth` mode and + /// the optional `--static-token`. A static token implies static auth; `--auth + /// static` without a token prompts for one; the two are only in conflict when + /// a token is paired with `--auth oauth2`. + fn resolve_auth_config( + &self, + mode: Option, + static_token: Option, + ) -> anyhow::Result { + match (mode, static_token) { + (Some(ProfileAuthMode::Oauth2), Some(_)) => { + log_error( + "--static-token cannot be combined with --auth oauth2. A static token implies --auth static.", + ); + bail!(NonSuccessfulExit); + } + (Some(ProfileAuthMode::Static) | None, Some(token)) => { + Ok(AuthenticationConfig::static_token(token)) + } + (Some(ProfileAuthMode::Static), None) => { + let token = self.ctx.interactive_handler().prompt_static_token()?; + Ok(AuthenticationConfig::static_token(token)) + } + (Some(ProfileAuthMode::Oauth2), None) | (None, None) => { + Ok(AuthenticationConfig::empty_oauth2()) + } + } + } + fn cmd_list(&self) -> anyhow::Result<()> { let config = Config::from_dir(self.ctx.config_dir())?; let default_profile_name = config.default_profile_name(); diff --git a/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx b/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx index 1c467fa6aa..22e6a04be5 100644 --- a/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx @@ -24,7 +24,7 @@ If you need a custom cloud profile (e.g., for a different cloud endpoint): golem profile new my-cloud --url https://release.api.golem.cloud --set-active ``` -When no `--static-token` is provided, the profile uses OAuth2 (GitHub) authentication — a browser window will open on first use. +By default (equivalently `--auth oauth2`) the profile uses OAuth2 (GitHub) authentication — a browser window will open on first use. Pass `--auth static` (or `--static-token`) for token-based auth instead. ## Step 2: Authenticate @@ -84,6 +84,12 @@ Use a static token in a profile for non-interactive environments: golem profile new ci-cloud --url https://release.api.golem.cloud --static-token "" --set-active ``` +For an interactive setup, `--auth static` (without `--static-token`) prompts for the token instead of putting it on the command line: + +```shell +golem profile new my-cloud --url https://release.api.golem.cloud --auth static --set-active +``` + ## Step 5: Configure Your Application for Cloud Deployment In your `golem.yaml`, add a `cloud` environment: diff --git a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx index dc00ceff7b..226ef37cfc 100644 --- a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx @@ -37,7 +37,9 @@ Profiles are global CLI configuration stored in `~/.golem/config.json`. They def ```shell golem profile new # Interactive setup -golem profile new my-staging --url https://staging.example.com --static-token "..." +golem profile new my-staging --url https://staging.example.com # OAuth2 (default) +golem profile new my-staging --url https://staging.example.com --auth static # static token (prompted, masked) +golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline (scripts) golem profile list # List all profiles golem profile switch my-staging # Set active profile golem profile get # Show active profile @@ -46,6 +48,11 @@ golem profile delete my-staging # Delete a profile golem profile config my-staging set-format json # Set default output format ``` +Authentication mode is explicit via `--auth`: `oauth2` (default — interactive +browser login on first use) or `static` (a pre-issued token). Passing +`--static-token` implies `--auth static`; `--auth static` without a token +prompts for one (masked). A token cannot be combined with `--auth oauth2`. + ### Global flags ```shell diff --git a/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md b/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md index 39c822df41..130b73cdd5 100644 --- a/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md +++ b/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md @@ -29,7 +29,7 @@ If you need a custom cloud profile (e.g., for a different cloud endpoint): golem profile new my-cloud --url https://release.api.golem.cloud --set-active ``` -When no `--static-token` is provided, the profile uses OAuth2 (GitHub) authentication — a browser window will open on first use. +By default (equivalently `--auth oauth2`) the profile uses OAuth2 (GitHub) authentication — a browser window will open on first use. Pass `--auth static` (or `--static-token`) for token-based auth instead. ## Step 2: Authenticate @@ -89,6 +89,12 @@ Use a static token in a profile for non-interactive environments: golem profile new ci-cloud --url https://release.api.golem.cloud --static-token "" --set-active ``` +For an interactive setup, `--auth static` (without `--static-token`) prompts for the token instead of putting it on the command line: + +```shell +golem profile new my-cloud --url https://release.api.golem.cloud --auth static --set-active +``` + ## Step 5: Configure Your Application for Cloud Deployment In your `golem.yaml`, add a `cloud` environment: diff --git a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md index aa0e54bae7..3e40e75f54 100644 --- a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md +++ b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md @@ -42,7 +42,9 @@ Profiles are global CLI configuration stored in `~/.golem/config.json`. They def ```shell golem profile new # Interactive setup -golem profile new my-staging --url https://staging.example.com --static-token "..." +golem profile new my-staging --url https://staging.example.com # OAuth2 (default) +golem profile new my-staging --url https://staging.example.com --auth static # static token (prompted, masked) +golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline (scripts) golem profile list # List all profiles golem profile switch my-staging # Set active profile golem profile get # Show active profile @@ -51,6 +53,11 @@ golem profile delete my-staging # Delete a profile golem profile config my-staging set-format json # Set default output format ``` +Authentication mode is explicit via `--auth`: `oauth2` (default — interactive +browser login on first use) or `static` (a pre-issued token). Passing +`--static-token` implies `--auth static`; `--auth static` without a token +prompts for one (masked). A token cannot be combined with `--auth oauth2`. + ### Global flags ```shell From 73620f34e77a41d51420bbc48fc2bb4f03c52656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 28 Jul 2026 23:18:15 +0200 Subject: [PATCH 18/70] unify structured output of bulk agent ops (update/redeploy/delete) --- .../command-output.schema.json | 87 ++++++++++--- .../src/command_handler/component/mod.rs | 67 +++++++--- .../src/command_handler/worker/mod.rs | 96 +++++++++------ cli/golem-cli/src/model/cli_output.rs | 116 ++++++++++++++---- cli/golem-cli/src/model/deploy.rs | 18 +-- cli/golem-cli/src/model/text/action_result.rs | 39 +++++- 6 files changed, 325 insertions(+), 98 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 993e6c5d28..07247fdeb7 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -51,6 +51,9 @@ { "$ref": "#/definitions/agent.delete" }, + { + "$ref": "#/definitions/agent.delete-all" + }, { "$ref": "#/definitions/agent.file-contents" }, @@ -1195,6 +1198,32 @@ }, "additionalProperties": false }, + "agent.delete-all": { + "type": "object", + "description": "Single structured output document emitted by `golem deploy` when it deletes all existing agents (e.g. `--reset`).", + "x-golem-output-mode": "single", + "x-golem-command": "deploy", + "required": [ + "$type", + "deleted", + "agents" + ], + "properties": { + "$type": { + "const": "agent.delete-all" + }, + "deleted": { + "type": "boolean" + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/definitions/AgentDeletionMeta" + } + } + }, + "additionalProperties": false + }, "agent.file-contents": { "type": "object", "description": "Single structured output document emitted by `golem agent file-contents`.", @@ -1449,7 +1478,7 @@ "required": [ "$type", "redeployed", - "components" + "agents" ], "properties": { "$type": { @@ -1458,10 +1487,10 @@ "redeployed": { "type": "boolean" }, - "components": { + "agents": { "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/AgentRedeploymentMeta" } } }, @@ -1600,23 +1629,23 @@ "x-golem-commands": ["agent update","update-agents","component update-agents"], "required": [ "$type", - "triggered", - "failed" + "agents", + "errors" ], "properties": { "$type": { "const": "agent.update" }, - "triggered": { + "agents": { "type": "array", "items": { - "$ref": "#/definitions/WorkerUpdateAttempt" + "$ref": "#/definitions/AgentUpdateMeta" } }, - "failed": { - "type": "array", - "items": { - "$ref": "#/definitions/WorkerUpdateAttempt" + "errors": { + "type": "object", + "additionalProperties": { + "type": "string" } } }, @@ -4136,14 +4165,38 @@ "missed-messages" ] }, - "WorkerUpdateAttempt": { + "AgentDeletionMeta": { "type": "object", - "required": ["componentName", "targetRevision", "agentName"], + "required": ["componentName", "agentName"], + "properties": { + "componentName": { "type": "string" }, + "agentName": { "type": "string" } + }, + "additionalProperties": false + }, + "AgentUpdateMeta": { + "type": "object", + "required": ["componentName", "agentName", "fromRevision", "revision"], + "properties": { + "componentName": { "type": "string" }, + "agentName": { "type": "string" }, + "fromRevision": { "type": "integer", "minimum": 0 }, + "revision": { "type": "integer", "minimum": 0 }, + "fromVersion": { "type": "string" }, + "version": { "type": "string" } + }, + "additionalProperties": false + }, + "AgentRedeploymentMeta": { + "type": "object", + "required": ["componentName", "agentName", "fromRevision", "revision"], "properties": { "componentName": { "type": "string" }, - "targetRevision": { "type": "integer", "minimum": 0 }, "agentName": { "type": "string" }, - "error": { "type": "string" } + "fromRevision": { "type": "integer", "minimum": 0 }, + "revision": { "type": "integer", "minimum": 0 }, + "fromVersion": { "type": "string" }, + "version": { "type": "string" } }, "additionalProperties": false }, @@ -6677,6 +6730,10 @@ "type": "agent.delete", "rustType": "AgentDeleteResult" }, + { + "type": "agent.delete-all", + "rustType": "AgentDeleteAllResult" + }, { "type": "agent.file-contents", "rustType": "AgentFileContentsResult" diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 8cf200f88c..57100dcb5d 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -41,7 +41,9 @@ use crate::model::deploy::{ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; -use crate::model::text::action_result::AgentRedeployResult; +use crate::model::text::action_result::{ + AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, +}; use crate::model::text::component::{ ComponentGetView, ComponentListView, ComponentManifestTraceView, }; @@ -377,13 +379,9 @@ impl ComponentCommandHandler { update_results.extend(result); } - self.ctx.log_handler().log_output(update_results.clone())?; + self.ctx.log_handler().log_output(update_results)?; - if !update_results.failed.is_empty() { - bail!(NonSuccessfulExit) - } else { - Ok(()) - } + Ok(()) } pub async fn redeploy_workers_by_components( @@ -397,20 +395,33 @@ impl ComponentCommandHandler { log_action("Redeploying", "existing agents"); let _indent = LogIndent::new(); + // TODO: unlike updating, redeploy is short-circuiting, should we normalize? + let mut agents = Vec::new(); for component in components { - self.ctx + let redeployed = self + .ctx .worker_handler() .redeploy_component_workers(&component.component_name, &component.id) .await?; + let version = component.metadata.root_package_version().clone(); + for (agent_name, from_revision) in redeployed { + let from_version = self + .component_version_at(&component.id, from_revision) + .await; + agents.push(AgentRedeploymentMeta { + component_name: component.component_name.clone(), + agent_name, + from_revision, + revision: component.revision, + from_version, + version: version.clone(), + }); + } } - // TODO: unlike updating, redeploy is short-circuiting, should we normalize? self.ctx.log_handler().log_output(AgentRedeployResult { redeployed: true, - components: components - .iter() - .map(|component| component.component_name.clone()) - .collect(), + agents, })?; Ok(()) @@ -427,24 +438,35 @@ impl ComponentCommandHandler { // NOTE: for now we naively keep deleting in a loop until we do not find any more agents, // we do so to help a bit with pending invocations or currently running worker creations, // but this is not a 100% guarantee. + let mut agents = Vec::new(); let mut found_any = true; let mut first_round = true; while found_any { found_any = false; for component in components { - let deleted_count = self + let deleted = self .ctx .worker_handler() .delete_component_workers(&component.component_name, &component.id, first_round) .await?; - if deleted_count > 0 { + if !deleted.is_empty() { found_any = true; } + for agent_name in deleted { + agents.push(AgentDeletionMeta { + component_name: component.component_name.clone(), + agent_name, + }); + } } first_round = false; } - // TODO: json / yaml output? + self.ctx.log_handler().log_output(AgentDeleteAllResult { + deleted: true, + agents, + })?; + Ok(()) } @@ -1291,6 +1313,19 @@ impl ComponentCommandHandler { .await } + /// Best-effort, cached lookup of a component's user-facing release version string at a given + /// revision. Returns `None` if the revision can't be fetched or has no version set. + pub async fn component_version_at( + &self, + component_id: &ComponentId, + revision: ComponentRevision, + ) -> Option { + self.get_component_revision_by_id(component_id, revision) + .await + .ok() + .and_then(|component| component.metadata.root_package_version().clone()) + } + pub async fn get_component_revision_by_id( &self, component_id: &ComponentId, diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 44e2187620..024297afa5 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -30,7 +30,7 @@ use crate::log::{ log_warn_action, logln, }; use crate::model::component::ComponentNameMatchKind; -use crate::model::deploy::{TryUpdateAllWorkersResult, WorkerUpdateAttempt}; +use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersResult}; use crate::model::invoke_result_view::InvokeResultView; use crate::model::text::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, @@ -1304,7 +1304,26 @@ impl WorkerCommandHandler { } }; + let from_revision = self + .worker_metadata(component.id.0, &component.component_name, &agent_name) + .await? + .map(|metadata| metadata.component_revision) + .unwrap_or(target_revision); + let meta = AgentUpdateMeta { + component_name: component.component_name.clone(), + agent_name: agent_name.clone(), + from_revision, + revision: target_revision, + from_version: self + .ctx + .component_handler() + .component_version_at(&component.id, from_revision) + .await, + version: component.metadata.root_package_version().clone(), + }; + let mut update_results = TryUpdateAllWorkersResult::default(); + update_results.agents.push(meta); match self .update_worker( &component.component_name, @@ -1317,19 +1336,11 @@ impl WorkerCommandHandler { ) .await { - Ok(()) => update_results.triggered.push(WorkerUpdateAttempt { - component_name: component.component_name.clone(), - target_revision, - agent_name: agent_name.0.as_str().into(), - error: None, - }), + Ok(()) => {} Err(error) => { - update_results.failed.push(WorkerUpdateAttempt { - component_name: component.component_name.clone(), - target_revision, - agent_name: agent_name.0.as_str().into(), - error: Some(error.to_string()), - }); + update_results + .errors + .insert(agent_name.0.clone(), error.to_string()); self.ctx.log_handler().log_output(update_results)?; return Err(error); } @@ -1856,6 +1867,11 @@ impl WorkerCommandHandler { ); let _indent = LogIndent::new(); + let version = self + .ctx + .component_handler() + .component_version_at(component_id, target_revision) + .await; let mut update_results = TryUpdateAllWorkersResult::default(); for worker in &workers_to_update { let result = self @@ -1870,24 +1886,23 @@ impl WorkerCommandHandler { ) .await; - match result { - Ok(_) => { - update_results.triggered.push(WorkerUpdateAttempt { - component_name: component_name.clone(), - target_revision, - agent_name: worker.agent_id.agent_id.as_str().into(), - error: None, - }); - } - Err(error) => { - update_results.triggered.push(WorkerUpdateAttempt { - component_name: component_name.clone(), - target_revision, - agent_name: worker.agent_id.agent_id.as_str().into(), - error: Some(error.to_string()), - }); - } + if let Err(error) = &result { + update_results + .errors + .insert(worker.agent_id.agent_id.clone(), error.to_string()); } + update_results.agents.push(AgentUpdateMeta { + component_name: component_name.clone(), + agent_name: worker.agent_id.agent_id.as_str().into(), + from_revision: worker.component_revision, + revision: target_revision, + from_version: self + .ctx + .component_handler() + .component_version_at(component_id, worker.component_revision) + .await, + version: version.clone(), + }); } if await_update { @@ -2057,11 +2072,13 @@ impl WorkerCommandHandler { } } + /// Redeploys all agents of a component, returning each redeployed agent's id and the revision it + /// was running at before (its "from" revision). pub async fn redeploy_component_workers( &self, component_name: &ComponentName, component_id: &ComponentId, - ) -> anyhow::Result<()> { + ) -> anyhow::Result> { let (workers, _) = self .list_component_workers(component_name, component_id, None, None, None, None, false) .await?; @@ -2071,7 +2088,7 @@ impl WorkerCommandHandler { "Skipping", format!("redeploying agents for component {component_name}, no agent found"), ); - return Ok(()); + return Ok(Vec::new()); } log_action( @@ -2092,19 +2109,24 @@ impl WorkerCommandHandler { bail!(NonSuccessfulExit); } + let mut redeployed = Vec::with_capacity(workers.len()); for worker in workers { + let agent_name: RawAgentId = worker.agent_id.agent_id.as_str().into(); + let from_revision = worker.component_revision; self.redeploy_worker(component_name, worker).await?; + redeployed.push((agent_name, from_revision)); } - Ok(()) + Ok(redeployed) } + /// Deletes all agents of a component, returning the ids of the agents that were deleted. pub async fn delete_component_workers( &self, component_name: &ComponentName, component_id: &ComponentId, show_skip: bool, - ) -> anyhow::Result { + ) -> anyhow::Result> { let (workers, _) = self .list_component_workers(component_name, component_id, None, None, None, None, false) .await?; @@ -2116,7 +2138,7 @@ impl WorkerCommandHandler { format!("deleting agents for component {component_name}, no agent found"), ); } - return Ok(0); + return Ok(Vec::new()); } log_action( @@ -2137,11 +2159,13 @@ impl WorkerCommandHandler { bail!(NonSuccessfulExit); } + let mut deleted = Vec::with_capacity(workers.len()); for worker in &workers { self.delete_worker(component_name, worker).await?; + deleted.push(worker.agent_id.agent_id.as_str().into()); } - Ok(workers.len()) + Ok(deleted) } async fn redeploy_worker( diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index 98e0ddd9da..a5da08e61b 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -350,6 +350,11 @@ mod tests { arb_agent_cancel_invocation_result ), registry_entry!("AgentDeleteResult", "agent.delete", arb_agent_delete_result), + registry_entry!( + "AgentDeleteAllResult", + "agent.delete-all", + arb_agent_delete_all_result + ), registry_entry!( "AgentFileContentsResult", "agent.file-contents", @@ -3024,31 +3029,92 @@ mod tests { fn arb_agent_update_result() -> OutputDocumentStrategy { serialized_output( ( - proptest::collection::vec(arb_worker_update_attempt(), 0..5), - proptest::collection::vec(arb_worker_update_attempt(), 0..5), + proptest::collection::vec(arb_agent_update_meta(), 0..5), + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), ) - .prop_map(|(triggered, failed)| { - crate::model::deploy::TryUpdateAllWorkersResult { triggered, failed } + .prop_map(|(agents, errors)| { + crate::model::deploy::TryUpdateAllWorkersResult { agents, errors } }), ) } - fn arb_worker_update_attempt() -> BoxedStrategy { + /// Shared field generator for the two identical revision-transition metas + /// (`AgentUpdateMeta` / `AgentRedeploymentMeta`). + fn arb_agent_transition_fields() -> BoxedStrategy<( + golem_common::model::component::ComponentName, + crate::model::worker::RawAgentId, + golem_common::model::component::ComponentRevision, + golem_common::model::component::ComponentRevision, + Option, + Option, + )> { ( arb_small_string(), - arb_small_u64(), arb_small_string(), + arb_small_u64(), + arb_small_u64(), + proptest::option::of(arb_small_string()), proptest::option::of(arb_small_string()), ) - .prop_map(|(component_name, target_revision, agent_name, error)| { - crate::model::deploy::WorkerUpdateAttempt { - component_name: golem_common::model::component::ComponentName(component_name), - target_revision: golem_common::model::component::ComponentRevision::new( - target_revision, + .prop_map( + |(component_name, agent_name, from_revision, revision, from_version, version)| { + ( + golem_common::model::component::ComponentName(component_name), + crate::model::worker::RawAgentId(agent_name), + golem_common::model::component::ComponentRevision::new(from_revision) + .expect("generated revision should be valid"), + golem_common::model::component::ComponentRevision::new(revision) + .expect("generated revision should be valid"), + from_version, + version, ) - .expect("generated revision should be valid"), + }, + ) + .boxed() + } + + fn arb_agent_update_meta() -> BoxedStrategy { + arb_agent_transition_fields() + .prop_map( + |(component_name, agent_name, from_revision, revision, from_version, version)| { + crate::model::deploy::AgentUpdateMeta { + component_name, + agent_name, + from_revision, + revision, + from_version, + version, + } + }, + ) + .boxed() + } + + fn arb_agent_redeployment_meta() + -> BoxedStrategy { + arb_agent_transition_fields() + .prop_map( + |(component_name, agent_name, from_revision, revision, from_version, version)| { + crate::model::text::action_result::AgentRedeploymentMeta { + component_name, + agent_name, + from_revision, + revision, + from_version, + version, + } + }, + ) + .boxed() + } + + fn arb_agent_deletion_meta() + -> BoxedStrategy { + (arb_small_string(), arb_small_string()) + .prop_map(|(component_name, agent_name)| { + crate::model::text::action_result::AgentDeletionMeta { + component_name: golem_common::model::component::ComponentName(component_name), agent_name: crate::model::worker::RawAgentId(agent_name), - error, } }) .boxed() @@ -3586,20 +3652,26 @@ mod tests { ) } + fn arb_agent_delete_all_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + proptest::collection::vec(arb_agent_deletion_meta(), 0..5), + ) + .prop_map(|(deleted, agents)| { + crate::model::text::action_result::AgentDeleteAllResult { deleted, agents } + }), + ) + } + fn arb_agent_redeploy_result() -> OutputDocumentStrategy { serialized_output( ( any::(), - proptest::collection::vec(arb_small_string(), 0..5), + proptest::collection::vec(arb_agent_redeployment_meta(), 0..5), ) - .prop_map(|(redeployed, components)| { - crate::model::text::action_result::AgentRedeployResult { - redeployed, - components: components - .into_iter() - .map(golem_common::model::component::ComponentName) - .collect(), - } + .prop_map(|(redeployed, agents)| { + crate::model::text::action_result::AgentRedeployResult { redeployed, agents } }), ) } diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index fd99c7ffba..f83b861488 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -1288,25 +1288,29 @@ fn mask_sensitive_key_value_for_deploy_diff( #[derive(Clone, Default, PartialEq, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TryUpdateAllWorkersResult { - pub triggered: Vec, - pub failed: Vec, + pub agents: Vec, + /// Per-agent update errors, keyed by the (environment-unique) agent id. + pub errors: BTreeMap, } impl TryUpdateAllWorkersResult { pub fn extend(&mut self, other: TryUpdateAllWorkersResult) { - self.triggered.extend(other.triggered); - self.failed.extend(other.failed); + self.agents.extend(other.agents); + self.errors.extend(other.errors); } } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WorkerUpdateAttempt { +pub struct AgentUpdateMeta { pub component_name: ComponentName, - pub target_revision: ComponentRevision, pub agent_name: RawAgentId, + pub from_revision: ComponentRevision, + pub revision: ComponentRevision, #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, + pub from_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, } #[derive(Clone, Debug)] diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs index 11bbecfdd2..40b67ef67f 100644 --- a/cli/golem-cli/src/model/text/action_result.rs +++ b/cli/golem-cli/src/model/text/action_result.rs @@ -24,7 +24,8 @@ use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::{NoTextOutput, TextOutput}; -use golem_common::model::component::ComponentName; +use crate::model::worker::RawAgentId; +use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -138,7 +139,7 @@ impl StructuredOutput for AgentCancelInvocationResult { #[serde(rename_all = "camelCase")] pub struct AgentRedeployResult { pub redeployed: bool, - pub components: Vec, + pub agents: Vec, } impl NoTextOutput for AgentRedeployResult {} @@ -148,6 +149,40 @@ impl StructuredOutput for AgentRedeployResult { const KIND: &'static str = "agent.redeploy"; } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentRedeploymentMeta { + pub component_name: ComponentName, + pub agent_name: RawAgentId, + pub from_revision: ComponentRevision, + pub revision: ComponentRevision, + #[serde(skip_serializing_if = "Option::is_none")] + pub from_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDeleteAllResult { + pub deleted: bool, + pub agents: Vec, +} + +impl NoTextOutput for AgentDeleteAllResult {} +impl TextOutput for AgentDeleteAllResult {} + +impl StructuredOutput for AgentDeleteAllResult { + const KIND: &'static str = "agent.delete-all"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDeletionMeta { + pub component_name: ComponentName, + pub agent_name: RawAgentId, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentPluginToggleResult { From 7f64fb96497a8e009a68c06db42a7ad92273f297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 28 Jul 2026 23:18:15 +0200 Subject: [PATCH 19/70] unify the agent identifier as agentId in the output --- .../command-output.schema.json | 52 +++++++++---------- .../src/command_handler/component/mod.rs | 6 +-- .../src/command_handler/worker/mod.rs | 34 ++++++------ cli/golem-cli/src/model/cli_output.rs | 40 +++++++++----- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/text/action_result.rs | 20 +++---- cli/golem-cli/src/model/text/fmt.rs | 2 +- .../src/model/text/http_api_deployment.rs | 2 +- cli/golem-cli/src/model/text/worker.rs | 18 +++---- cli/golem-cli/src/model/worker.rs | 4 +- 10 files changed, 96 insertions(+), 84 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 07247fdeb7..c706db8240 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -1156,7 +1156,7 @@ "required": [ "$type", "canceled", - "agent", + "agentId", "idempotencyKey" ], "properties": { @@ -1166,7 +1166,7 @@ "canceled": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" }, "idempotencyKey": { @@ -1183,7 +1183,7 @@ "required": [ "$type", "deleted", - "agent" + "agentId" ], "properties": { "$type": { @@ -1192,7 +1192,7 @@ "deleted": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" } }, @@ -1232,7 +1232,7 @@ "required": [ "$type", "saved", - "agent", + "agentId", "path", "outputPath", "bytes" @@ -1244,7 +1244,7 @@ "saved": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" }, "path": { @@ -1313,7 +1313,7 @@ "required": [ "$type", "interrupted", - "agent" + "agentId" ], "properties": { "$type": { @@ -1322,7 +1322,7 @@ "interrupted": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" } }, @@ -1399,7 +1399,7 @@ "required": [ "$type", "componentName", - "agentName" + "agentId" ], "properties": { "$type": { @@ -1408,7 +1408,7 @@ "componentName": { "type": "string" }, - "agentName": { + "agentId": { "type": "string" } }, @@ -1446,7 +1446,7 @@ "required": [ "$type", "activated", - "agent", + "agentId", "plugin", "priority" ], @@ -1457,7 +1457,7 @@ "activated": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" }, "plugin": { @@ -1504,7 +1504,7 @@ "required": [ "$type", "resumed", - "agent" + "agentId" ], "properties": { "$type": { @@ -1513,7 +1513,7 @@ "resumed": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" } }, @@ -1527,7 +1527,7 @@ "required": [ "$type", "reverted", - "agent" + "agentId" ], "properties": { "$type": { @@ -1536,7 +1536,7 @@ "reverted": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" }, "lastOplogIndex": { @@ -1558,7 +1558,7 @@ "required": [ "$type", "simulated", - "agent" + "agentId" ], "properties": { "$type": { @@ -1567,7 +1567,7 @@ "simulated": { "type": "boolean" }, - "agent": { + "agentId": { "type": "string" } }, @@ -3604,7 +3604,7 @@ "type": "object", "required": [ "componentName", - "agentName", + "agentId", "createdBy", "environmentId", "env", @@ -3623,7 +3623,7 @@ ], "properties": { "componentName": { "type": "string" }, - "agentName": { "type": "string" }, + "agentId": { "type": "string" }, "createdBy": { "type": "string" }, "environmentId": { "type": "string" }, "env": { "type": "object", "additionalProperties": { "type": "string" } }, @@ -4167,19 +4167,19 @@ }, "AgentDeletionMeta": { "type": "object", - "required": ["componentName", "agentName"], + "required": ["componentName", "agentId"], "properties": { "componentName": { "type": "string" }, - "agentName": { "type": "string" } + "agentId": { "type": "string" } }, "additionalProperties": false }, "AgentUpdateMeta": { "type": "object", - "required": ["componentName", "agentName", "fromRevision", "revision"], + "required": ["componentName", "agentId", "fromRevision", "revision"], "properties": { "componentName": { "type": "string" }, - "agentName": { "type": "string" }, + "agentId": { "type": "string" }, "fromRevision": { "type": "integer", "minimum": 0 }, "revision": { "type": "integer", "minimum": 0 }, "fromVersion": { "type": "string" }, @@ -4189,10 +4189,10 @@ }, "AgentRedeploymentMeta": { "type": "object", - "required": ["componentName", "agentName", "fromRevision", "revision"], + "required": ["componentName", "agentId", "fromRevision", "revision"], "properties": { "componentName": { "type": "string" }, - "agentName": { "type": "string" }, + "agentId": { "type": "string" }, "fromRevision": { "type": "integer", "minimum": 0 }, "revision": { "type": "integer", "minimum": 0 }, "fromVersion": { "type": "string" }, diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 57100dcb5d..e630ccdc97 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -410,7 +410,7 @@ impl ComponentCommandHandler { .await; agents.push(AgentRedeploymentMeta { component_name: component.component_name.clone(), - agent_name, + agent_id: agent_name, from_revision, revision: component.revision, from_version, @@ -432,7 +432,7 @@ impl ComponentCommandHandler { return Ok(()); } - log_action("Deleting", "existing workers"); + log_action("Deleting", "existing agents"); let _indent = LogIndent::new(); // NOTE: for now we naively keep deleting in a loop until we do not find any more agents, @@ -455,7 +455,7 @@ impl ComponentCommandHandler { for agent_name in deleted { agents.push(AgentDeletionMeta { component_name: component.component_name.clone(), - agent_name, + agent_id: agent_name, }); } } diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 024297afa5..4eda5d5b21 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -324,7 +324,7 @@ impl WorkerCommandHandler { logln(""); self.ctx.log_handler().log_output(WorkerCreateView { component_name: agent_name_match.component_name, - agent_name: display_agent_name, + agent_id: display_agent_name, })?; Ok(()) @@ -680,7 +680,7 @@ impl WorkerCommandHandler { .log_handler() .log_output(AgentSimulateCrashResult { simulated: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), })?; Ok(()) @@ -807,7 +807,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentRevertResult { reverted: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), last_oplog_index, number_of_invocations, })?; @@ -854,7 +854,7 @@ impl WorkerCommandHandler { .log_handler() .log_output(AgentCancelInvocationResult { canceled, - agent: agent_name.0, + agent_id: agent_name.0, idempotency_key: idempotency_key.value, })?; @@ -1156,7 +1156,7 @@ impl WorkerCommandHandler { .with_secret_config_paths(secret_config_paths); let parsed = ParsedAgentId::parse(&raw_agent_name, &worker_component.metadata).ok(); - agent_view.agent_name = crate::agent_id_display::render_agent_id_or_raw( + agent_view.agent_id = crate::agent_id_display::render_agent_id_or_raw( parsed.as_ref(), &source_language, &raw_agent_name, @@ -1180,7 +1180,7 @@ impl WorkerCommandHandler { view.agents.sort_by(|a, b| { a.component_name .cmp(&b.component_name) - .then_with(|| a.agent_name.0.cmp(&b.agent_name.0)) + .then_with(|| a.agent_id.0.cmp(&b.agent_id.0)) }); } @@ -1223,7 +1223,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentInterruptResult { interrupted: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), })?; Ok(()) @@ -1250,7 +1250,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentResumeResult { resumed: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), })?; Ok(()) @@ -1311,7 +1311,7 @@ impl WorkerCommandHandler { .unwrap_or(target_revision); let meta = AgentUpdateMeta { component_name: component.component_name.clone(), - agent_name: agent_name.clone(), + agent_id: agent_name.clone(), from_revision, revision: target_revision, from_version: self @@ -1385,10 +1385,10 @@ impl WorkerCommandHandler { .with_defaults(defaults) .with_secret_config_paths(secret_config_paths) .with_source_language(agent_name_match.source_language.clone()); - metadata_view.agent_name = crate::agent_id_display::render_agent_id_or_raw( + metadata_view.agent_id = crate::agent_id_display::render_agent_id_or_raw( agent_name_match.parsed_agent_id.as_ref(), &agent_name_match.source_language, - &metadata_view.agent_name.0, + &metadata_view.agent_id.0, ) .into(); @@ -1420,7 +1420,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentDeleteResult { deleted: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), })?; Ok(()) @@ -1557,7 +1557,7 @@ impl WorkerCommandHandler { ); self.ctx.log_handler().log_output(AgentFileContentsResult { saved: false, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), path, output_path: output_path.into(), bytes: 0, @@ -1574,7 +1574,7 @@ impl WorkerCommandHandler { ); self.ctx.log_handler().log_output(AgentFileContentsResult { saved: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), path, output_path: output_path.into(), bytes: file_contents.len(), @@ -1634,7 +1634,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentPluginToggleResult { activated: true, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), plugin: plugin_name.clone(), priority: plugin_priority, })?; @@ -1685,7 +1685,7 @@ impl WorkerCommandHandler { self.ctx.log_handler().log_output(AgentPluginToggleResult { activated: false, - agent: agent_name.0.clone(), + agent_id: agent_name.0.clone(), plugin: plugin_name.clone(), priority: plugin_priority, })?; @@ -1893,7 +1893,7 @@ impl WorkerCommandHandler { } update_results.agents.push(AgentUpdateMeta { component_name: component_name.clone(), - agent_name: worker.agent_id.agent_id.as_str().into(), + agent_id: worker.agent_id.agent_id.as_str().into(), from_revision: worker.component_revision, revision: target_revision, from_version: self diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index a5da08e61b..558fdd6951 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -1016,7 +1016,7 @@ mod tests { fn sample_agent_metadata_view() -> crate::model::worker::AgentMetadataView { crate::model::worker::AgentMetadataView { component_name: golem_common::model::component::ComponentName("component".to_string()), - agent_name: crate::model::worker::RawAgentId("agent()".to_string()), + agent_id: crate::model::worker::RawAgentId("agent()".to_string()), created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), env: BTreeMap::new().into_iter().collect(), @@ -2931,7 +2931,7 @@ mod tests { (arb_small_string(), arb_small_string()).prop_map( |(component_name, agent_name)| crate::model::text::worker::WorkerCreateView { component_name: golem_common::model::component::ComponentName(component_name), - agent_name: crate::model::worker::RawAgentId(agent_name), + agent_id: crate::model::worker::RawAgentId(agent_name), }, ), ) @@ -3079,7 +3079,7 @@ mod tests { |(component_name, agent_name, from_revision, revision, from_version, version)| { crate::model::deploy::AgentUpdateMeta { component_name, - agent_name, + agent_id: agent_name, from_revision, revision, from_version, @@ -3097,7 +3097,7 @@ mod tests { |(component_name, agent_name, from_revision, revision, from_version, version)| { crate::model::text::action_result::AgentRedeploymentMeta { component_name, - agent_name, + agent_id: agent_name, from_revision, revision, from_version, @@ -3114,7 +3114,7 @@ mod tests { .prop_map(|(component_name, agent_name)| { crate::model::text::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), - agent_name: crate::model::worker::RawAgentId(agent_name), + agent_id: crate::model::worker::RawAgentId(agent_name), } }) .boxed() @@ -3175,7 +3175,7 @@ mod tests { crate::model::worker::AgentMetadataView { component_name: golem_common::model::component::ComponentName(component_name), - agent_name: crate::model::worker::RawAgentId(agent_name), + agent_id: crate::model::worker::RawAgentId(agent_name), created_by: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), ), @@ -3286,7 +3286,10 @@ mod tests { fn arb_agent_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, agent)| { - crate::model::text::action_result::AgentDeleteResult { deleted, agent } + crate::model::text::action_result::AgentDeleteResult { + deleted, + agent_id: agent, + } }), ) } @@ -3303,7 +3306,7 @@ mod tests { .prop_map(|(saved, agent, path, output_path, bytes)| { crate::model::text::action_result::AgentFileContentsResult { saved, - agent, + agent_id: agent, path, output_path: output_path.into(), bytes: bytes as usize, @@ -3315,7 +3318,10 @@ mod tests { fn arb_agent_interrupt_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(interrupted, agent)| { - crate::model::text::action_result::AgentInterruptResult { interrupted, agent } + crate::model::text::action_result::AgentInterruptResult { + interrupted, + agent_id: agent, + } }), ) } @@ -3323,7 +3329,10 @@ mod tests { fn arb_agent_resume_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(resumed, agent)| { - crate::model::text::action_result::AgentResumeResult { resumed, agent } + crate::model::text::action_result::AgentResumeResult { + resumed, + agent_id: agent, + } }), ) } @@ -3331,7 +3340,10 @@ mod tests { fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(simulated, agent)| { - crate::model::text::action_result::AgentSimulateCrashResult { simulated, agent } + crate::model::text::action_result::AgentSimulateCrashResult { + simulated, + agent_id: agent, + } }), ) } @@ -3644,7 +3656,7 @@ mod tests { |(canceled, agent, idempotency_key)| { crate::model::text::action_result::AgentCancelInvocationResult { canceled, - agent, + agent_id: agent, idempotency_key, } }, @@ -3688,7 +3700,7 @@ mod tests { |(reverted, agent, last_oplog_index, number_of_invocations)| { crate::model::text::action_result::AgentRevertResult { reverted, - agent, + agent_id: agent, last_oplog_index, number_of_invocations, } @@ -3708,7 +3720,7 @@ mod tests { .prop_map(|(activated, agent, plugin, priority)| { crate::model::text::action_result::AgentPluginToggleResult { activated, - agent, + agent_id: agent, plugin, priority, } diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index f83b861488..1f1691d3ed 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -1304,7 +1304,7 @@ impl TryUpdateAllWorkersResult { #[serde(rename_all = "camelCase")] pub struct AgentUpdateMeta { pub component_name: ComponentName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, pub from_revision: ComponentRevision, pub revision: ComponentRevision, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs index 40b67ef67f..aeaa2bcb48 100644 --- a/cli/golem-cli/src/model/text/action_result.rs +++ b/cli/golem-cli/src/model/text/action_result.rs @@ -33,7 +33,7 @@ use std::path::PathBuf; #[serde(rename_all = "camelCase")] pub struct AgentDeleteResult { pub deleted: bool, - pub agent: String, + pub agent_id: String, } impl NoTextOutput for AgentDeleteResult {} @@ -47,7 +47,7 @@ impl StructuredOutput for AgentDeleteResult { #[serde(rename_all = "camelCase")] pub struct AgentFileContentsResult { pub saved: bool, - pub agent: String, + pub agent_id: String, pub path: String, pub output_path: PathBuf, pub bytes: usize, @@ -64,7 +64,7 @@ impl StructuredOutput for AgentFileContentsResult { #[serde(rename_all = "camelCase")] pub struct AgentInterruptResult { pub interrupted: bool, - pub agent: String, + pub agent_id: String, } impl NoTextOutput for AgentInterruptResult {} @@ -78,7 +78,7 @@ impl StructuredOutput for AgentInterruptResult { #[serde(rename_all = "camelCase")] pub struct AgentResumeResult { pub resumed: bool, - pub agent: String, + pub agent_id: String, } impl NoTextOutput for AgentResumeResult {} @@ -92,7 +92,7 @@ impl StructuredOutput for AgentResumeResult { #[serde(rename_all = "camelCase")] pub struct AgentSimulateCrashResult { pub simulated: bool, - pub agent: String, + pub agent_id: String, } impl NoTextOutput for AgentSimulateCrashResult {} @@ -106,7 +106,7 @@ impl StructuredOutput for AgentSimulateCrashResult { #[serde(rename_all = "camelCase")] pub struct AgentRevertResult { pub reverted: bool, - pub agent: String, + pub agent_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub last_oplog_index: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -124,7 +124,7 @@ impl StructuredOutput for AgentRevertResult { #[serde(rename_all = "camelCase")] pub struct AgentCancelInvocationResult { pub canceled: bool, - pub agent: String, + pub agent_id: String, pub idempotency_key: String, } @@ -153,7 +153,7 @@ impl StructuredOutput for AgentRedeployResult { #[serde(rename_all = "camelCase")] pub struct AgentRedeploymentMeta { pub component_name: ComponentName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, pub from_revision: ComponentRevision, pub revision: ComponentRevision, #[serde(skip_serializing_if = "Option::is_none")] @@ -180,14 +180,14 @@ impl StructuredOutput for AgentDeleteAllResult { #[serde(rename_all = "camelCase")] pub struct AgentDeletionMeta { pub component_name: ComponentName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentPluginToggleResult { pub activated: bool, - pub agent: String, + pub agent_id: String, pub plugin: String, pub priority: i32, } diff --git a/cli/golem-cli/src/model/text/fmt.rs b/cli/golem-cli/src/model/text/fmt.rs index 07398bc4a3..17ab74fa86 100644 --- a/cli/golem-cli/src/model/text/fmt.rs +++ b/cli/golem-cli/src/model/text/fmt.rs @@ -1029,7 +1029,7 @@ mod tests { ) -> SelfFormattingTableSpec<'static> { let headers = vec![ Column::new("Component name").width_range(12, 28), - Column::new("Agent name"), + Column::new("Agent ID"), Column::new("Status").content_right(), ]; let rows = component_names diff --git a/cli/golem-cli/src/model/text/http_api_deployment.rs b/cli/golem-cli/src/model/text/http_api_deployment.rs index d543c134b5..547311fc16 100644 --- a/cli/golem-cli/src/model/text/http_api_deployment.rs +++ b/cli/golem-cli/src/model/text/http_api_deployment.rs @@ -69,7 +69,7 @@ fn http_api_deployment_fields(dep: &HttpApiDeployment) -> Vec<(String, String)> .fmt_field("Agents", &dep.agents, |agents| { let mut result = String::new(); for (agent_name, agent_options) in agents { - result.push_str(&format!("- Agent name: {}", agent_name)); + result.push_str(&format!("- Agent ID: {}", agent_name)); match &agent_options.security { None => {} Some(HttpApiDeploymentAgentSecurity::SecurityScheme(inner)) => { diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index 507fbb8378..63be2e64c7 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -49,7 +49,7 @@ use std::fmt::Write; #[serde(rename_all = "camelCase")] pub struct WorkerCreateView { pub component_name: ComponentName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, } impl Masked for WorkerCreateView {} @@ -58,7 +58,7 @@ impl MessageWithFields for WorkerCreateView { fn message(&self) -> String { format!( "Created new agent {}", - format_message_highlight(&self.agent_name) + format_message_highlight(&self.agent_id) ) } @@ -67,7 +67,7 @@ impl MessageWithFields for WorkerCreateView { fields .fmt_field("Component name", &self.component_name, format_id) - .fmt_field("Agent name", &self.agent_name, |agent_name| { + .fmt_field("Agent ID", &self.agent_id, |agent_name| { format_agent_id_in( &agent_name.0, colored::control::SHOULD_COLORIZE.should_colorize(), @@ -124,7 +124,7 @@ impl MessageWithFields for WorkerGetView { fn message(&self) -> String { format!( "Got metadata for agent {}", - format_message_highlight(&self.metadata.agent_name) + format_message_highlight(&self.metadata.agent_id) ) } @@ -184,7 +184,7 @@ impl MessageWithFields for WorkerGetView { &self.metadata.component_revision, format_id, ) - .fmt_field("Agent name", &self.metadata.agent_name, |agent_name| { + .fmt_field("Agent ID", &self.metadata.agent_id, |agent_name| { format_agent_id_in( &agent_name.0, colored::control::SHOULD_COLORIZE.should_colorize(), @@ -353,7 +353,7 @@ impl AgentsMetadataResponseView { let headers = vec![ Column::new("Component name") .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), - Column::new("Agent name"), + Column::new("Agent ID"), Column::new("Revision").content_right(), Column::new("Status").content_right(), Column::new("Pending").content_right(), @@ -370,7 +370,7 @@ impl AgentsMetadataResponseView { .map(|agent| { vec![ TableCell::new(agent.component_name.to_string()), - TableCell::new(agent.agent_name.0.clone()), + TableCell::new(agent.agent_id.0.clone()), TableCell::new(agent.component_revision.to_string()).right(), TableCell::new(agent.status.to_string()) .right() @@ -1434,7 +1434,7 @@ mod tests { fn agent_metadata(agent_name: &str) -> AgentMetadataView { AgentMetadataView { component_name: ComponentName("shop:cart".to_string()), - agent_name: RawAgentId(agent_name.to_string()), + agent_id: RawAgentId(agent_name.to_string()), created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), env: HashMap::new(), @@ -1488,7 +1488,7 @@ mod tests { fn table_measures_cells_with_ansi_escapes_stripped() { let mut table = ComfyTable::new(); table - .set_header(vec!["Agent name"]) + .set_header(vec!["Agent ID"]) .add_row(vec![Cell::new("\u{1b}[32mColored(\"id\")\u{1b}[0m")]); assert_rows_aligned(&table.to_string()); diff --git a/cli/golem-cli/src/model/worker.rs b/cli/golem-cli/src/model/worker.rs index e3a9ae3092..9ecd63b54b 100644 --- a/cli/golem-cli/src/model/worker.rs +++ b/cli/golem-cli/src/model/worker.rs @@ -119,7 +119,7 @@ impl Display for AgentListMode { #[serde(rename_all = "camelCase")] pub struct AgentMetadataView { pub component_name: ComponentName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, pub created_by: AccountId, pub environment_id: EnvironmentId, pub env: HashMap, @@ -147,7 +147,7 @@ impl From for AgentMetadataView { fn from(value: AgentMetadata) -> Self { AgentMetadataView { component_name: value.component_name, - agent_name: value.agent_id.agent_id.into(), + agent_id: value.agent_id.agent_id.into(), created_by: value.created_by, environment_id: value.environment_id, env: value.env, From 267a0ba00b445a74fe17f4df4d59e77651bfadbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 28 Jul 2026 23:18:15 +0200 Subject: [PATCH 20/70] rename agent_name to agent_id across the cli internals --- cli/golem-cli/src/app/context.rs | 8 +- cli/golem-cli/src/command.rs | 10 +- cli/golem-cli/src/command_handler/card.rs | 8 +- .../src/command_handler/component/mod.rs | 24 +- .../src/command_handler/component/staging.rs | 26 +- .../src/command_handler/interactive.rs | 28 +- .../src/command_handler/partial_match.rs | 34 +- .../src/command_handler/worker/mod.rs | 576 ++++++++---------- .../src/command_handler/worker/stream.rs | 10 +- cli/golem-cli/src/model/app.rs | 6 +- cli/golem-cli/src/model/cli_output.rs | 34 +- cli/golem-cli/src/model/component.rs | 10 +- cli/golem-cli/src/model/text/diff.rs | 16 +- cli/golem-cli/src/model/text/help.rs | 22 +- .../src/model/text/http_api_deployment.rs | 4 +- cli/golem-cli/src/model/text/worker.rs | 34 +- cli/golem-cli/src/model/worker.rs | 12 +- 17 files changed, 408 insertions(+), 454 deletions(-) diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 48be3806f8..1f02be0b7d 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -621,8 +621,8 @@ impl ApplicationContext { for (site, deployment) in http_api_deployments { logln(format!(" {}", site.to_string().log_color_highlight(),)); - for agent_name in deployment.value.agents.keys() { - logln(format!(" {}", agent_name.as_str().log_color_highlight(),)); + for agent_id in deployment.value.agents.keys() { + logln(format!(" {}", agent_id.as_str().log_color_highlight(),)); } } logln(""); @@ -650,8 +650,8 @@ impl ApplicationContext { for (site, deployment) in mcp_deployments { logln(format!(" {}", site.to_string().log_color_highlight(),)); - for agent_name in deployment.value.agents.keys() { - logln(format!(" {}", agent_name.as_str().log_color_highlight(),)); + for agent_id in deployment.value.agents.keys() { + logln(format!(" {}", agent_id.as_str().log_color_highlight(),)); } } logln(""); diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 09b0732456..f7fb757708 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -532,7 +532,7 @@ impl GolemCliCommand { missing_positional_arg: "function_name", to_partial_match: |args| { GolemCliCommandPartialMatch::AgentInvokeMissingFunctionName { - agent_name: args[0].clone().into(), + agent_id: args[0].clone().into(), } }, }, @@ -634,7 +634,7 @@ pub enum GolemCliCommandPartialMatch { ComponentHelp, ComponentMissingSubcommandHelp, AgentHelp, - AgentInvokeMissingFunctionName { agent_name: RawAgentId }, + AgentInvokeMissingFunctionName { agent_id: RawAgentId }, AgentInvokeMissingAgentName, ProfileSwitchMissingProfileName, } @@ -1541,7 +1541,7 @@ pub mod worker { #[command(after_help = crate::command_examples::AGENT_FILES)] Files { #[command(flatten)] - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, /// Absolute path inside the agent's guest filesystem (e.g. `/`, /// `/data`). Always starts with `/`. #[arg(default_value = "/")] @@ -1556,7 +1556,7 @@ pub mod worker { #[command(after_help = crate::command_examples::AGENT_FILE_CONTENTS)] FileContents { #[command(flatten)] - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, /// Absolute path inside the agent's guest filesystem (e.g. /// `/data/state.json`). Always starts with `/`. path: String, @@ -2438,7 +2438,7 @@ pub mod server { /// Use deterministic agent filesystem directories rooted at the given /// path instead of random temp directories. The directory layout is: - /// //// + /// //// #[clap(long)] pub agent_filesystem_root: Option, } diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index dda3c54b3c..805916f17d 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -141,9 +141,9 @@ impl CardCommandHandler { self.ctx.silence_app_context_init().await; let worker_handler = WorkerCommandHandler::new(self.ctx.clone()); - let agent_name_match = worker_handler.match_agent_name(agent).await?; - let (component, agent_name) = worker_handler - .component_by_agent_name_match(&agent_name_match) + let agent_id_match = worker_handler.match_agent_id(agent).await?; + let (component, agent_id) = worker_handler + .component_by_agent_id_match(&agent_id_match) .await?; let cards = self @@ -151,7 +151,7 @@ impl CardCommandHandler { .golem_clients() .await? .worker - .get_agent_wallet(&component.id.0, &agent_name.0) + .get_agent_wallet(&component.id.0, &agent_id.0) .await .map_service_error()?; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index e630ccdc97..442746e9de 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -404,13 +404,13 @@ impl ComponentCommandHandler { .redeploy_component_workers(&component.component_name, &component.id) .await?; let version = component.metadata.root_package_version().clone(); - for (agent_name, from_revision) in redeployed { + for (agent_id, from_revision) in redeployed { let from_version = self .component_version_at(&component.id, from_revision) .await; agents.push(AgentRedeploymentMeta { component_name: component.component_name.clone(), - agent_id: agent_name, + agent_id, from_revision, revision: component.revision, from_version, @@ -452,10 +452,10 @@ impl ComponentCommandHandler { if !deleted.is_empty() { found_any = true; } - for agent_name in deleted { + for agent_id in deleted { agents.push(AgentDeletionMeta { component_name: component.component_name.clone(), - agent_id: agent_name, + agent_id, }); } } @@ -758,10 +758,10 @@ impl ComponentCommandHandler { match (component, component_revision_selection) { (Some(component), Some(component_revision_selection)) => { let revision = match component_revision_selection { - ComponentRevisionSelection::ByAgentName(agent_name) => self + ComponentRevisionSelection::ByAgentId(agent_id) => self .ctx .worker_handler() - .worker_metadata(component.id.0, &component.component_name, agent_name) + .worker_metadata(component.id.0, &component.component_name, agent_id) .await? .map(|worker_metadata| worker_metadata.component_revision), ComponentRevisionSelection::ByExplicitRevision(revision) => Some(revision), @@ -792,7 +792,7 @@ impl ComponentCommandHandler { ( app.component_names().into_iter().collect::>(), app.application() - .agent_names() + .agent_ids() .cloned() .collect::>(), ) @@ -817,16 +817,16 @@ impl ComponentCommandHandler { let unknown_declared_agents: Vec = declared_agents .into_iter() .filter(|declared_agent| !exported_agents.contains_key(declared_agent)) - .map(|agent_name| agent_name.0) + .map(|agent_id| agent_id.0) .collect(); if !unknown_declared_agents.is_empty() { // TODO: atl: validate against resolved ATL agent set after template/preset expansion, // not only directly declared manifest agents. - for agent_name in &unknown_declared_agents { + for agent_id in &unknown_declared_agents { log_error(format!( "Manifest declares agent {} but it is not exported by any component.", - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); } @@ -932,12 +932,12 @@ impl ComponentCommandHandler { } if !unused_config_by_agent.is_empty() { - for (agent_name, unused_keys) in &unused_config_by_agent { + for (agent_id, unused_keys) in &unused_config_by_agent { log_warn_action( "Ignoring unused config keys", format!( "for agent {}: {}", - agent_name.0.log_color_highlight(), + agent_id.0.log_color_highlight(), unused_keys.join(", ") ), ); diff --git a/cli/golem-cli/src/command_handler/component/staging.rs b/cli/golem-cli/src/command_handler/component/staging.rs index 2d06910a56..f7a01afefa 100644 --- a/cli/golem-cli/src/command_handler/component/staging.rs +++ b/cli/golem-cli/src/command_handler/component/staging.rs @@ -366,13 +366,13 @@ impl<'a> ComponentStager<'a> { // Compute permissions-only updates per agent type let mut file_permission_updates_per_agent = BTreeMap::new(); for (agent_type_str, agent_diff) in self.diff.file_changes_per_agent() { - let agent_name = golem_common::model::agent::AgentTypeName(agent_type_str.to_string()); + let agent_id = golem_common::model::agent::AgentTypeName(agent_type_str.to_string()); let manifest_files = match self .component_deploy_properties .agent_type_configs - .get(&agent_name) + .get(&agent_id) { - Some(_) => self.manifest_files_for_agent(&agent_name).await?, + Some(_) => self.manifest_files_for_agent(&agent_id).await?, None => Vec::new(), }; let manifest_files: std::collections::HashMap<_, _> = manifest_files @@ -400,7 +400,7 @@ impl<'a> ComponentStager<'a> { } } if !perm_updates.is_empty() { - file_permission_updates_per_agent.insert(agent_name, perm_updates); + file_permission_updates_per_agent.insert(agent_id, perm_updates); } } @@ -824,7 +824,7 @@ mod tests { use golem_common::model::diff::Hash; use test_r::test; - fn agent_name() -> AgentTypeName { + fn agent_id() -> AgentTypeName { AgentTypeName("Cart".to_string()) } @@ -861,7 +861,7 @@ mod tests { #[test] fn value_diff_without_plugin_changes_emits_no_plugin_actions() { - let agent_name = agent_name(); + let agent_id = agent_id(); let agent_diff = empty_agent_diff(); let agent_change = diff::BTreeMapDiffValue::Update(diff::DiffForHashOf::ValueDiff { diff: agent_diff }); @@ -869,7 +869,7 @@ mod tests { let plugins = vec![plugin_installation(grant_id)]; let updates = ComponentStager::plugin_updates_for_agent_change( - &agent_name, + &agent_id, Some(&agent_change), &plugins, ) @@ -880,12 +880,12 @@ mod tests { #[test] fn create_provision_config_installs_manifest_plugins() { - let agent_name = agent_name(); + let agent_id = agent_id(); let grant_id = uuid::Uuid::from_u128(1); let plugins = vec![plugin_installation(grant_id)]; let updates = ComponentStager::plugin_updates_for_agent_change( - &agent_name, + &agent_id, Some(&diff::BTreeMapDiffValue::Create), &plugins, ) @@ -901,14 +901,14 @@ mod tests { #[test] fn plugin_hash_diff_error_names_plugin_action_context() { - let agent_name = agent_name(); + let agent_id = agent_id(); let agent_change = diff::BTreeMapDiffValue::Update(diff::DiffForHashOf::HashDiff { new_hash: Hash::empty(), current_hash: Hash::new(blake3::hash(b"current")), }); let err = - ComponentStager::plugin_updates_for_agent_change(&agent_name, Some(&agent_change), &[]) + ComponentStager::plugin_updates_for_agent_change(&agent_id, Some(&agent_change), &[]) .unwrap_err(); assert!( @@ -920,7 +920,7 @@ mod tests { #[test] fn plugin_diff_emits_targeted_actions() { - let agent_name = agent_name(); + let agent_id = agent_id(); let install_grant_id = uuid::Uuid::from_u128(1); let uninstall_grant_id = uuid::Uuid::from_u128(2); let update_grant_id = uuid::Uuid::from_u128(3); @@ -947,7 +947,7 @@ mod tests { ]; let updates = ComponentStager::plugin_updates_for_agent_change( - &agent_name, + &agent_id, Some(&agent_change), &plugins, ) diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index 214eb2767a..d63fe0250c 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -32,7 +32,9 @@ use golem_common::model::environment::EnvironmentName; use indoc::formatdoc; use inquire::error::InquireResult; use inquire::validator::{ErrorMessage, Validation}; -use inquire::{Confirm, CustomType, InquireError, MultiSelect, Password, PasswordDisplayMode, Select, Text}; +use inquire::{ + Confirm, CustomType, InquireError, MultiSelect, Password, PasswordDisplayMode, Select, Text, +}; use itertools::Itertools; use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; @@ -135,7 +137,10 @@ impl InteractiveHandler { ) -> anyhow::Result { self.confirm( true, - format!("Continue with staging step ({operation} {})?", name.as_ref()), + format!( + "Continue with staging step ({operation} {})?", + name.as_ref() + ), None, ) } @@ -285,21 +290,21 @@ impl InteractiveHandler { pub fn confirm_update_to_current( &self, component_name: &ComponentName, - agent_name: &RawAgentId, + agent_id: &RawAgentId, parsed_agent_id: Option<&golem_common::model::agent::ParsedAgentId>, source_language: &crate::agent_id_display::SourceLanguage, target_revision: ComponentRevision, ) -> anyhow::Result { - let rendered_agent_name = crate::agent_id_display::render_agent_id_or_raw( + let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( parsed_agent_id, source_language, - &agent_name.0, + &agent_id.0, ); self.confirm( true, format!("Agent {}/{} will be updated to the current component revision: {}. Do you want to continue?", component_name.0.log_color_highlight(), - rendered_agent_name.log_color_highlight(), + rendered_agent_id.log_color_highlight(), target_revision.to_string().log_color_highlight() ), None, @@ -400,11 +405,12 @@ impl InteractiveHandler { .with_starting_cursor(2) .prompt()?; - let static_token = Password::new("Static token for authentication (leave empty for OAuth2):") - .with_display_mode(PasswordDisplayMode::Masked) - .without_confirmation() - .with_help_message("Mainly for testing or custom servers") - .prompt()?; + let static_token = + Password::new("Static token for authentication (leave empty for OAuth2):") + .with_display_mode(PasswordDisplayMode::Masked) + .without_confirmation() + .with_help_message("Mainly for testing or custom servers") + .prompt()?; let auth = if static_token.is_empty() { AuthenticationConfig::empty_oauth2() diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index 7c6b04268e..e9d41e38ec 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -131,22 +131,18 @@ impl ErrorHandler { Ok(()) } - GolemCliCommandPartialMatch::AgentInvokeMissingFunctionName { agent_name } => { + GolemCliCommandPartialMatch::AgentInvokeMissingFunctionName { agent_id } => { self.ctx.silence_app_context_init().await; logln(""); log_action( "Checking", - format!("provided agent ID: {}", agent_name.0.log_color_highlight()), + format!("provided agent ID: {}", agent_id.0.log_color_highlight()), ); - let agent_name_match = { + let agent_id_match = { let _indent = DecoratedIndent::new_primary(Format::Text); - let agent_name_match = self - .ctx - .worker_handler() - .match_agent_name(agent_name) - .await?; + let agent_id_match = self.ctx.worker_handler().match_agent_id(agent_id).await?; - let environment_formatted = match agent_name_match.environment_reference() { + let environment_formatted = match agent_id_match.environment_reference() { Some(env) => { format!(" environment: {} /", env.to_string().log_color_highlight()) } @@ -157,9 +153,9 @@ impl ErrorHandler { "[{}]{} component: {} / agent: {}, {}", "ok".green(), environment_formatted, - agent_name_match.component_name.0.log_color_highlight(), - agent_name_match.agent_name.0.log_color_highlight(), - match agent_name_match.component_name_match_kind { + agent_id_match.component_name.0.log_color_highlight(), + agent_id_match.agent_id.0.log_color_highlight(), + match agent_id_match.component_name_match_kind { ComponentNameMatchKind::AppCurrentDir => "component was selected based on current dir", ComponentNameMatchKind::App => @@ -167,29 +163,29 @@ impl ErrorHandler { ComponentNameMatchKind::Unknown => "", } )); - agent_name_match + agent_id_match }; logln(""); if let Ok(Some(component)) = self .ctx .component_handler() .resolve_component( - &agent_name_match.environment, - &agent_name_match.component_name, - Some((&agent_name_match.agent_name).into()), + &agent_id_match.environment, + &agent_id_match.component_name, + Some((&agent_id_match.agent_id).into()), ) .await { - let canonical_agent_name = self + let canonical_agent_id = self .ctx .worker_handler() - .try_recanonicalize_agent_name(&agent_name_match.agent_name, &component); + .try_recanonicalize_agent_id(&agent_id_match.agent_id, &component); let agent_id = self .ctx .worker_handler() .validate_worker_and_function_names( &component, - &canonical_agent_name, + &canonical_agent_id, None, )?; diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 4eda5d5b21..8547faa8a7 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -43,7 +43,7 @@ use crate::model::text::help::{ }; use crate::model::text::worker::{ AgentOplogEntryView, FileNodeView, WorkerCreateView, WorkerFilesView, WorkerGetView, - format_agent_name_match, format_timestamp, + format_agent_id_match, format_timestamp, }; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; @@ -54,7 +54,7 @@ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::worker::{ - AgentListMode, AgentMetadata, AgentMetadataView, AgentNameMatch, AgentUpdateMode, + AgentIdMatch, AgentListMode, AgentMetadata, AgentMetadataView, AgentUpdateMode, AgentsMetadataResponseView, RawAgentId, }; use golem_client::api::{AgentClient, ComponentClient, WorkerClient}; @@ -113,12 +113,12 @@ impl WorkerCommandHandler { Box::pin(async move { match subcommand { AgentSubcommand::New { - agent_id: agent_name, + agent_id, env, config, - } => self.cmd_new(agent_name, env, config).await, + } => self.cmd_new(agent_id, env, config).await, AgentSubcommand::Invoke { - agent_id: agent_name, + agent_id, function_name, arguments, trigger, @@ -129,7 +129,7 @@ impl WorkerCommandHandler { schedule_at, } => { self.cmd_invoke( - agent_name, + agent_id, &function_name, arguments, trigger, @@ -141,12 +141,8 @@ impl WorkerCommandHandler { ) .await } - AgentSubcommand::Get { - agent_id: agent_name, - } => self.cmd_get(agent_name).await, - AgentSubcommand::Delete { - agent_id: agent_name, - } => self.cmd_delete(agent_name).await, + AgentSubcommand::Get { agent_id } => self.cmd_get(agent_id).await, + AgentSubcommand::Delete { agent_id } => self.cmd_delete(agent_id).await, AgentSubcommand::List { agent_type_name, component_name, @@ -170,9 +166,9 @@ impl WorkerCommandHandler { .await } AgentSubcommand::Stream { - agent_id: agent_name, + agent_id, stream_args, - } => self.cmd_stream(agent_name, stream_args).await, + } => self.cmd_stream(agent_id, stream_args).await, AgentSubcommand::ReplStream { agent_type_name, parameters, @@ -189,18 +185,16 @@ impl WorkerCommandHandler { ) .await } - AgentSubcommand::Interrupt { - agent_id: agent_name, - } => self.cmd_interrupt(agent_name).await, + AgentSubcommand::Interrupt { agent_id } => self.cmd_interrupt(agent_id).await, AgentSubcommand::Update { - agent_id: agent_name, + agent_id, mode, target_revision, r#await, disable_wakeup, } => { self.cmd_update( - agent_name, + agent_id, mode.unwrap_or(AgentUpdateMode::Automatic), target_revision, r#await, @@ -208,54 +202,47 @@ impl WorkerCommandHandler { ) .await } - AgentSubcommand::Resume { - agent_id: agent_name, - } => self.cmd_resume(agent_name).await, - AgentSubcommand::SimulateCrash { - agent_id: agent_name, - } => self.cmd_simulate_crash(agent_name).await, + AgentSubcommand::Resume { agent_id } => self.cmd_resume(agent_id).await, + AgentSubcommand::SimulateCrash { agent_id } => { + self.cmd_simulate_crash(agent_id).await + } AgentSubcommand::Oplog { - agent_id: agent_name, + agent_id, from, query, - } => self.cmd_oplog(agent_name, from, query).await, + } => self.cmd_oplog(agent_id, from, query).await, AgentSubcommand::Revert { - agent_id: agent_name, + agent_id, last_oplog_index, number_of_invocations, } => { - self.cmd_revert(agent_name, last_oplog_index, number_of_invocations) + self.cmd_revert(agent_id, last_oplog_index, number_of_invocations) .await } AgentSubcommand::CancelInvocation { - agent_id: agent_name, + agent_id, idempotency_key, - } => { - self.cmd_cancel_invocation(agent_name, idempotency_key) - .await - } - AgentSubcommand::Files { agent_name, path } => { - self.cmd_files(agent_name, path).await - } + } => self.cmd_cancel_invocation(agent_id, idempotency_key).await, + AgentSubcommand::Files { agent_id, path } => self.cmd_files(agent_id, path).await, AgentSubcommand::FileContents { - agent_name, + agent_id, path, output, - } => self.cmd_file_contents(agent_name, path, output).await, + } => self.cmd_file_contents(agent_id, path, output).await, AgentSubcommand::ActivatePlugin { - agent_id: agent_name, + agent_id, plugin_name, plugin_priority, } => { - self.cmd_activate_plugin(agent_name, plugin_name, plugin_priority) + self.cmd_activate_plugin(agent_id, plugin_name, plugin_priority) .await } AgentSubcommand::DeactivatePlugin { - agent_id: agent_name, + agent_id, plugin_name, plugin_priority, } => { - self.cmd_deactivate_plugin(agent_name, plugin_name, plugin_priority) + self.cmd_deactivate_plugin(agent_id, plugin_name, plugin_priority) .await } } @@ -264,67 +251,66 @@ impl WorkerCommandHandler { async fn cmd_new( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, env: Vec<(String, String)>, config: Vec, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name = agent_name.agent_id; - let mut agent_name_match = self.match_agent_name(agent_name).await?; + let agent_id = agent_id.agent_id; + let mut agent_id_match = self.match_agent_id(agent_id).await?; let component = self .ctx .component_handler() .component_by_name_with_auto_deploy( - &agent_name_match.environment, - agent_name_match.component_name_match_kind, - &agent_name_match.component_name, - Some((&agent_name_match.agent_name).into()), + &agent_id_match.environment, + agent_id_match.component_name_match_kind, + &agent_id_match.component_name, + Some((&agent_id_match.agent_id).into()), None, None, false, ) .await?; - let agent_name = agent_name_match.agent_name.clone(); - let agent_name = - match self.validate_worker_and_function_names(&component, &agent_name, None)? { - Some((agent_id, agent_type)) => { - // `normalize_public_agent_id` may auto-generate a phantom - // UUID for ephemeral agents, changing the canonical form. - let normalized = normalize_public_agent_id(&agent_id, &agent_type)?; - let canonical: RawAgentId = normalized.to_string().into(); - agent_name_match.agent_name = canonical.clone(); - agent_name_match.parsed_agent_id = Some(normalized); - canonical - } - None => agent_name, - }; + let agent_id = agent_id_match.agent_id.clone(); + let agent_id = match self.validate_worker_and_function_names(&component, &agent_id, None)? { + Some((agent_id, agent_type)) => { + // `normalize_public_agent_id` may auto-generate a phantom + // UUID for ephemeral agents, changing the canonical form. + let normalized = normalize_public_agent_id(&agent_id, &agent_type)?; + let canonical: RawAgentId = normalized.to_string().into(); + agent_id_match.agent_id = canonical.clone(); + agent_id_match.parsed_agent_id = Some(normalized); + canonical + } + None => agent_id, + }; log_action( "Creating", - format!("new agent {}", format_agent_name_match(&agent_name_match)), + format!("new agent {}", format_agent_id_match(&agent_id_match)), ); self.new_worker( component.id.0, - agent_name.0.clone(), + agent_id.0.clone(), env.into_iter().collect(), config, ) .await?; - let display_agent_name: RawAgentId = crate::agent_id_display::render_agent_id_or_raw( - agent_name_match.parsed_agent_id.as_ref(), - &agent_name_match.source_language, - &agent_name.0, + let display_agent_id: RawAgentId = crate::agent_id_display::render_agent_id_or_raw( + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, + &agent_id.0, ) .into(); logln(""); self.ctx.log_handler().log_output(WorkerCreateView { - component_name: agent_name_match.component_name, - agent_id: display_agent_name, + component_name: agent_id_match.component_name, + agent_id: display_agent_id, })?; Ok(()) @@ -332,7 +318,7 @@ impl WorkerCommandHandler { async fn cmd_invoke( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, function_name: &AgentFunctionName, arguments: Vec, trigger: bool, @@ -371,16 +357,16 @@ impl WorkerCommandHandler { None => new_idempotency_key(), }; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; let component = self .ctx .component_handler() .component_by_name_with_auto_deploy( - &agent_name_match.environment, - agent_name_match.component_name_match_kind, - &agent_name_match.component_name, - Some((&agent_name_match.agent_name).into()), + &agent_id_match.environment, + agent_id_match.component_name_match_kind, + &agent_id_match.component_name, + Some((&agent_id_match.agent_id).into()), post_deploy_args.as_ref(), None, false, @@ -388,12 +374,9 @@ impl WorkerCommandHandler { .await?; // First, validate without the function name. The agent name was - // already canonicalized when the `AgentNameMatch` was constructed. - let agent_id_and_type = self.validate_worker_and_function_names( - &component, - &agent_name_match.agent_name, - None, - )?; + // already canonicalized when the `AgentIdMatch` was constructed. + let agent_id_and_type = + self.validate_worker_and_function_names(&component, &agent_id_match.agent_id, None)?; let (agent_id, agent_type) = agent_id_and_type.ok_or_else(|| anyhow!("Agent invoke requires an agent component"))?; @@ -456,9 +439,9 @@ impl WorkerCommandHandler { }, }; - // Update agent_name with normalized agent id (and keep the parsed form + // Update agent_id with normalized agent id (and keep the parsed form // for language-specific display). - let agent_name_match = agent_name_match + let agent_id_match = agent_id_match .with_canonical_and_parsed(agent_id.to_string().into(), Some(agent_id.clone())); let mode = if trigger { @@ -466,7 +449,7 @@ impl WorkerCommandHandler { "Triggering", format!( "invocation for agent {}/{}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), method_name.log_color_highlight() ), ); @@ -476,7 +459,7 @@ impl WorkerCommandHandler { "Invoking", format!( "agent {}/{} ", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), method_name.log_color_highlight() ), ); @@ -515,7 +498,7 @@ impl WorkerCommandHandler { None }; - let environment = &agent_name_match.environment; + let environment = &agent_id_match.environment; let request = AgentInvocationRequest { app_name: environment.application_name.to_string(), @@ -566,26 +549,24 @@ impl WorkerCommandHandler { async fn cmd_stream( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, stream_args: StreamArgs, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Connecting", - format!("to agent {}", format_agent_name_match(&agent_name_match)), + format!("to agent {}", format_agent_id_match(&agent_id_match)), ); let connection = WorkerConnection::new( self.ctx.worker_service_url().clone(), self.ctx.auth_token().await?, &component.id, - agent_name.0.clone(), + agent_id.0.clone(), stream_args.into(), self.ctx.allow_insecure(), self.ctx.format(), @@ -637,13 +618,13 @@ impl WorkerCommandHandler { phantom_id, &idempotency_key, )?; - let agent_name = RawAgentId(agent_id.to_string()); + let agent_id = RawAgentId(agent_id.to_string()); let connection = WorkerConnection::new( self.ctx.worker_service_url().clone(), self.ctx.auth_token().await?, &agent_type.implemented_by.component_id, - agent_name.0.clone(), + agent_id.0.clone(), stream_args.into(), self.ctx.allow_insecure(), self.ctx.format(), @@ -657,30 +638,28 @@ impl WorkerCommandHandler { Ok(()) } - async fn cmd_simulate_crash(&self, agent_name: AgentIdArgs) -> anyhow::Result<()> { + async fn cmd_simulate_crash(&self, agent_id: AgentIdArgs) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Simulating crash", - format!("for agent {}", format_agent_name_match(&agent_name_match)), + format!("for agent {}", format_agent_id_match(&agent_id_match)), ); - self.interrupt_worker(&component, &agent_name, true).await?; + self.interrupt_worker(&component, &agent_id, true).await?; log_action( "Simulated crash", - format!("for agent {}", format_agent_name_match(&agent_name_match)), + format!("for agent {}", format_agent_id_match(&agent_id_match)), ); self.ctx .log_handler() .log_output(AgentSimulateCrashResult { simulated: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), })?; Ok(()) @@ -688,15 +667,13 @@ impl WorkerCommandHandler { async fn cmd_oplog( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, from: Option, query: Option, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; let batch_size = self.ctx.http_batch_size(); let mut cursor = Option::::None; @@ -710,7 +687,7 @@ impl WorkerCommandHandler { .worker .get_oplog( &component.id.0, - &agent_name.0, + &agent_id.0, from, batch_size, cursor.as_ref(), @@ -751,7 +728,7 @@ impl WorkerCommandHandler { async fn cmd_revert( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, last_oplog_index: Option, number_of_invocations: Option, ) -> anyhow::Result<()> { @@ -765,14 +742,12 @@ impl WorkerCommandHandler { } self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Reverting", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); let clients = self.ctx.golem_clients().await?; @@ -794,7 +769,7 @@ impl WorkerCommandHandler { clients .worker - .revert_worker(&component.id.0, &agent_name.0, &target) + .revert_worker(&component.id.0, &agent_id.0, &target) .await .map(|_| ()) .map_service_error()? @@ -802,12 +777,12 @@ impl WorkerCommandHandler { log_action( "Reverted", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); self.ctx.log_handler().log_output(AgentRevertResult { reverted: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), last_oplog_index, number_of_invocations, })?; @@ -817,20 +792,18 @@ impl WorkerCommandHandler { async fn cmd_cancel_invocation( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, idempotency_key: IdempotencyKey, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_warn_action( "Canceling invocation", format!( "for agent {} using idempotency key: {}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), idempotency_key.value.log_color_highlight() ), ); @@ -839,7 +812,7 @@ impl WorkerCommandHandler { let canceled = clients .worker - .cancel_invocation(&component.id.0, &agent_name.0, &idempotency_key.value) + .cancel_invocation(&component.id.0, &agent_id.0, &idempotency_key.value) .await .map(|result| result.canceled) .map_service_error()?; @@ -854,7 +827,7 @@ impl WorkerCommandHandler { .log_handler() .log_output(AgentCancelInvocationResult { canceled, - agent_id: agent_name.0, + agent_id: agent_id.0, idempotency_key: idempotency_key.value, })?; @@ -1107,7 +1080,7 @@ impl WorkerCommandHandler { .await?; for worker in workers { - let raw_agent_name = worker.agent_id.agent_id.clone(); + let raw_agent_id = worker.agent_id.agent_id.clone(); let worker_component = self .ctx @@ -1119,7 +1092,7 @@ impl WorkerCommandHandler { .await?; let parsed_agent_type_name = - ParsedAgentId::parse_agent_type_name(&raw_agent_name).ok(); + ParsedAgentId::parse_agent_type_name(&raw_agent_id).ok(); let defaults = parsed_agent_type_name.as_ref().and_then(|agent_type_name| { worker_component @@ -1155,11 +1128,11 @@ impl WorkerCommandHandler { .with_defaults(defaults) .with_secret_config_paths(secret_config_paths); - let parsed = ParsedAgentId::parse(&raw_agent_name, &worker_component.metadata).ok(); + let parsed = ParsedAgentId::parse(&raw_agent_id, &worker_component.metadata).ok(); agent_view.agent_id = crate::agent_id_display::render_agent_id_or_raw( parsed.as_ref(), &source_language, - &raw_agent_name, + &raw_agent_id, ) .into(); @@ -1187,18 +1160,16 @@ impl WorkerCommandHandler { Ok(view) } - async fn cmd_interrupt(&self, agent_name: AgentIdArgs) -> anyhow::Result<()> { + async fn cmd_interrupt(&self, agent_id: AgentIdArgs) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; if component .metadata .agent_types() .iter() - .find(|agent_type| agent_type.type_name == agent_name_match.agent_type_name) + .find(|agent_type| agent_type.type_name == agent_id_match.agent_type_name) .is_some_and(|agent_type| agent_type.mode == AgentMode::Ephemeral) && !self .ctx @@ -1210,47 +1181,44 @@ impl WorkerCommandHandler { log_action( "Interrupting", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); - self.interrupt_worker(&component, &agent_name, false) - .await?; + self.interrupt_worker(&component, &agent_id, false).await?; log_action( "Interrupted", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); self.ctx.log_handler().log_output(AgentInterruptResult { interrupted: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), })?; Ok(()) } - async fn cmd_resume(&self, agent_name: AgentIdArgs) -> anyhow::Result<()> { + async fn cmd_resume(&self, agent_id: AgentIdArgs) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Resuming", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); - self.resume_worker(&component, &agent_name).await?; + self.resume_worker(&component, &agent_id).await?; log_action( "Resumed", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); self.ctx.log_handler().log_output(AgentResumeResult { resumed: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), })?; Ok(()) @@ -1258,19 +1226,17 @@ impl WorkerCommandHandler { async fn cmd_update( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, mode: AgentUpdateMode, target_revision: Option, await_update: bool, disable_wakeup: bool, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; - let environment = &agent_name_match.environment; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; + let environment = &agent_id_match.environment; let target_revision = match target_revision { Some(target_revision) => target_revision, @@ -1292,9 +1258,9 @@ impl WorkerCommandHandler { if !self.ctx.interactive_handler().confirm_update_to_current( &component.component_name, - &agent_name, - agent_name_match.parsed_agent_id.as_ref(), - &agent_name_match.source_language, + &agent_id, + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, current_deployed_revision.revision, )? { bail!(NonSuccessfulExit) @@ -1305,13 +1271,13 @@ impl WorkerCommandHandler { }; let from_revision = self - .worker_metadata(component.id.0, &component.component_name, &agent_name) + .worker_metadata(component.id.0, &component.component_name, &agent_id) .await? .map(|metadata| metadata.component_revision) .unwrap_or(target_revision); let meta = AgentUpdateMeta { component_name: component.component_name.clone(), - agent_id: agent_name.clone(), + agent_id: agent_id.clone(), from_revision, revision: target_revision, from_version: self @@ -1328,7 +1294,7 @@ impl WorkerCommandHandler { .update_worker( &component.component_name, &component.id, - &agent_name.0, + &agent_id.0, mode, target_revision, await_update, @@ -1340,7 +1306,7 @@ impl WorkerCommandHandler { Err(error) => { update_results .errors - .insert(agent_name.0.clone(), error.to_string()); + .insert(agent_id.0.clone(), error.to_string()); self.ctx.log_handler().log_output(update_results)?; return Err(error); } @@ -1351,43 +1317,41 @@ impl WorkerCommandHandler { Ok(()) } - async fn cmd_get(&self, agent_name: AgentIdArgs) -> anyhow::Result<()> { + async fn cmd_get(&self, agent_id: AgentIdArgs) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; let clients = self.ctx.golem_clients().await?; let metadata = { let result = clients .worker - .get_worker_metadata(&component.id.0, &agent_name.0) + .get_worker_metadata(&component.id.0, &agent_id.0) .await .map_service_error()?; - AgentMetadata::from(agent_name_match.component_name, result) + AgentMetadata::from(agent_id_match.component_name, result) }; let defaults = component .metadata .agent_type_provision_configs() - .get(&agent_name_match.agent_type_name) + .get(&agent_id_match.agent_type_name) .cloned(); let secret_config_paths = secret_config_paths_for_agent_type( component.metadata.agent_types(), - &agent_name_match.agent_type_name, + &agent_id_match.agent_type_name, ); let mut metadata_view = AgentMetadataView::from(metadata) .with_defaults(defaults) .with_secret_config_paths(secret_config_paths) - .with_source_language(agent_name_match.source_language.clone()); + .with_source_language(agent_id_match.source_language.clone()); metadata_view.agent_id = crate::agent_id_display::render_agent_id_or_raw( - agent_name_match.parsed_agent_id.as_ref(), - &agent_name_match.source_language, + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, &metadata_view.agent_id.0, ) .into(); @@ -1399,45 +1363,41 @@ impl WorkerCommandHandler { Ok(()) } - async fn cmd_delete(&self, agent_name: AgentIdArgs) -> anyhow::Result<()> { + async fn cmd_delete(&self, agent_id: AgentIdArgs) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_warn_action( "Deleting", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); - self.delete(component.id.0, &agent_name.0).await?; + self.delete(component.id.0, &agent_id.0).await?; log_action( "Deleted", - format!("agent {}", format_agent_name_match(&agent_name_match)), + format!("agent {}", format_agent_id_match(&agent_id_match)), ); self.ctx.log_handler().log_output(AgentDeleteResult { deleted: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), })?; Ok(()) } - async fn cmd_files(&self, agent_name: AgentIdArgs, path: String) -> anyhow::Result<()> { + async fn cmd_files(&self, agent_id: AgentIdArgs, path: String) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Listing files", format!( "for agent {} at path {}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), path.log_color_highlight() ), ); @@ -1445,7 +1405,7 @@ impl WorkerCommandHandler { let clients = self.ctx.golem_clients().await?; let nodes = match clients .worker - .get_files(&component.id.0, &agent_name.0, &path) + .get_files(&component.id.0, &agent_id.0, &path) .await .map_service_error() { @@ -1455,7 +1415,7 @@ impl WorkerCommandHandler { "Failed to list files", format!( "for agent {} at path {}: {e}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), path.log_color_error_highlight() ), ); @@ -1488,7 +1448,7 @@ impl WorkerCommandHandler { "Listed files", format!( "for agent {} at path {}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), path.log_color_highlight() ), ); @@ -1498,21 +1458,19 @@ impl WorkerCommandHandler { async fn cmd_file_contents( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, path: String, output: Option, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; log_action( "Downloading file", format!( "from agent {} at path {}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), path.log_color_highlight() ), ); @@ -1520,7 +1478,7 @@ impl WorkerCommandHandler { let clients = self.ctx.golem_clients().await?; let file_contents = match clients .worker - .get_file_content(&component.id.0, &agent_name.0, &path) + .get_file_content(&component.id.0, &agent_id.0, &path) .await .map_service_error() { @@ -1530,7 +1488,7 @@ impl WorkerCommandHandler { "Failed to download file", format!( "from agent {} at path {}: {e}", - format_agent_name_match(&agent_name_match), + format_agent_id_match(&agent_id_match), path.log_color_error_highlight() ), ); @@ -1557,7 +1515,7 @@ impl WorkerCommandHandler { ); self.ctx.log_handler().log_output(AgentFileContentsResult { saved: false, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), path, output_path: output_path.into(), bytes: 0, @@ -1574,7 +1532,7 @@ impl WorkerCommandHandler { ); self.ctx.log_handler().log_output(AgentFileContentsResult { saved: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), path, output_path: output_path.into(), bytes: file_contents.len(), @@ -1593,32 +1551,30 @@ impl WorkerCommandHandler { async fn cmd_activate_plugin( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, plugin_name: String, explicit_priority: Option, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; let plugin_priority = - self.resolve_plugin_priority(&component, &agent_name, &plugin_name, explicit_priority)?; + self.resolve_plugin_priority(&component, &agent_id, &plugin_name, explicit_priority)?; log_action( "Activating plugin", format!( "{} for agent {}", plugin_name.log_color_highlight(), - format_agent_name_match(&agent_name_match) + format_agent_id_match(&agent_id_match) ), ); let clients = self.ctx.golem_clients().await?; clients .worker - .activate_plugin(&component.id.0, &agent_name.0, plugin_priority) + .activate_plugin(&component.id.0, &agent_id.0, plugin_priority) .await .map(|_| ()) .map_service_error()?; @@ -1628,13 +1584,13 @@ impl WorkerCommandHandler { format!( "{} for agent {}", plugin_name.log_color_highlight(), - format_agent_name_match(&agent_name_match) + format_agent_id_match(&agent_id_match) ), ); self.ctx.log_handler().log_output(AgentPluginToggleResult { activated: true, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), plugin: plugin_name.clone(), priority: plugin_priority, })?; @@ -1644,32 +1600,30 @@ impl WorkerCommandHandler { async fn cmd_deactivate_plugin( &self, - agent_name: AgentIdArgs, + agent_id: AgentIdArgs, plugin_name: String, explicit_priority: Option, ) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let agent_name_match = self.match_agent_name(agent_name.agent_id).await?; - let (component, agent_name) = self - .component_by_agent_name_match(&agent_name_match) - .await?; + let agent_id_match = self.match_agent_id(agent_id.agent_id).await?; + let (component, agent_id) = self.component_by_agent_id_match(&agent_id_match).await?; let plugin_priority = - self.resolve_plugin_priority(&component, &agent_name, &plugin_name, explicit_priority)?; + self.resolve_plugin_priority(&component, &agent_id, &plugin_name, explicit_priority)?; log_action( "Deactivating plugin", format!( "{} for agent {}", plugin_name.log_color_highlight(), - format_agent_name_match(&agent_name_match) + format_agent_id_match(&agent_id_match) ), ); let clients = self.ctx.golem_clients().await?; clients .worker - .deactivate_plugin(&component.id.0, &agent_name.0, plugin_priority) + .deactivate_plugin(&component.id.0, &agent_id.0, plugin_priority) .await .map(|_| ()) .map_service_error()?; @@ -1679,13 +1633,13 @@ impl WorkerCommandHandler { format!( "{} for agent {}", plugin_name.log_color_highlight(), - format_agent_name_match(&agent_name_match) + format_agent_id_match(&agent_id_match) ), ); self.ctx.log_handler().log_output(AgentPluginToggleResult { activated: false, - agent_id: agent_name.0.clone(), + agent_id: agent_id.0.clone(), plugin: plugin_name.clone(), priority: plugin_priority, })?; @@ -1696,11 +1650,11 @@ impl WorkerCommandHandler { fn resolve_plugin_priority( &self, component: &ComponentDto, - agent_name: &RawAgentId, + agent_id: &RawAgentId, plugin_name: &str, explicit_priority: Option, ) -> anyhow::Result { - let agent_type_name = ParsedAgentId::parse_agent_type_name(&agent_name.0) + let agent_type_name = ParsedAgentId::parse_agent_type_name(&agent_id.0) .map(|n| n.0) .unwrap_or_default(); @@ -1773,7 +1727,7 @@ impl WorkerCommandHandler { async fn new_worker( &self, component_id: Uuid, - agent_name: String, + agent_id: String, env: HashMap, config: Vec, ) -> anyhow::Result<()> { @@ -1784,7 +1738,7 @@ impl WorkerCommandHandler { .launch_new_worker( &component_id, &golem_client::model::AgentCreationRequest { - name: agent_name, + name: agent_id, env, config, }, @@ -1800,24 +1754,24 @@ impl WorkerCommandHandler { &self, component_id: Uuid, component_name: &ComponentName, - agent_name: &RawAgentId, + agent_id: &RawAgentId, ) -> anyhow::Result> { let clients = self.ctx.golem_clients().await?; Ok(clients .worker - .get_worker_metadata(&component_id, &agent_name.0) + .get_worker_metadata(&component_id, &agent_id.0) .await .map_service_error_not_found_as_opt()? .map(|result| AgentMetadata::from(component_name.clone(), result))) } - async fn delete(&self, component_id: Uuid, agent_name: &str) -> anyhow::Result<()> { + async fn delete(&self, component_id: Uuid, agent_id: &str) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; clients .worker - .delete_worker(&component_id, agent_name) + .delete_worker(&component_id, agent_id) .await .map(|_| ()) .map_service_error()?; @@ -1924,7 +1878,7 @@ impl WorkerCommandHandler { &self, component_name: &ComponentName, component_id: &ComponentId, - agent_name: &str, + agent_id: &str, update_mode: AgentUpdateMode, target_revision: ComponentRevision, await_update: bool, @@ -1935,7 +1889,7 @@ impl WorkerCommandHandler { format!( "for agent {}/{} to revision {} using {} update mode", component_name.0.bold().blue(), - agent_name.bold().green(), + agent_id.bold().green(), target_revision.to_string().log_color_highlight(), update_mode.to_string().log_color_highlight() ), @@ -1947,7 +1901,7 @@ impl WorkerCommandHandler { .worker .update_worker( &component_id.0, - agent_name, + agent_id, &UpdateWorkerRequest { mode: match update_mode { AgentUpdateMode::Automatic => { @@ -1968,7 +1922,7 @@ impl WorkerCommandHandler { log_action("Triggered update", ""); if await_update { - self.await_update_result(component_id, agent_name, target_revision) + self.await_update_result(component_id, agent_id, target_revision) .await?; } @@ -1986,14 +1940,14 @@ impl WorkerCommandHandler { async fn await_update_result( &self, component_id: &ComponentId, - agent_name: &str, + agent_id: &str, target_revision: ComponentRevision, ) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; loop { let metadata = clients .worker - .get_worker_metadata(&component_id.0, agent_name) + .get_worker_metadata(&component_id.0, agent_id) .await?; for update_record in metadata.updates { let mut latest_success = None; @@ -2111,10 +2065,10 @@ impl WorkerCommandHandler { let mut redeployed = Vec::with_capacity(workers.len()); for worker in workers { - let agent_name: RawAgentId = worker.agent_id.agent_id.as_str().into(); + let agent_id: RawAgentId = worker.agent_id.agent_id.as_str().into(); let from_revision = worker.component_revision; self.redeploy_worker(component_name, worker).await?; - redeployed.push((agent_name, from_revision)); + redeployed.push((agent_id, from_revision)); } Ok(redeployed) @@ -2303,44 +2257,41 @@ impl WorkerCommandHandler { Ok((workers, final_result_cursor)) } - pub(crate) async fn component_by_agent_name_match( + pub(crate) async fn component_by_agent_id_match( &self, - agent_name_match: &AgentNameMatch, + agent_id_match: &AgentIdMatch, ) -> anyhow::Result<(ComponentDto, RawAgentId)> { let component = self .ctx .component_handler() .resolve_component( - &agent_name_match.environment, - &agent_name_match.component_name, - Some((&agent_name_match.agent_name).into()), + &agent_id_match.environment, + &agent_id_match.component_name, + Some((&agent_id_match.agent_id).into()), ) .await?; let Some(component) = component else { log_error(format!( "Component {} not found", - agent_name_match - .component_name - .0 - .log_color_error_highlight() + agent_id_match.component_name.0.log_color_error_highlight() )); logln(""); bail!(NonSuccessfulExit); }; - // `agent_name_match.agent_name` is already canonicalized in - // `match_agent_name_in_environment`. Just return a clone for callers + // `agent_id_match.agent_id` is already canonicalized in + // `match_agent_id_in_environment`. Just return a clone for callers // that need it for downstream HTTP/gRPC calls. - Ok((component, agent_name_match.agent_name.clone())) + Ok((component, agent_id_match.agent_id.clone())) } - pub(crate) fn try_recanonicalize_agent_name( + pub(crate) fn try_recanonicalize_agent_id( &self, - agent_name: &RawAgentId, + agent_id: &RawAgentId, component: &ComponentDto, ) -> RawAgentId { - try_recanonicalize_agent_name_with_parsed(agent_name, component).0 + try_recanonicalize_agent_id_with_parsed(agent_id, component).0 } } @@ -2349,16 +2300,16 @@ impl WorkerCommandHandler { /// representation (when language-aware parsing succeeded). The parsed form /// can be used by display code to render the agent id in its source /// language (see [`crate::agent_id_display::render_agent_id`]). -pub(crate) fn try_recanonicalize_agent_name_with_parsed( - agent_name: &RawAgentId, +pub(crate) fn try_recanonicalize_agent_id_with_parsed( + agent_id: &RawAgentId, component: &ComponentDto, ) -> (RawAgentId, Option) { - let raw = &agent_name.0; + let raw = &agent_id.0; // Extract type name and params using ParsedAgentId::parse_agent_type_name // and manual splitting for the params portion let Some(paren_pos) = raw.find('(') else { - return (agent_name.clone(), None); + return (agent_id.clone(), None); }; let type_name = &raw[..paren_pos]; @@ -2380,7 +2331,7 @@ pub(crate) fn try_recanonicalize_agent_name_with_parsed( } let Some(close_pos) = close_pos else { - return (agent_name.clone(), None); + return (agent_id.clone(), None); }; let params_str = &raw[paren_pos + 1..close_pos]; @@ -2398,7 +2349,7 @@ pub(crate) fn try_recanonicalize_agent_name_with_parsed( .find(|at| at.type_name.0 == type_name); let Some(agent_type) = agent_type else { - return (agent_name.clone(), None); + return (agent_id.clone(), None); }; // Derive source language from agent type metadata @@ -2410,14 +2361,14 @@ pub(crate) fn try_recanonicalize_agent_name_with_parsed( &agent_type.constructor.input_schema, &source_language, ) else { - return (agent_name.clone(), None); + return (agent_id.clone(), None); }; let typed = typed_constructor_parameters(agent_type, value); let Ok(canonical) = golem_common::model::agent::structural_format::format_structural_typed(&typed) else { - return (agent_name.clone(), None); + return (agent_id.clone(), None); }; let mut new_id = format!("{}({canonical})", agent_type.type_name.0); @@ -2439,13 +2390,13 @@ impl WorkerCommandHandler { async fn resume_worker( &self, component: &ComponentDto, - agent_name: &RawAgentId, + agent_id: &RawAgentId, ) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; clients .worker - .resume_worker(&component.id.0, &agent_name.0) + .resume_worker(&component.id.0, &agent_id.0) .await .map(|_| ()) .map_service_error()?; @@ -2456,14 +2407,14 @@ impl WorkerCommandHandler { async fn interrupt_worker( &self, component: &ComponentDto, - agent_name: &RawAgentId, + agent_id: &RawAgentId, recover_immediately: bool, ) -> anyhow::Result<()> { let clients = self.ctx.golem_clients().await?; clients .worker - .interrupt_worker(&component.id.0, &agent_name.0, Some(recover_immediately)) + .interrupt_worker(&component.id.0, &agent_id.0, Some(recover_immediately)) .await .map(|_| ()) .map_service_error()?; @@ -2471,18 +2422,18 @@ impl WorkerCommandHandler { Ok(()) } - async fn match_agent_name_in_environment( + async fn match_agent_id_in_environment( &self, environment: ResolvedEnvironmentIdentity, - agent_name: String, - ) -> anyhow::Result { - let parsed_agent_type_name = match ParsedAgentId::parse_agent_type_name(&agent_name) { + agent_id: String, + ) -> anyhow::Result { + let parsed_agent_type_name = match ParsedAgentId::parse_agent_type_name(&agent_id) { Ok(agent_type_name) => agent_type_name, Err(err) => { logln(""); log_error(format!( "Failed to parse agent name ({}) as agent id: {err}", - agent_name.log_color_error_highlight() + agent_id.log_color_error_highlight() )); logln(""); log_text_view(&AgentNameHelp); @@ -2526,28 +2477,28 @@ impl WorkerCommandHandler { // Recanonicalize the user input against the component's agent type // metadata so the canonical form (used for HTTP/gRPC calls) and the // parsed form (used for language-specific display) are both available - // immediately on the resulting `AgentNameMatch`. - let raw_agent_name: RawAgentId = agent_name.into(); - let (canonical_agent_name, parsed_agent_id) = - try_recanonicalize_agent_name_with_parsed(&raw_agent_name, &component); + // immediately on the resulting `AgentIdMatch`. + let raw_agent_id: RawAgentId = agent_id.into(); + let (canonical_agent_id, parsed_agent_id) = + try_recanonicalize_agent_id_with_parsed(&raw_agent_id, &component); - Ok(AgentNameMatch { + Ok(AgentIdMatch { environment, component_name_match_kind: ComponentNameMatchKind::Unknown, component_name: component.component_name, agent_type_name: parsed_agent_type_name, - agent_name: canonical_agent_name, + agent_id: canonical_agent_id, source_language: SourceLanguage::from(agent_type.agent_type.source_language.as_str()), parsed_agent_id, }) } - pub async fn match_agent_name(&self, agent_name: RawAgentId) -> anyhow::Result { - let segments = split_agent_name(&agent_name.0); + pub async fn match_agent_id(&self, agent_id: RawAgentId) -> anyhow::Result { + let segments = split_agent_id(&agent_id.0); match segments.len() { // 1 => { - let agent_name = segments[0].to_string(); + let agent_id = segments[0].to_string(); let environment = self .ctx @@ -2555,7 +2506,7 @@ impl WorkerCommandHandler { .resolve_environment(EnvironmentResolveMode::Any) .await?; - self.match_agent_name_in_environment(environment, agent_name) + self.match_agent_id_in_environment(environment, agent_id) .await } // / @@ -2612,7 +2563,7 @@ impl WorkerCommandHandler { Ok(non_empty("agent", value)?.to_string()) } - let (environment_reference, agent_name): (Option, String) = + let (environment_reference, agent_id): (Option, String) = match segments.len() { 2 => ( Some(EnvironmentReference::Environment { @@ -2647,14 +2598,14 @@ impl WorkerCommandHandler { ) .await?; - self.match_agent_name_in_environment(environment, agent_name) + self.match_agent_id_in_environment(environment, agent_id) .await } _ => { logln(""); log_error(format!( "Failed to parse agent name: {}", - agent_name.0.log_color_error_highlight() + agent_id.0.log_color_error_highlight() )); logln(""); log_text_view(&AgentNameHelp); @@ -2666,14 +2617,14 @@ impl WorkerCommandHandler { pub fn validate_worker_and_function_names( &self, component: &ComponentDto, - agent_name: &RawAgentId, + agent_id: &RawAgentId, function_name: Option<&str>, ) -> anyhow::Result> { if !component.metadata.is_agent() { return Ok(None); } - match ParsedAgentId::parse_and_resolve_type(&agent_name.0, &component.metadata) { + match ParsedAgentId::parse_and_resolve_type(&agent_id.0, &component.metadata) { Ok((agent_id, agent_type)) => match function_name { Some(function_name) => { let parsed = match ParsedFunctionName::parse(function_name) { @@ -2728,13 +2679,12 @@ impl WorkerCommandHandler { None => Ok(Some((agent_id, agent_type.clone()))), }, Err(err) => { - let parsed_agent_type_name = - ParsedAgentId::parse_agent_type_name(&agent_name.0).ok(); + let parsed_agent_type_name = ParsedAgentId::parse_agent_type_name(&agent_id.0).ok(); logln(""); log_error(format!( "Failed to parse agent name ({}) as agent id: {err}", - agent_name.0.log_color_error_highlight() + agent_id.0.log_color_error_highlight() )); logln(""); log_text_view(&AvailableAgentConstructorsHelp::for_component( @@ -3096,25 +3046,25 @@ fn parse_worker_error(status: u16, body: Vec) -> ServiceError { } } -fn split_agent_name(agent_name: &str) -> Vec<&str> { - match agent_name.find('(') { +fn split_agent_id(agent_id: &str) -> Vec<&str> { + match agent_id.find('(') { Some(constructor_open_parentheses_idx) => { - let splittable = &agent_name[0..constructor_open_parentheses_idx]; + let splittable = &agent_id[0..constructor_open_parentheses_idx]; let last_slash_idx = splittable.rfind('/'); match last_slash_idx { Some(last_slash_idx) => { - let mut segments = agent_name[0..last_slash_idx] + let mut segments = agent_id[0..last_slash_idx] .split('/') .collect::>(); - segments.push(&agent_name[last_slash_idx + 1..]); + segments.push(&agent_id[last_slash_idx + 1..]); segments } None => { - vec![agent_name] + vec![agent_id] } } } - None => agent_name.split("/").collect(), + None => agent_id.split("/").collect(), } } @@ -3164,7 +3114,7 @@ fn validate_public_invocation_agent_id( mod tests { use super::{ AgentListMode, apply_list_mode_filter, build_repl_agent_id, normalize_public_agent_id, - parse_method_argument_schema_value, split_agent_name, validate_public_invocation_agent_id, + parse_method_argument_schema_value, split_agent_id, validate_public_invocation_agent_id, }; use crate::agent_id_display::SourceLanguage; use golem_common::model::agent::{AgentMode, AgentTypeName, ParsedAgentId, Snapshotting}; @@ -3261,18 +3211,18 @@ mod tests { } #[test] - fn test_split_agent_name() { - assert_eq!(split_agent_name("a"), vec!["a"]); - assert_eq!(split_agent_name("a()"), vec!["a()"]); - assert_eq!(split_agent_name("a(\"///\")"), vec!["a(\"///\")"]); - assert_eq!(split_agent_name("a/b"), vec!["a", "b"]); - assert_eq!(split_agent_name("a/b()"), vec!["a", "b()"]); - assert_eq!(split_agent_name("a/b(\"///\")"), vec!["a", "b(\"///\")"]); - assert_eq!(split_agent_name("a/b/c"), vec!["a", "b", "c"]); - assert_eq!(split_agent_name("a/b/c()"), vec!["a", "b", "c()"]); - assert_eq!(split_agent_name("a/b/c(\"/\")"), vec!["a", "b", "c(\"/\")"]); - assert_eq!(split_agent_name("/"), vec!["", ""]); - assert_eq!(split_agent_name("a(/"), vec!["a(/"]); + fn test_split_agent_id() { + assert_eq!(split_agent_id("a"), vec!["a"]); + assert_eq!(split_agent_id("a()"), vec!["a()"]); + assert_eq!(split_agent_id("a(\"///\")"), vec!["a(\"///\")"]); + assert_eq!(split_agent_id("a/b"), vec!["a", "b"]); + assert_eq!(split_agent_id("a/b()"), vec!["a", "b()"]); + assert_eq!(split_agent_id("a/b(\"///\")"), vec!["a", "b(\"///\")"]); + assert_eq!(split_agent_id("a/b/c"), vec!["a", "b", "c"]); + assert_eq!(split_agent_id("a/b/c()"), vec!["a", "b", "c()"]); + assert_eq!(split_agent_id("a/b/c(\"/\")"), vec!["a", "b", "c(\"/\")"]); + assert_eq!(split_agent_id("/"), vec!["", ""]); + assert_eq!(split_agent_id("a(/"), vec!["a(/"]); } #[test] diff --git a/cli/golem-cli/src/command_handler/worker/stream.rs b/cli/golem-cli/src/command_handler/worker/stream.rs index fcce4f2af8..609923183d 100644 --- a/cli/golem-cli/src/command_handler/worker/stream.rs +++ b/cli/golem-cli/src/command_handler/worker/stream.rs @@ -57,7 +57,7 @@ impl WorkerConnection { worker_service_url: Url, auth_token: TokenSecret, component_id: &ComponentId, - agent_name: String, + agent_id: String, connect_options: AgentLogStreamOptions, allow_insecure: bool, format: Format, @@ -68,7 +68,7 @@ impl WorkerConnection { worker_service_url, auth_token.secret().to_string(), component_id.0, - agent_name, + agent_id, allow_insecure, )?; let output = WorkerStreamOutput::new(connect_options, format); @@ -151,10 +151,10 @@ impl WorkerConnection { worker_service_url: Url, auth_token: String, component_id: Uuid, - agent_name: String, + agent_id: String, allow_insecure: bool, ) -> anyhow::Result<(Request, Option)> { - AgentId::validate_length(&agent_name).map_err(|err| anyhow!(err))?; + AgentId::validate_length(&agent_id).map_err(|err| anyhow!(err))?; let mut url = worker_service_url; @@ -168,7 +168,7 @@ impl WorkerConnection { .push("components") .push(&component_id.to_string()) .push("workers") - .push(&agent_name) + .push(&agent_id) .push("connect"); debug!(url = url.as_str(), "Worker stream connect"); diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 41b63931a3..4802233177 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -725,7 +725,7 @@ impl Application { !self.components.is_empty() } - pub fn agent_names(&self) -> impl Iterator { + pub fn agent_ids(&self) -> impl Iterator { self.agents.keys() } @@ -2748,8 +2748,8 @@ mod app_builder { UniqueSourceCheckedEntityKey::Component(component_name) => { component_name.as_str().log_color_highlight().to_string() } - UniqueSourceCheckedEntityKey::Agent(agent_name) => { - agent_name.0.log_color_highlight().to_string() + UniqueSourceCheckedEntityKey::Agent(agent_id) => { + agent_id.0.log_color_highlight().to_string() } UniqueSourceCheckedEntityKey::Environment(environment_name) => { environment_name.0.log_color_highlight().to_string() diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index 558fdd6951..7af222a3e7 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -2927,14 +2927,12 @@ mod tests { } fn arb_agent_new_result() -> OutputDocumentStrategy { - serialized_output( - (arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_name)| crate::model::text::worker::WorkerCreateView { - component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_name), - }, - ), - ) + serialized_output((arb_small_string(), arb_small_string()).prop_map( + |(component_name, agent_id)| crate::model::text::worker::WorkerCreateView { + component_name: golem_common::model::component::ComponentName(component_name), + agent_id: crate::model::worker::RawAgentId(agent_id), + }, + )) } fn arb_agent_oplog_result() -> OutputDocumentStrategy { @@ -3057,10 +3055,10 @@ mod tests { proptest::option::of(arb_small_string()), ) .prop_map( - |(component_name, agent_name, from_revision, revision, from_version, version)| { + |(component_name, agent_id, from_revision, revision, from_version, version)| { ( golem_common::model::component::ComponentName(component_name), - crate::model::worker::RawAgentId(agent_name), + crate::model::worker::RawAgentId(agent_id), golem_common::model::component::ComponentRevision::new(from_revision) .expect("generated revision should be valid"), golem_common::model::component::ComponentRevision::new(revision) @@ -3076,10 +3074,10 @@ mod tests { fn arb_agent_update_meta() -> BoxedStrategy { arb_agent_transition_fields() .prop_map( - |(component_name, agent_name, from_revision, revision, from_version, version)| { + |(component_name, agent_id, from_revision, revision, from_version, version)| { crate::model::deploy::AgentUpdateMeta { component_name, - agent_id: agent_name, + agent_id, from_revision, revision, from_version, @@ -3094,10 +3092,10 @@ mod tests { -> BoxedStrategy { arb_agent_transition_fields() .prop_map( - |(component_name, agent_name, from_revision, revision, from_version, version)| { + |(component_name, agent_id, from_revision, revision, from_version, version)| { crate::model::text::action_result::AgentRedeploymentMeta { component_name, - agent_id: agent_name, + agent_id, from_revision, revision, from_version, @@ -3111,10 +3109,10 @@ mod tests { fn arb_agent_deletion_meta() -> BoxedStrategy { (arb_small_string(), arb_small_string()) - .prop_map(|(component_name, agent_name)| { + .prop_map(|(component_name, agent_id)| { crate::model::text::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_name), + agent_id: crate::model::worker::RawAgentId(agent_id), } }) .boxed() @@ -3152,7 +3150,7 @@ mod tests { .prop_map(|(left, right)| { let ( component_name, - agent_name, + agent_id, created_by, environment_id, env, @@ -3175,7 +3173,7 @@ mod tests { crate::model::worker::AgentMetadataView { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_name), + agent_id: crate::model::worker::RawAgentId(agent_id), created_by: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), ), diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index ed04d2f249..c4671a3dd5 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -104,13 +104,13 @@ fn parse_manifest_grants( } pub enum ComponentRevisionSelection<'a> { - ByAgentName(&'a RawAgentId), + ByAgentId(&'a RawAgentId), ByExplicitRevision(ComponentRevision), } impl<'a> From<&'a RawAgentId> for ComponentRevisionSelection<'a> { fn from(value: &'a RawAgentId) -> Self { - Self::ByAgentName(value) + Self::ByAgentId(value) } } @@ -353,7 +353,7 @@ fn render_exported_agent( show_dummy_return_type, &lang, )); - let agent_name = if wrapper_naming { + let agent_id = if wrapper_naming { format!("{}.", agent.type_name.0) } else { " ".to_string() @@ -362,11 +362,11 @@ fn render_exported_agent( let output = render_output_schema(&agent.schema, &method.output_schema, &lang); let input = render_input_schema(&agent.schema, &method.input_schema, &lang, true); if output.is_empty() { - result.push(format!("{}{}({})", agent_name, method.name, input)); + result.push(format!("{}{}({})", agent_id, method.name, input)); } else { result.push(format!( "{}{}({}) -> {}", - agent_name, method.name, input, output + agent_id, method.name, input, output )); } } diff --git a/cli/golem-cli/src/model/text/diff.rs b/cli/golem-cli/src/model/text/diff.rs index 0345115cb8..6aec1bb7d5 100644 --- a/cli/golem-cli/src/model/text/diff.rs +++ b/cli/golem-cli/src/model/text/diff.rs @@ -150,27 +150,27 @@ impl TextOutput for DeploymentDiff { } if !diff.agents_changes.is_empty() { logln(" - agents"); - for (agent_name, agent_diff) in &diff.agents_changes { + for (agent_id, agent_diff) in &diff.agents_changes { match agent_diff { BTreeMapDiffValue::Create => { logln(format!( " - {} agent {}", "create".green(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); } BTreeMapDiffValue::Delete => { logln(format!( " - {} agent {}", "delete".red(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); } BTreeMapDiffValue::Update(diff) => { logln(format!( " - {} agent {}, changes:", "update".yellow(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); if diff.security_scheme_changed { logln(" - security_scheme"); @@ -222,27 +222,27 @@ impl TextOutput for DeploymentDiff { )); if !diff.agents_changes.is_empty() { logln(" - agents"); - for (agent_name, agent_diff) in &diff.agents_changes { + for (agent_id, agent_diff) in &diff.agents_changes { match agent_diff { BTreeMapDiffValue::Create => { logln(format!( " - {} agent {}", "create".green(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); } BTreeMapDiffValue::Delete => { logln(format!( " - {} agent {}", "delete".red(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); } BTreeMapDiffValue::Update(diff) => { logln(format!( " - {} agent {}, changes:", "update".yellow(), - agent_name.log_color_highlight() + agent_id.log_color_highlight() )); if diff.security_scheme_changed { logln(" - security_scheme"); diff --git a/cli/golem-cli/src/model/text/help.rs b/cli/golem-cli/src/model/text/help.rs index f04f312c23..25eabd8fde 100644 --- a/cli/golem-cli/src/model/text/help.rs +++ b/cli/golem-cli/src/model/text/help.rs @@ -199,7 +199,11 @@ impl AvailableProfileNamesHelp { impl TextOutput for AvailableProfileNamesHelp { fn log(&self) { - let mut names = self.0.iter().map(|name| name.0.as_str()).collect::>(); + let mut names = self + .0 + .iter() + .map(|name| name.0.as_str()) + .collect::>(); names.sort(); logln( @@ -215,7 +219,7 @@ impl TextOutput for AvailableProfileNamesHelp { pub struct AvailableFunctionNamesHelp { pub component_name: String, - pub agent_name: Option, + pub agent_id: Option, pub function_names: Vec, } @@ -227,7 +231,7 @@ impl AvailableFunctionNamesHelp { ) -> Self { AvailableFunctionNamesHelp { component_name: component.component_name.0.clone(), - agent_name: Some(agent_id.agent_type.0.clone()), + agent_id: Some(agent_id.agent_type.0.clone()), function_names: agent_type.methods.iter().map(|m| m.name.clone()).collect(), } } @@ -236,12 +240,12 @@ impl AvailableFunctionNamesHelp { impl TextOutput for AvailableFunctionNamesHelp { fn log(&self) { if self.function_names.is_empty() { - match &self.agent_name { - Some(agent_name) => { + match &self.agent_id { + Some(agent_id) => { logln( format!( "No methods are available for agent {}.", - agent_name.underline() + agent_id.underline() ) .log_color_warn() .to_string(), @@ -261,10 +265,10 @@ impl TextOutput for AvailableFunctionNamesHelp { return; } - match &self.agent_name { - Some(agent_name) => { + match &self.agent_id { + Some(agent_id) => { logln( - format!("Available method names for agent {}:", agent_name) + format!("Available method names for agent {}:", agent_id) .bold() .underline() .to_string(), diff --git a/cli/golem-cli/src/model/text/http_api_deployment.rs b/cli/golem-cli/src/model/text/http_api_deployment.rs index 547311fc16..cfef53922a 100644 --- a/cli/golem-cli/src/model/text/http_api_deployment.rs +++ b/cli/golem-cli/src/model/text/http_api_deployment.rs @@ -68,8 +68,8 @@ fn http_api_deployment_fields(dep: &HttpApiDeployment) -> Vec<(String, String)> }) .fmt_field("Agents", &dep.agents, |agents| { let mut result = String::new(); - for (agent_name, agent_options) in agents { - result.push_str(&format!("- Agent ID: {}", agent_name)); + for (agent_id, agent_options) in agents { + result.push_str(&format!("- Agent ID: {}", agent_id)); match &agent_options.security { None => {} Some(HttpApiDeploymentAgentSecurity::SecurityScheme(inner)) => { diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index 63be2e64c7..509d164111 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -21,7 +21,7 @@ use crate::model::invoke_result_view::InvokeResultView; use crate::model::masking::{Masked, MaskingConfig}; use crate::model::text::fmt::*; use crate::model::worker::{ - AgentMetadataView, AgentNameMatch, AgentsMetadataResponseView, RawAgentId, + AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, }; use base64::Engine; use base64::prelude::BASE64_STANDARD; @@ -67,9 +67,9 @@ impl MessageWithFields for WorkerCreateView { fields .fmt_field("Component name", &self.component_name, format_id) - .fmt_field("Agent ID", &self.agent_id, |agent_name| { + .fmt_field("Agent ID", &self.agent_id, |agent_id| { format_agent_id_in( - &agent_name.0, + &agent_id.0, colored::control::SHOULD_COLORIZE.should_colorize(), field_value_width::(), ) @@ -184,9 +184,9 @@ impl MessageWithFields for WorkerGetView { &self.metadata.component_revision, format_id, ) - .fmt_field("Agent ID", &self.metadata.agent_id, |agent_name| { + .fmt_field("Agent ID", &self.metadata.agent_id, |agent_id| { format_agent_id_in( - &agent_name.0, + &agent_id.0, colored::control::SHOULD_COLORIZE.should_colorize(), field_value_width::(), ) @@ -1359,8 +1359,8 @@ fn render_typed_schema_value_line( } /// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). -fn format_agent_id_in(agent_name: &str, colorize: bool, width: usize) -> String { - crate::agent_id_display::format_agent_id_for_terminal(agent_name, colorize, Some(width)) +fn format_agent_id_in(agent_id: &str, colorize: bool, width: usize) -> String { + crate::agent_id_display::format_agent_id_for_terminal(agent_id, colorize, Some(width)) } fn log_optional_error(pad: &str, error: &Option) { @@ -1431,10 +1431,10 @@ mod tests { Timestamp::from(0) } - fn agent_metadata(agent_name: &str) -> AgentMetadataView { + fn agent_metadata(agent_id: &str) -> AgentMetadataView { AgentMetadataView { component_name: ComponentName("shop:cart".to_string()), - agent_id: RawAgentId(agent_name.to_string()), + agent_id: RawAgentId(agent_id.to_string()), created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), env: HashMap::new(), @@ -1759,16 +1759,16 @@ pub fn format_timestamp(timestamp: u64) -> String { } } -pub fn format_agent_name_match(agent_name_match: &AgentNameMatch) -> String { - let rendered_agent_name = crate::agent_id_display::render_agent_id_or_raw( - agent_name_match.parsed_agent_id.as_ref(), - &agent_name_match.source_language, - &agent_name_match.agent_name.0, +pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { + let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, + &agent_id_match.agent_id.0, ); format!( "{}{}/{}", - match &agent_name_match.environment_reference() { + match &agent_id_match.environment_reference() { Some(environment_reference) => { match environment_reference { EnvironmentReference::Environment { environment_name } => { @@ -1800,7 +1800,7 @@ pub fn format_agent_name_match(agent_name_match: &AgentNameMatch) -> String { } None => "".to_string(), }, - agent_name_match.component_name.0.blue().bold(), - rendered_agent_name.green().bold(), + agent_id_match.component_name.0.blue().bold(), + rendered_agent_id.green().bold(), ) } diff --git a/cli/golem-cli/src/model/worker.rs b/cli/golem-cli/src/model/worker.rs index 9ecd63b54b..c2a583dfc5 100644 --- a/cli/golem-cli/src/model/worker.rs +++ b/cli/golem-cli/src/model/worker.rs @@ -294,17 +294,17 @@ impl From for AgentLogStreamOptions { } } -pub struct AgentNameMatch { +pub struct AgentIdMatch { pub environment: ResolvedEnvironmentIdentity, pub component_name_match_kind: ComponentNameMatchKind, pub component_name: ComponentName, pub agent_type_name: AgentTypeName, - pub agent_name: RawAgentId, + pub agent_id: RawAgentId, pub source_language: SourceLanguage, pub parsed_agent_id: Option, } -impl AgentNameMatch { +impl AgentIdMatch { pub fn environment_reference(&self) -> Option<&EnvironmentReference> { match &self.environment.source { ResolvedEnvironmentIdentitySource::Reference(reference) => Some(reference), @@ -312,15 +312,15 @@ impl AgentNameMatch { } } - /// Updates the canonical agent_name and the parsed form together. Use this + /// Updates the canonical agent_id and the parsed form together. Use this /// after re-canonicalizing or normalizing the agent id so that downstream /// display code can use the language-specific renderer. pub fn with_canonical_and_parsed( mut self, - agent_name: RawAgentId, + agent_id: RawAgentId, parsed_agent_id: Option, ) -> Self { - self.agent_name = agent_name; + self.agent_id = agent_id; self.parsed_agent_id = parsed_agent_id; self } From a553d1c51bdd0abf9d9a08370e1870eff8614e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 15:00:59 +0200 Subject: [PATCH 21/70] drop auth-mode use-context framing from profile/token docs --- .../how-to-guides/common/golem-cloud-account-setup.mdx | 8 ++++---- .../common/golem-profiles-and-environments.mdx | 2 +- .../skills/common/golem-cloud-account-setup/SKILL.md | 8 ++++---- .../common/golem-profiles-and-environments/SKILL.md | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx b/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx index 22e6a04be5..b9ee5d43b4 100644 --- a/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-cloud-account-setup.mdx @@ -67,7 +67,7 @@ golem -C account new "Team Account" "team@example.com" # Create add ## Step 4: Create and Manage API Tokens -For programmatic access (CI/CD, scripts), create static API tokens: +Create static API tokens: ```shell golem -C api-token list # List existing tokens @@ -78,13 +78,13 @@ golem -C api-token delete # Delete a t `golem api-token new` prints the token secret once, including when using structured output such as `--format json`. Store that value securely; it cannot be retrieved later. -Use a static token in a profile for non-interactive environments: +Use a static token in a profile: ```shell -golem profile new ci-cloud --url https://release.api.golem.cloud --static-token "" --set-active +golem profile new token-cloud --url https://release.api.golem.cloud --static-token "" --set-active ``` -For an interactive setup, `--auth static` (without `--static-token`) prompts for the token instead of putting it on the command line: +`--auth static` without `--static-token` prompts for the token (masked) instead of putting it on the command line: ```shell golem profile new my-cloud --url https://release.api.golem.cloud --auth static --set-active diff --git a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx index 226ef37cfc..bc8d47f9a6 100644 --- a/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-profiles-and-environments.mdx @@ -39,7 +39,7 @@ Profiles are global CLI configuration stored in `~/.golem/config.json`. They def golem profile new # Interactive setup golem profile new my-staging --url https://staging.example.com # OAuth2 (default) golem profile new my-staging --url https://staging.example.com --auth static # static token (prompted, masked) -golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline (scripts) +golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline golem profile list # List all profiles golem profile switch my-staging # Set active profile golem profile get # Show active profile diff --git a/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md b/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md index 130b73cdd5..89903ca80c 100644 --- a/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md +++ b/golem-skills/skills/common/golem-cloud-account-setup/SKILL.md @@ -72,7 +72,7 @@ golem -C account new "Team Account" "team@example.com" # Create add ## Step 4: Create and Manage API Tokens -For programmatic access (CI/CD, scripts), create static API tokens: +Create static API tokens: ```shell golem -C api-token list # List existing tokens @@ -83,13 +83,13 @@ golem -C api-token delete # Delete a t `golem api-token new` prints the token secret once, including when using structured output such as `--format json`. Store that value securely; it cannot be retrieved later. -Use a static token in a profile for non-interactive environments: +Use a static token in a profile: ```shell -golem profile new ci-cloud --url https://release.api.golem.cloud --static-token "" --set-active +golem profile new token-cloud --url https://release.api.golem.cloud --static-token "" --set-active ``` -For an interactive setup, `--auth static` (without `--static-token`) prompts for the token instead of putting it on the command line: +`--auth static` without `--static-token` prompts for the token (masked) instead of putting it on the command line: ```shell golem profile new my-cloud --url https://release.api.golem.cloud --auth static --set-active diff --git a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md index 3e40e75f54..d1490d2f76 100644 --- a/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md +++ b/golem-skills/skills/common/golem-profiles-and-environments/SKILL.md @@ -44,7 +44,7 @@ Profiles are global CLI configuration stored in `~/.golem/config.json`. They def golem profile new # Interactive setup golem profile new my-staging --url https://staging.example.com # OAuth2 (default) golem profile new my-staging --url https://staging.example.com --auth static # static token (prompted, masked) -golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline (scripts) +golem profile new my-staging --url https://staging.example.com --static-token "..." # static token inline golem profile list # List all profiles golem profile switch my-staging # Set active profile golem profile get # Show active profile From 10ff9b67469a26e8ac55f49afb00c9546d91000b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 15:13:44 +0200 Subject: [PATCH 22/70] use OAuth2 variant with explicit oauth2 clap value in ProfileAuthMode --- cli/golem-cli/src/command.rs | 3 ++- cli/golem-cli/src/command_handler/profile/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index f7fb757708..429e00ba51 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -2100,7 +2100,8 @@ pub mod profile { #[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] #[clap(rename_all = "kebab-case")] pub enum ProfileAuthMode { - Oauth2, + #[value(name = "oauth2")] + OAuth2, Static, } diff --git a/cli/golem-cli/src/command_handler/profile/mod.rs b/cli/golem-cli/src/command_handler/profile/mod.rs index 29ee79d124..f913933581 100644 --- a/cli/golem-cli/src/command_handler/profile/mod.rs +++ b/cli/golem-cli/src/command_handler/profile/mod.rs @@ -150,7 +150,7 @@ impl ProfileCommandHandler { static_token: Option, ) -> anyhow::Result { match (mode, static_token) { - (Some(ProfileAuthMode::Oauth2), Some(_)) => { + (Some(ProfileAuthMode::OAuth2), Some(_)) => { log_error( "--static-token cannot be combined with --auth oauth2. A static token implies --auth static.", ); @@ -163,7 +163,7 @@ impl ProfileCommandHandler { let token = self.ctx.interactive_handler().prompt_static_token()?; Ok(AuthenticationConfig::static_token(token)) } - (Some(ProfileAuthMode::Oauth2), None) | (None, None) => { + (Some(ProfileAuthMode::OAuth2), None) | (None, None) => { Ok(AuthenticationConfig::empty_oauth2()) } } From a9545f5f56f9bc171f4aff4151d4b291a22d01bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:05:48 +0200 Subject: [PATCH 23/70] rename cli agent-domain Worker* view/handler/stream types to Agent* --- .../command-output.schema.json | 6 ++--- cli/golem-cli/src/command_handler/card.rs | 4 ++-- cli/golem-cli/src/command_handler/mod.rs | 8 +++---- .../src/command_handler/worker/mod.rs | 24 +++++++++---------- .../src/command_handler/worker/stream.rs | 14 +++++------ .../command_handler/worker/stream_output.rs | 14 +++++------ cli/golem-cli/src/model/cli_output.rs | 14 +++++------ cli/golem-cli/src/model/text/worker.rs | 24 +++++++++---------- 8 files changed, 54 insertions(+), 54 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index c706db8240..97a158e653 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -6740,11 +6740,11 @@ }, { "type": "agent.files", - "rustType": "WorkerFilesView" + "rustType": "AgentFilesView" }, { "type": "agent.get", - "rustType": "WorkerGetView" + "rustType": "AgentGetView" }, { "type": "agent.interrupt", @@ -6760,7 +6760,7 @@ }, { "type": "agent.new", - "rustType": "WorkerCreateView" + "rustType": "AgentCreateView" }, { "type": "agent.oplog", diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 805916f17d..ad5d928f43 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -14,7 +14,7 @@ use crate::command::card::CardSubcommand; use crate::command_handler::Handlers; -use crate::command_handler::worker::WorkerCommandHandler; +use crate::command_handler::worker::AgentCommandHandler; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; @@ -140,7 +140,7 @@ impl CardCommandHandler { async fn cmd_list_agent_wallet(&self, agent: RawAgentId) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let worker_handler = WorkerCommandHandler::new(self.ctx.clone()); + let worker_handler = AgentCommandHandler::new(self.ctx.clone()); let agent_id_match = worker_handler.match_agent_id(agent).await?; let (component, agent_id) = worker_handler .component_by_agent_id_match(&agent_id_match) diff --git a/cli/golem-cli/src/command_handler/mod.rs b/cli/golem-cli/src/command_handler/mod.rs index fd90c91c4f..2e85940632 100644 --- a/cli/golem-cli/src/command_handler/mod.rs +++ b/cli/golem-cli/src/command_handler/mod.rs @@ -41,7 +41,7 @@ use crate::command_handler::plugin::PluginCommandHandler; use crate::command_handler::profile::ProfileCommandHandler; use crate::command_handler::profile::config::ProfileConfigCommandHandler; use crate::command_handler::repl::ReplHandler; -use crate::command_handler::worker::WorkerCommandHandler; +use crate::command_handler::worker::AgentCommandHandler; use crate::context::Context; use crate::error::{ContextInitHintError, HintError, NonSuccessfulExit, PipedExitCode}; use crate::log::{Output, log_anyhow_error, logln, set_log_output}; @@ -592,7 +592,7 @@ pub trait Handlers { fn profile_config_handler(&self) -> ProfileConfigCommandHandler; fn profile_handler(&self) -> ProfileCommandHandler; fn repl_handler(&self) -> ReplHandler; - fn worker_handler(&self) -> WorkerCommandHandler; + fn worker_handler(&self) -> AgentCommandHandler; } impl Handlers for Arc { @@ -687,8 +687,8 @@ impl Handlers for Arc { ReplHandler::new(self.clone()) } - fn worker_handler(&self) -> WorkerCommandHandler { - WorkerCommandHandler::new(self.clone()) + fn worker_handler(&self) -> AgentCommandHandler { + AgentCommandHandler::new(self.clone()) } } diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 8547faa8a7..f46a8fcc2b 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -20,7 +20,7 @@ use crate::command::shared_args::{ }; use crate::command::worker::AgentSubcommand; use crate::command_handler::Handlers; -use crate::command_handler::worker::stream::WorkerConnection; +use crate::command_handler::worker::stream::AgentConnection; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::{MapServiceError, ServiceError}; @@ -42,7 +42,7 @@ use crate::model::text::help::{ ParameterErrorTableView, }; use crate::model::text::worker::{ - AgentOplogEntryView, FileNodeView, WorkerCreateView, WorkerFilesView, WorkerGetView, + AgentCreateView, AgentFilesView, AgentGetView, AgentOplogEntryView, FileNodeView, format_agent_id_match, format_timestamp, }; use anyhow::{Context as AnyhowContext, anyhow, bail}; @@ -97,11 +97,11 @@ use tokio::time::{sleep, timeout}; use tracing::debug; use uuid::Uuid; -pub struct WorkerCommandHandler { +pub struct AgentCommandHandler { ctx: Arc, } -impl WorkerCommandHandler { +impl AgentCommandHandler { pub fn new(ctx: Arc) -> Self { Self { ctx } } @@ -308,7 +308,7 @@ impl WorkerCommandHandler { .into(); logln(""); - self.ctx.log_handler().log_output(WorkerCreateView { + self.ctx.log_handler().log_output(AgentCreateView { component_name: agent_id_match.component_name, agent_id: display_agent_id, })?; @@ -475,7 +475,7 @@ impl WorkerCommandHandler { )?; let mut connect_handle = if !no_stream { - let connection = WorkerConnection::new( + let connection = AgentConnection::new( self.ctx.worker_service_url().clone(), self.ctx.auth_token().await?, &component.id, @@ -562,7 +562,7 @@ impl WorkerCommandHandler { format!("to agent {}", format_agent_id_match(&agent_id_match)), ); - let connection = WorkerConnection::new( + let connection = AgentConnection::new( self.ctx.worker_service_url().clone(), self.ctx.auth_token().await?, &component.id, @@ -620,7 +620,7 @@ impl WorkerCommandHandler { )?; let agent_id = RawAgentId(agent_id.to_string()); - let connection = WorkerConnection::new( + let connection = AgentConnection::new( self.ctx.worker_service_url().clone(), self.ctx.auth_token().await?, &agent_type.implemented_by.component_id, @@ -1358,7 +1358,7 @@ impl WorkerCommandHandler { self.ctx .log_handler() - .log_output(WorkerGetView::from_metadata(metadata_view, true))?; + .log_output(AgentGetView::from_metadata(metadata_view, true))?; Ok(()) } @@ -1423,8 +1423,8 @@ impl WorkerCommandHandler { } }; - // Convert nodes to WorkerFilesView with human-readable timestamps - let view = WorkerFilesView { + // Convert nodes to AgentFilesView with human-readable timestamps + let view = AgentFilesView { nodes: nodes .nodes .into_iter() @@ -2386,7 +2386,7 @@ pub(crate) fn try_recanonicalize_agent_id_with_parsed( (RawAgentId(new_id), parsed) } -impl WorkerCommandHandler { +impl AgentCommandHandler { async fn resume_worker( &self, component: &ComponentDto, diff --git a/cli/golem-cli/src/command_handler/worker/stream.rs b/cli/golem-cli/src/command_handler/worker/stream.rs index 609923183d..3c9b253019 100644 --- a/cli/golem-cli/src/command_handler/worker/stream.rs +++ b/cli/golem-cli/src/command_handler/worker/stream.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::command_handler::worker::parse_worker_error; -use crate::command_handler::worker::stream_output::WorkerStreamOutput; +use crate::command_handler::worker::stream_output::AgentStreamOutput; use crate::model::format::Format; use crate::model::worker::AgentLogStreamOptions; use anyhow::{Context, anyhow}; @@ -41,17 +41,17 @@ use tracing::{debug, error, info, trace}; use url::Url; use uuid::Uuid; -pub struct WorkerConnection { +pub struct AgentConnection { request: Request, connector: Option, - output: WorkerStreamOutput, + output: AgentStreamOutput, idempotency_key: Option, last_seen_idempotency_key: Arc>>, goal_reached: Arc, ping_interval: Duration, } -impl WorkerConnection { +impl AgentConnection { /// Initializes a worker stream. Use `run_forever` to connect and output worker events. pub async fn new( worker_service_url: Url, @@ -63,7 +63,7 @@ impl WorkerConnection { format: Format, ping_interval: Duration, idempotency_key: Option, - ) -> anyhow::Result { + ) -> anyhow::Result { let (request, connector) = Self::create_request( worker_service_url, auth_token.secret().to_string(), @@ -71,7 +71,7 @@ impl WorkerConnection { agent_id, allow_insecure, )?; - let output = WorkerStreamOutput::new(connect_options, format); + let output = AgentStreamOutput::new(connect_options, format); let last_seen_idempotency_key = Arc::new(Mutex::new(None)); let goal_reached = Arc::new(AtomicBool::new(false)); @@ -224,7 +224,7 @@ impl WorkerConnection { async fn read_loop( read: SplitStream>>, - output: WorkerStreamOutput, + output: AgentStreamOutput, last_seen_idempotency_key: Arc>>, idempotency_key_to_look_for: Option, goal_reached: Arc, diff --git a/cli/golem-cli/src/command_handler/worker/stream_output.rs b/cli/golem-cli/src/command_handler/worker/stream_output.rs index fdc00d435f..9cdfac75bc 100644 --- a/cli/golem-cli/src/command_handler/worker/stream_output.rs +++ b/cli/golem-cli/src/command_handler/worker/stream_output.rs @@ -28,13 +28,13 @@ use tokio::sync::Mutex; use tokio_tungstenite::tungstenite; #[derive(Clone)] -pub struct WorkerStreamOutput { - state: Arc>, +pub struct AgentStreamOutput { + state: Arc>, options: AgentLogStreamOptions, format: Format, } -struct WorkerStreamOutputState { +struct AgentStreamOutputState { pub last_stdout_timestamp: Timestamp, pub stdout: String, pub last_stderr_timestamp: Timestamp, @@ -43,10 +43,10 @@ struct WorkerStreamOutputState { pub last_timestamp_hashes: HashSet, } -impl WorkerStreamOutput { +impl AgentStreamOutput { pub fn new(options: AgentLogStreamOptions, format: Format) -> Self { - WorkerStreamOutput { - state: Arc::new(Mutex::new(WorkerStreamOutputState { + AgentStreamOutput { + state: Arc::new(Mutex::new(AgentStreamOutputState { last_stdout_timestamp: Timestamp::now_utc(), stdout: String::new(), last_stderr_timestamp: Timestamp::now_utc(), @@ -257,7 +257,7 @@ impl WorkerStreamOutput { async fn check_already_seen( &self, - state: &mut WorkerStreamOutputState, + state: &mut AgentStreamOutputState, timestamp: Timestamp, message: &str, ) -> bool { diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index 7af222a3e7..5eb44a54bf 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -360,8 +360,8 @@ mod tests { "agent.file-contents", arb_agent_file_contents_result ), - registry_entry!("WorkerFilesView", "agent.files", arb_agent_files_result), - registry_entry!("WorkerGetView", "agent.get", arb_agent_get_result), + registry_entry!("AgentFilesView", "agent.files", arb_agent_files_result), + registry_entry!("AgentGetView", "agent.get", arb_agent_get_result), registry_entry!( "AgentInterruptResult", "agent.interrupt", @@ -373,7 +373,7 @@ mod tests { "agent.list", arb_agent_list_result ), - registry_entry!("WorkerCreateView", "agent.new", arb_agent_new_result), + registry_entry!("AgentCreateView", "agent.new", arb_agent_new_result), registry_entry!("AgentOplogEntryView", "agent.oplog", arb_agent_oplog_result), registry_entry!( "AgentPluginToggleResult", @@ -920,7 +920,7 @@ mod tests { #[test] fn agent_get_structured_output_masks_secret_config_paths() { - let value = hidden_structured_output(crate::model::text::worker::WorkerGetView { + let value = hidden_structured_output(crate::model::text::worker::AgentGetView { metadata: sample_agent_metadata_view(), precise: true, }); @@ -2851,7 +2851,7 @@ mod tests { fn arb_agent_files_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::worker::WorkerFilesView { nodes }), + .prop_map(|nodes| crate::model::text::worker::AgentFilesView { nodes }), ) } @@ -2877,7 +2877,7 @@ mod tests { fn arb_agent_get_result() -> OutputDocumentStrategy { serialized_output((arb_agent_metadata_view(), any::()).prop_map( - |(metadata, precise)| crate::model::text::worker::WorkerGetView { metadata, precise }, + |(metadata, precise)| crate::model::text::worker::AgentGetView { metadata, precise }, )) } @@ -2928,7 +2928,7 @@ mod tests { fn arb_agent_new_result() -> OutputDocumentStrategy { serialized_output((arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_id)| crate::model::text::worker::WorkerCreateView { + |(component_name, agent_id)| crate::model::text::worker::AgentCreateView { component_name: golem_common::model::component::ComponentName(component_name), agent_id: crate::model::worker::RawAgentId(agent_id), }, diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index 509d164111..9799da89f5 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -47,14 +47,14 @@ use std::fmt::Write; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WorkerCreateView { +pub struct AgentCreateView { pub component_name: ComponentName, pub agent_id: RawAgentId, } -impl Masked for WorkerCreateView {} +impl Masked for AgentCreateView {} -impl MessageWithFields for WorkerCreateView { +impl MessageWithFields for AgentCreateView { fn message(&self) -> String { format!( "Created new agent {}", @@ -79,24 +79,24 @@ impl MessageWithFields for WorkerCreateView { } } -impl StructuredOutput for WorkerCreateView { +impl StructuredOutput for AgentCreateView { const KIND: &'static str = "agent.new"; } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WorkerGetView { +pub struct AgentGetView { pub metadata: AgentMetadataView, pub precise: bool, } -impl WorkerGetView { +impl AgentGetView { pub fn from_metadata(metadata: AgentMetadataView, precise: bool) -> Self { Self { metadata, precise } } } -impl Masked for WorkerGetView { +impl Masked for AgentGetView { fn masked(mut self, config: MaskingConfig) -> anyhow::Result { self.metadata = self.metadata.masked(config)?; Ok(self) @@ -120,7 +120,7 @@ fn to_sorted_btree_map(map: &HashMap) -> BTreeMap String { format!( "Got metadata for agent {}", @@ -256,7 +256,7 @@ impl MessageWithFields for WorkerGetView { } } -impl StructuredOutput for WorkerGetView { +impl StructuredOutput for AgentGetView { const KIND: &'static str = "agent.get"; fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result @@ -1706,11 +1706,11 @@ mod tests { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct WorkerFilesView { +pub struct AgentFilesView { pub nodes: Vec, } -impl StructuredOutput for WorkerFilesView { +impl StructuredOutput for AgentFilesView { const KIND: &'static str = "agent.files"; } @@ -1724,7 +1724,7 @@ pub struct FileNodeView { pub size: u64, } -impl TextOutput for WorkerFilesView { +impl TextOutput for AgentFilesView { fn log(&self) { if self.nodes.is_empty() { logln("No files found."); From 4e0335d871d63d0d422d79695371df2415fd9d8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:10:46 +0200 Subject: [PATCH 24/70] rename cli agent-op functions and the worker_handler accessor to agent --- cli/golem-cli/src/command_handler/app/mod.rs | 14 ++++---- cli/golem-cli/src/command_handler/card.rs | 6 ++-- .../src/command_handler/component/mod.rs | 32 +++++++++---------- cli/golem-cli/src/command_handler/mod.rs | 10 +++--- .../src/command_handler/partial_match.rs | 17 ++++------ .../src/command_handler/worker/mod.rs | 30 ++++++++--------- 6 files changed, 53 insertions(+), 56 deletions(-) diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index b2cdb0c246..18e8565dfe 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -422,7 +422,7 @@ impl AppCommandHandler { Ok(()) } - pub async fn cmd_update_workers( + pub async fn cmd_update_agents( &self, component_names: Vec, update_mode: AgentUpdateMode, @@ -435,13 +435,13 @@ impl AppCommandHandler { let components = self.components_for_deploy_args().await?; self.ctx .component_handler() - .update_workers_by_components(&components, update_mode, await_update, disable_wakeup) + .update_agents_by_components(&components, update_mode, await_update, disable_wakeup) .await?; Ok(()) } - pub async fn cmd_redeploy_workers( + pub async fn cmd_redeploy_agents( &self, component_names: Vec, ) -> anyhow::Result<()> { @@ -451,7 +451,7 @@ impl AppCommandHandler { let components = self.components_for_deploy_args().await?; self.ctx .component_handler() - .redeploy_workers_by_components(&components) + .redeploy_agents_by_components(&components) .await?; Ok(()) @@ -2295,21 +2295,21 @@ impl AppCommandHandler { if let Some(update_mode) = post_deploy_args.update_agents_mode(env_deploy_args) { self.ctx .component_handler() - .update_workers_by_components(&components, update_mode, true, false) + .update_agents_by_components(&components, update_mode, true, false) .await .map(|()| PostDeploySummary::AgentUpdateOk) .map_err(PostDeployError::AgentUpdateError) } else if post_deploy_args.redeploy_agents(env_deploy_args) { self.ctx .component_handler() - .redeploy_workers_by_components(&components) + .redeploy_agents_by_components(&components) .await .map(|()| PostDeploySummary::AgentRedeployOk) .map_err(PostDeployError::AgentRedeployError) } else if post_deploy_args.delete_agents(env_deploy_args) { self.ctx .component_handler() - .delete_workers(&components) + .delete_agents(&components) .await .map(|()| PostDeploySummary::AgentDeleteOk) .map_err(PostDeployError::AgentDeleteError) diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index ad5d928f43..910863fc63 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -140,9 +140,9 @@ impl CardCommandHandler { async fn cmd_list_agent_wallet(&self, agent: RawAgentId) -> anyhow::Result<()> { self.ctx.silence_app_context_init().await; - let worker_handler = AgentCommandHandler::new(self.ctx.clone()); - let agent_id_match = worker_handler.match_agent_id(agent).await?; - let (component, agent_id) = worker_handler + let agent_handler = AgentCommandHandler::new(self.ctx.clone()); + let agent_id_match = agent_handler.match_agent_id(agent).await?; + let (component, agent_id) = agent_handler .component_by_agent_id_match(&agent_id_match) .await?; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 442746e9de..97f0ea5b03 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -101,7 +101,7 @@ impl ComponentCommandHandler { r#await, disable_wakeup, } => { - self.cmd_update_workers( + self.cmd_update_agents( component_name.component_name, update_mode, r#await, @@ -110,7 +110,7 @@ impl ComponentCommandHandler { .await } ComponentSubcommand::RedeployAgents { component_name } => { - self.cmd_redeploy_workers(component_name.component_name) + self.cmd_redeploy_agents(component_name.component_name) .await } ComponentSubcommand::ManifestTrace { component_name } => { @@ -244,7 +244,7 @@ impl ComponentCommandHandler { Ok(()) } - async fn cmd_update_workers( + async fn cmd_update_agents( &self, component_name: Option, update_mode: AgentUpdateMode, @@ -252,18 +252,18 @@ impl ComponentCommandHandler { disable_wakeup: bool, ) -> anyhow::Result<()> { let components = self.components_for_deploy_args(component_name).await?; - self.update_workers_by_components(&components, update_mode, await_update, disable_wakeup) + self.update_agents_by_components(&components, update_mode, await_update, disable_wakeup) .await?; Ok(()) } - async fn cmd_redeploy_workers( + async fn cmd_redeploy_agents( &self, component_name: Option, ) -> anyhow::Result<()> { let components = self.components_for_deploy_args(component_name).await?; - self.redeploy_workers_by_components(&components).await?; + self.redeploy_agents_by_components(&components).await?; Ok(()) } @@ -348,7 +348,7 @@ impl ComponentCommandHandler { Ok(()) } - pub async fn update_workers_by_components( + pub async fn update_agents_by_components( &self, components: &[ComponentDto], update: AgentUpdateMode, @@ -366,8 +366,8 @@ impl ComponentCommandHandler { for component in components { let result = self .ctx - .worker_handler() - .update_component_workers( + .agent_handler() + .update_component_agents( &component.component_name, &component.id, update, @@ -384,7 +384,7 @@ impl ComponentCommandHandler { Ok(()) } - pub async fn redeploy_workers_by_components( + pub async fn redeploy_agents_by_components( &self, components: &[ComponentDto], ) -> anyhow::Result<()> { @@ -400,8 +400,8 @@ impl ComponentCommandHandler { for component in components { let redeployed = self .ctx - .worker_handler() - .redeploy_component_workers(&component.component_name, &component.id) + .agent_handler() + .redeploy_component_agents(&component.component_name, &component.id) .await?; let version = component.metadata.root_package_version().clone(); for (agent_id, from_revision) in redeployed { @@ -427,7 +427,7 @@ impl ComponentCommandHandler { Ok(()) } - pub async fn delete_workers(&self, components: &[ComponentDto]) -> anyhow::Result<()> { + pub async fn delete_agents(&self, components: &[ComponentDto]) -> anyhow::Result<()> { if components.is_empty() { return Ok(()); } @@ -446,8 +446,8 @@ impl ComponentCommandHandler { for component in components { let deleted = self .ctx - .worker_handler() - .delete_component_workers(&component.component_name, &component.id, first_round) + .agent_handler() + .delete_component_agents(&component.component_name, &component.id, first_round) .await?; if !deleted.is_empty() { found_any = true; @@ -760,7 +760,7 @@ impl ComponentCommandHandler { let revision = match component_revision_selection { ComponentRevisionSelection::ByAgentId(agent_id) => self .ctx - .worker_handler() + .agent_handler() .worker_metadata(component.id.0, &component.component_name, agent_id) .await? .map(|worker_metadata| worker_metadata.component_revision), diff --git a/cli/golem-cli/src/command_handler/mod.rs b/cli/golem-cli/src/command_handler/mod.rs index 2e85940632..3cd75ca597 100644 --- a/cli/golem-cli/src/command_handler/mod.rs +++ b/cli/golem-cli/src/command_handler/mod.rs @@ -350,7 +350,7 @@ impl CommandHandler { ctx.get_or_init() .await? .app_handler() - .cmd_update_workers( + .cmd_update_agents( component_name.component_name, update_mode, r#await, @@ -362,7 +362,7 @@ impl CommandHandler { ctx.get_or_init() .await? .app_handler() - .cmd_redeploy_workers(component_name.component_name) + .cmd_redeploy_agents(component_name.component_name) .await } GolemCliSubcommand::Exec { subcommand } => { @@ -391,7 +391,7 @@ impl CommandHandler { GolemCliSubcommand::Agent { subcommand } => { ctx.get_or_init() .await? - .worker_handler() + .agent_handler() .handle_command(subcommand) .await } @@ -592,7 +592,7 @@ pub trait Handlers { fn profile_config_handler(&self) -> ProfileConfigCommandHandler; fn profile_handler(&self) -> ProfileCommandHandler; fn repl_handler(&self) -> ReplHandler; - fn worker_handler(&self) -> AgentCommandHandler; + fn agent_handler(&self) -> AgentCommandHandler; } impl Handlers for Arc { @@ -687,7 +687,7 @@ impl Handlers for Arc { ReplHandler::new(self.clone()) } - fn worker_handler(&self) -> AgentCommandHandler { + fn agent_handler(&self) -> AgentCommandHandler { AgentCommandHandler::new(self.clone()) } } diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index e9d41e38ec..e017cc1caf 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -140,7 +140,7 @@ impl ErrorHandler { ); let agent_id_match = { let _indent = DecoratedIndent::new_primary(Format::Text); - let agent_id_match = self.ctx.worker_handler().match_agent_id(agent_id).await?; + let agent_id_match = self.ctx.agent_handler().match_agent_id(agent_id).await?; let environment_formatted = match agent_id_match.environment_reference() { Some(env) => { @@ -178,16 +178,13 @@ impl ErrorHandler { { let canonical_agent_id = self .ctx - .worker_handler() + .agent_handler() .try_recanonicalize_agent_id(&agent_id_match.agent_id, &component); - let agent_id = self - .ctx - .worker_handler() - .validate_worker_and_function_names( - &component, - &canonical_agent_id, - None, - )?; + let agent_id = self.ctx.agent_handler().validate_agent_and_function_names( + &component, + &canonical_agent_id, + None, + )?; if let Some((agent_id, agent_type)) = agent_id.as_ref() { log_text_view(&AvailableFunctionNamesHelp::new_agent( diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index f46a8fcc2b..75381cbd8b 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -274,7 +274,7 @@ impl AgentCommandHandler { .await?; let agent_id = agent_id_match.agent_id.clone(); - let agent_id = match self.validate_worker_and_function_names(&component, &agent_id, None)? { + let agent_id = match self.validate_agent_and_function_names(&component, &agent_id, None)? { Some((agent_id, agent_type)) => { // `normalize_public_agent_id` may auto-generate a phantom // UUID for ephemeral agents, changing the canonical form. @@ -376,7 +376,7 @@ impl AgentCommandHandler { // First, validate without the function name. The agent name was // already canonicalized when the `AgentIdMatch` was constructed. let agent_id_and_type = - self.validate_worker_and_function_names(&component, &agent_id_match.agent_id, None)?; + self.validate_agent_and_function_names(&component, &agent_id_match.agent_id, None)?; let (agent_id, agent_type) = agent_id_and_type.ok_or_else(|| anyhow!("Agent invoke requires an agent component"))?; @@ -1068,7 +1068,7 @@ impl AgentCommandHandler { for component in components { let (workers, component_scan_cursor) = self - .list_component_workers( + .list_component_agents( &component.component_name, &component.id, Some(filters), @@ -1779,7 +1779,7 @@ impl AgentCommandHandler { Ok(()) } - pub async fn update_component_workers( + pub async fn update_component_agents( &self, component_name: &ComponentName, component_id: &ComponentId, @@ -1795,7 +1795,7 @@ impl AgentCommandHandler { AgentFilter::new_revision(FilterComparator::Less, target_revision).to_string(), ]; let (workers_to_update, _) = self - .list_component_workers( + .list_component_agents( component_name, component_id, Some(&agent_filters), @@ -2028,13 +2028,13 @@ impl AgentCommandHandler { /// Redeploys all agents of a component, returning each redeployed agent's id and the revision it /// was running at before (its "from" revision). - pub async fn redeploy_component_workers( + pub async fn redeploy_component_agents( &self, component_name: &ComponentName, component_id: &ComponentId, ) -> anyhow::Result> { let (workers, _) = self - .list_component_workers(component_name, component_id, None, None, None, None, false) + .list_component_agents(component_name, component_id, None, None, None, None, false) .await?; if workers.is_empty() { @@ -2075,14 +2075,14 @@ impl AgentCommandHandler { } /// Deletes all agents of a component, returning the ids of the agents that were deleted. - pub async fn delete_component_workers( + pub async fn delete_component_agents( &self, component_name: &ComponentName, component_id: &ComponentId, show_skip: bool, ) -> anyhow::Result> { let (workers, _) = self - .list_component_workers(component_name, component_id, None, None, None, None, false) + .list_component_agents(component_name, component_id, None, None, None, None, false) .await?; if workers.is_empty() { @@ -2181,7 +2181,7 @@ impl AgentCommandHandler { Ok(()) } - pub async fn list_component_workers( + pub async fn list_component_agents( &self, component_name: &ComponentName, component_id: &ComponentId, @@ -2559,7 +2559,7 @@ impl AgentCommandHandler { validated("environment", value) } - fn validated_worker(value: &str) -> anyhow::Result { + fn validated_agent(value: &str) -> anyhow::Result { Ok(non_empty("agent", value)?.to_string()) } @@ -2569,14 +2569,14 @@ impl AgentCommandHandler { Some(EnvironmentReference::Environment { environment_name: validated_environment(segments[0])?, }), - validated_worker(segments[1])?, + validated_agent(segments[1])?, ), 3 => ( Some(EnvironmentReference::ApplicationEnvironment { application_name: validated_application(segments[0])?, environment_name: validated_environment(segments[1])?, }), - validated_worker(segments[2])?, + validated_agent(segments[2])?, ), 4 => ( Some(EnvironmentReference::AccountApplicationEnvironment { @@ -2584,7 +2584,7 @@ impl AgentCommandHandler { application_name: validated_application(segments[1])?, environment_name: validated_environment(segments[2])?, }), - validated_worker(segments[3])?, + validated_agent(segments[3])?, ), other => panic!("Unexpected segment count: {other}"), }; @@ -2614,7 +2614,7 @@ impl AgentCommandHandler { } } - pub fn validate_worker_and_function_names( + pub fn validate_agent_and_function_names( &self, component: &ComponentDto, agent_id: &RawAgentId, From 49a16f0a585a6f434def5ab08b775c3da4937f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:16:48 +0200 Subject: [PATCH 25/70] rename cli single-agent wrapper fns to agent, preserving golem_client worker calls --- .../src/command_handler/component/mod.rs | 2 +- .../src/command_handler/worker/mod.rs | 36 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 97f0ea5b03..9e287206cc 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -761,7 +761,7 @@ impl ComponentCommandHandler { ComponentRevisionSelection::ByAgentId(agent_id) => self .ctx .agent_handler() - .worker_metadata(component.id.0, &component.component_name, agent_id) + .agent_metadata(component.id.0, &component.component_name, agent_id) .await? .map(|worker_metadata| worker_metadata.component_revision), ComponentRevisionSelection::ByExplicitRevision(revision) => Some(revision), diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 75381cbd8b..37d175d541 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -292,7 +292,7 @@ impl AgentCommandHandler { format!("new agent {}", format_agent_id_match(&agent_id_match)), ); - self.new_worker( + self.new_agent( component.id.0, agent_id.0.clone(), env.into_iter().collect(), @@ -648,7 +648,7 @@ impl AgentCommandHandler { format!("for agent {}", format_agent_id_match(&agent_id_match)), ); - self.interrupt_worker(&component, &agent_id, true).await?; + self.interrupt_agent(&component, &agent_id, true).await?; log_action( "Simulated crash", @@ -1184,7 +1184,7 @@ impl AgentCommandHandler { format!("agent {}", format_agent_id_match(&agent_id_match)), ); - self.interrupt_worker(&component, &agent_id, false).await?; + self.interrupt_agent(&component, &agent_id, false).await?; log_action( "Interrupted", @@ -1209,7 +1209,7 @@ impl AgentCommandHandler { format!("agent {}", format_agent_id_match(&agent_id_match)), ); - self.resume_worker(&component, &agent_id).await?; + self.resume_agent(&component, &agent_id).await?; log_action( "Resumed", @@ -1271,7 +1271,7 @@ impl AgentCommandHandler { }; let from_revision = self - .worker_metadata(component.id.0, &component.component_name, &agent_id) + .agent_metadata(component.id.0, &component.component_name, &agent_id) .await? .map(|metadata| metadata.component_revision) .unwrap_or(target_revision); @@ -1291,7 +1291,7 @@ impl AgentCommandHandler { let mut update_results = TryUpdateAllWorkersResult::default(); update_results.agents.push(meta); match self - .update_worker( + .update_agent( &component.component_name, &component.id, &agent_id.0, @@ -1724,7 +1724,7 @@ impl AgentCommandHandler { } } - async fn new_worker( + async fn new_agent( &self, component_id: Uuid, agent_id: String, @@ -1750,7 +1750,7 @@ impl AgentCommandHandler { Ok(()) } - pub async fn worker_metadata( + pub async fn agent_metadata( &self, component_id: Uuid, component_name: &ComponentName, @@ -1829,7 +1829,7 @@ impl AgentCommandHandler { let mut update_results = TryUpdateAllWorkersResult::default(); for worker in &workers_to_update { let result = self - .update_worker( + .update_agent( component_name, &worker.agent_id.component_id, &worker.agent_id.agent_id, @@ -1874,7 +1874,7 @@ impl AgentCommandHandler { Ok(update_results) } - async fn update_worker( + async fn update_agent( &self, component_name: &ComponentName, component_id: &ComponentId, @@ -2067,7 +2067,7 @@ impl AgentCommandHandler { for worker in workers { let agent_id: RawAgentId = worker.agent_id.agent_id.as_str().into(); let from_revision = worker.component_revision; - self.redeploy_worker(component_name, worker).await?; + self.redeploy_agent(component_name, worker).await?; redeployed.push((agent_id, from_revision)); } @@ -2115,14 +2115,14 @@ impl AgentCommandHandler { let mut deleted = Vec::with_capacity(workers.len()); for worker in &workers { - self.delete_worker(component_name, worker).await?; + self.delete_agent(component_name, worker).await?; deleted.push(worker.agent_id.agent_id.as_str().into()); } Ok(deleted) } - async fn redeploy_worker( + async fn redeploy_agent( &self, component_name: &ComponentName, worker_metadata: AgentMetadata, @@ -2137,7 +2137,7 @@ impl AgentCommandHandler { ); let _indent = LogIndent::new(); - self.delete_worker(component_name, &worker_metadata).await?; + self.delete_agent(component_name, &worker_metadata).await?; log_action( "Recreating", @@ -2147,7 +2147,7 @@ impl AgentCommandHandler { worker_metadata.agent_id.agent_id.bold().green(), ), ); - self.new_worker( + self.new_agent( worker_metadata.agent_id.component_id.0, worker_metadata.agent_id.agent_id, worker_metadata.env, @@ -2159,7 +2159,7 @@ impl AgentCommandHandler { Ok(()) } - pub async fn delete_worker( + pub async fn delete_agent( &self, component_name: &ComponentName, worker_metadata: &AgentMetadata, @@ -2387,7 +2387,7 @@ pub(crate) fn try_recanonicalize_agent_id_with_parsed( } impl AgentCommandHandler { - async fn resume_worker( + async fn resume_agent( &self, component: &ComponentDto, agent_id: &RawAgentId, @@ -2404,7 +2404,7 @@ impl AgentCommandHandler { Ok(()) } - async fn interrupt_worker( + async fn interrupt_agent( &self, component: &ComponentDto, agent_id: &RawAgentId, From 68e5fd9cb9640cbf8abd92ab778ddc68fab76373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:38:38 +0200 Subject: [PATCH 26/70] rename cli-domain local worker variables to agent --- .../src/command_handler/worker/mod.rs | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/worker/mod.rs index 37d175d541..ba07f77607 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/worker/mod.rs @@ -1067,7 +1067,7 @@ impl AgentCommandHandler { let mut view = AgentsMetadataResponseView::default(); for component in components { - let (workers, component_scan_cursor) = self + let (agents, component_scan_cursor) = self .list_component_agents( &component.component_name, &component.id, @@ -1079,15 +1079,15 @@ impl AgentCommandHandler { ) .await?; - for worker in workers { - let raw_agent_id = worker.agent_id.agent_id.clone(); + for agent in agents { + let raw_agent_id = agent.agent_id.agent_id.clone(); - let worker_component = self + let agent_component = self .ctx .component_handler() .get_component_revision_by_id( - &worker.agent_id.component_id, - worker.component_revision, + &agent.agent_id.component_id, + agent.component_revision, ) .await?; @@ -1095,7 +1095,7 @@ impl AgentCommandHandler { ParsedAgentId::parse_agent_type_name(&raw_agent_id).ok(); let defaults = parsed_agent_type_name.as_ref().and_then(|agent_type_name| { - worker_component + agent_component .metadata .agent_type_provision_configs() .get(agent_type_name) @@ -1105,7 +1105,7 @@ impl AgentCommandHandler { let source_language = parsed_agent_type_name .as_ref() .and_then(|type_name| { - worker_component + agent_component .metadata .agent_types() .iter() @@ -1118,17 +1118,17 @@ impl AgentCommandHandler { .as_ref() .map(|agent_type_name| { secret_config_paths_for_agent_type( - worker_component.metadata.agent_types(), + agent_component.metadata.agent_types(), agent_type_name, ) }) .unwrap_or_default(); - let mut agent_view = AgentMetadataView::from(worker) + let mut agent_view = AgentMetadataView::from(agent) .with_defaults(defaults) .with_secret_config_paths(secret_config_paths); - let parsed = ParsedAgentId::parse(&raw_agent_id, &worker_component.metadata).ok(); + let parsed = ParsedAgentId::parse(&raw_agent_id, &agent_component.metadata).ok(); agent_view.agent_id = crate::agent_id_display::render_agent_id_or_raw( parsed.as_ref(), &source_language, @@ -1794,7 +1794,7 @@ impl AgentCommandHandler { // only consider agents in previous revisions that can be upgraded AgentFilter::new_revision(FilterComparator::Less, target_revision).to_string(), ]; - let (workers_to_update, _) = self + let (agents_to_update, _) = self .list_component_agents( component_name, component_id, @@ -1806,7 +1806,7 @@ impl AgentCommandHandler { ) .await?; - if workers_to_update.is_empty() { + if agents_to_update.is_empty() { return Ok(TryUpdateAllWorkersResult::default()); } @@ -1814,7 +1814,7 @@ impl AgentCommandHandler { "Updating", format!( "all agents ({}) for component {} to revision {}", - workers_to_update.len().to_string().log_color_highlight(), + agents_to_update.len().to_string().log_color_highlight(), component_name.0.blue().bold(), target_revision.to_string().log_color_highlight() ), @@ -1827,12 +1827,12 @@ impl AgentCommandHandler { .component_version_at(component_id, target_revision) .await; let mut update_results = TryUpdateAllWorkersResult::default(); - for worker in &workers_to_update { + for agent in &agents_to_update { let result = self .update_agent( component_name, - &worker.agent_id.component_id, - &worker.agent_id.agent_id, + &agent.agent_id.component_id, + &agent.agent_id.agent_id, update_mode, target_revision, false, @@ -1843,28 +1843,28 @@ impl AgentCommandHandler { if let Err(error) = &result { update_results .errors - .insert(worker.agent_id.agent_id.clone(), error.to_string()); + .insert(agent.agent_id.agent_id.clone(), error.to_string()); } update_results.agents.push(AgentUpdateMeta { component_name: component_name.clone(), - agent_id: worker.agent_id.agent_id.as_str().into(), - from_revision: worker.component_revision, + agent_id: agent.agent_id.agent_id.as_str().into(), + from_revision: agent.component_revision, revision: target_revision, from_version: self .ctx .component_handler() - .component_version_at(component_id, worker.component_revision) + .component_version_at(component_id, agent.component_revision) .await, version: version.clone(), }); } if await_update { - for worker in workers_to_update { + for agent in agents_to_update { let _ = self .await_update_result( - &worker.agent_id.component_id, - &worker.agent_id.agent_id, + &agent.agent_id.component_id, + &agent.agent_id.agent_id, target_revision, ) .await; @@ -2033,11 +2033,11 @@ impl AgentCommandHandler { component_name: &ComponentName, component_id: &ComponentId, ) -> anyhow::Result> { - let (workers, _) = self + let (agents, _) = self .list_component_agents(component_name, component_id, None, None, None, None, false) .await?; - if workers.is_empty() { + if agents.is_empty() { log_warn_action( "Skipping", format!("redeploying agents for component {component_name}, no agent found"), @@ -2049,7 +2049,7 @@ impl AgentCommandHandler { "Redeploying", format!( "all agents ({}) for component {}", - workers.len().to_string().log_color_highlight(), + agents.len().to_string().log_color_highlight(), component_name.0.blue().bold(), ), ); @@ -2058,16 +2058,16 @@ impl AgentCommandHandler { if !self .ctx .interactive_handler() - .confirm_redeploy_agents(workers.len())? + .confirm_redeploy_agents(agents.len())? { bail!(NonSuccessfulExit); } - let mut redeployed = Vec::with_capacity(workers.len()); - for worker in workers { - let agent_id: RawAgentId = worker.agent_id.agent_id.as_str().into(); - let from_revision = worker.component_revision; - self.redeploy_agent(component_name, worker).await?; + let mut redeployed = Vec::with_capacity(agents.len()); + for agent in agents { + let agent_id: RawAgentId = agent.agent_id.agent_id.as_str().into(); + let from_revision = agent.component_revision; + self.redeploy_agent(component_name, agent).await?; redeployed.push((agent_id, from_revision)); } @@ -2081,11 +2081,11 @@ impl AgentCommandHandler { component_id: &ComponentId, show_skip: bool, ) -> anyhow::Result> { - let (workers, _) = self + let (agents, _) = self .list_component_agents(component_name, component_id, None, None, None, None, false) .await?; - if workers.is_empty() { + if agents.is_empty() { if show_skip { log_warn_action( "Skipping", @@ -2099,7 +2099,7 @@ impl AgentCommandHandler { "Deleting", format!( "all agents ({}) for component {}", - workers.len().to_string().log_color_highlight(), + agents.len().to_string().log_color_highlight(), component_name.0.blue().bold(), ), ); @@ -2108,15 +2108,15 @@ impl AgentCommandHandler { if !self .ctx .interactive_handler() - .confirm_deleting_agents(workers.len())? + .confirm_deleting_agents(agents.len())? { bail!(NonSuccessfulExit); } - let mut deleted = Vec::with_capacity(workers.len()); - for worker in &workers { - self.delete_agent(component_name, worker).await?; - deleted.push(worker.agent_id.agent_id.as_str().into()); + let mut deleted = Vec::with_capacity(agents.len()); + for agent in &agents { + self.delete_agent(component_name, agent).await?; + deleted.push(agent.agent_id.agent_id.as_str().into()); } Ok(deleted) @@ -2125,33 +2125,33 @@ impl AgentCommandHandler { async fn redeploy_agent( &self, component_name: &ComponentName, - worker_metadata: AgentMetadata, + agent_metadata: AgentMetadata, ) -> anyhow::Result<()> { log_warn_action( "Redeploying", format!( "agent {}/{} to current version", component_name.0.bold().blue(), - worker_metadata.agent_id.agent_id.bold().green(), + agent_metadata.agent_id.agent_id.bold().green(), ), ); let _indent = LogIndent::new(); - self.delete_agent(component_name, &worker_metadata).await?; + self.delete_agent(component_name, &agent_metadata).await?; log_action( "Recreating", format!( "agent {}/{}", component_name.0.bold().blue(), - worker_metadata.agent_id.agent_id.bold().green(), + agent_metadata.agent_id.agent_id.bold().green(), ), ); self.new_agent( - worker_metadata.agent_id.component_id.0, - worker_metadata.agent_id.agent_id, - worker_metadata.env, - worker_metadata.config, + agent_metadata.agent_id.component_id.0, + agent_metadata.agent_id.agent_id, + agent_metadata.env, + agent_metadata.config, ) .await?; log_action("Recreated", "agent"); @@ -2162,19 +2162,19 @@ impl AgentCommandHandler { pub async fn delete_agent( &self, component_name: &ComponentName, - worker_metadata: &AgentMetadata, + agent_metadata: &AgentMetadata, ) -> anyhow::Result<()> { log_warn_action( "Deleting", format!( "agent {}/{}", component_name.0.bold().blue(), - worker_metadata.agent_id.agent_id.bold().green(), + agent_metadata.agent_id.agent_id.bold().green(), ), ); self.delete( - worker_metadata.agent_id.component_id.0, - &worker_metadata.agent_id.agent_id, + agent_metadata.agent_id.component_id.0, + &agent_metadata.agent_id.agent_id, ) .await?; log_action("Deleted", "agent"); @@ -2192,7 +2192,7 @@ impl AgentCommandHandler { precise: bool, ) -> anyhow::Result<(Vec, Option)> { let clients = self.ctx.golem_clients().await?; - let mut workers = Vec::::new(); + let mut agents = Vec::::new(); let mut final_result_cursor = Option::::None; // The structured `find_workers_metadata` POST endpoint is used for all @@ -2231,7 +2231,7 @@ impl AgentCommandHandler { .await .map_service_error()?; - workers.extend( + agents.extend( results .workers .into_iter() @@ -2254,7 +2254,7 @@ impl AgentCommandHandler { } } - Ok((workers, final_result_cursor)) + Ok((agents, final_result_cursor)) } pub(crate) async fn component_by_agent_id_match( From 13605115cbca04702cc1d5860c40e7d3ed6e43a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:42:59 +0200 Subject: [PATCH 27/70] move the worker command handler module to command_handler/agent --- cli/golem-cli/src/command_handler/{worker => agent}/mod.rs | 2 +- cli/golem-cli/src/command_handler/{worker => agent}/stream.rs | 4 ++-- .../src/command_handler/{worker => agent}/stream_output.rs | 0 cli/golem-cli/src/command_handler/card.rs | 2 +- cli/golem-cli/src/command_handler/mod.rs | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename cli/golem-cli/src/command_handler/{worker => agent}/mod.rs (99%) rename cli/golem-cli/src/command_handler/{worker => agent}/stream.rs (99%) rename cli/golem-cli/src/command_handler/{worker => agent}/stream_output.rs (100%) diff --git a/cli/golem-cli/src/command_handler/worker/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs similarity index 99% rename from cli/golem-cli/src/command_handler/worker/mod.rs rename to cli/golem-cli/src/command_handler/agent/mod.rs index ba07f77607..b35f97d845 100644 --- a/cli/golem-cli/src/command_handler/worker/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -20,7 +20,7 @@ use crate::command::shared_args::{ }; use crate::command::worker::AgentSubcommand; use crate::command_handler::Handlers; -use crate::command_handler::worker::stream::AgentConnection; +use crate::command_handler::agent::stream::AgentConnection; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::{MapServiceError, ServiceError}; diff --git a/cli/golem-cli/src/command_handler/worker/stream.rs b/cli/golem-cli/src/command_handler/agent/stream.rs similarity index 99% rename from cli/golem-cli/src/command_handler/worker/stream.rs rename to cli/golem-cli/src/command_handler/agent/stream.rs index 3c9b253019..0b620ef295 100644 --- a/cli/golem-cli/src/command_handler/worker/stream.rs +++ b/cli/golem-cli/src/command_handler/agent/stream.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::command_handler::worker::parse_worker_error; -use crate::command_handler::worker::stream_output::AgentStreamOutput; +use crate::command_handler::agent::parse_worker_error; +use crate::command_handler::agent::stream_output::AgentStreamOutput; use crate::model::format::Format; use crate::model::worker::AgentLogStreamOptions; use anyhow::{Context, anyhow}; diff --git a/cli/golem-cli/src/command_handler/worker/stream_output.rs b/cli/golem-cli/src/command_handler/agent/stream_output.rs similarity index 100% rename from cli/golem-cli/src/command_handler/worker/stream_output.rs rename to cli/golem-cli/src/command_handler/agent/stream_output.rs diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 910863fc63..c9d7053adf 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -14,7 +14,7 @@ use crate::command::card::CardSubcommand; use crate::command_handler::Handlers; -use crate::command_handler::worker::AgentCommandHandler; +use crate::command_handler::agent::AgentCommandHandler; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; diff --git a/cli/golem-cli/src/command_handler/mod.rs b/cli/golem-cli/src/command_handler/mod.rs index 3cd75ca597..b4902aa240 100644 --- a/cli/golem-cli/src/command_handler/mod.rs +++ b/cli/golem-cli/src/command_handler/mod.rs @@ -23,6 +23,7 @@ use crate::command::{ GolemCliSubcommand, }; use crate::command_handler::account::AccountCommandHandler; +use crate::command_handler::agent::AgentCommandHandler; use crate::command_handler::api::ApiCommandHandler; use crate::command_handler::api::deployment::ApiDeploymentCommandHandler; use crate::command_handler::api::domain::ApiDomainCommandHandler; @@ -41,7 +42,6 @@ use crate::command_handler::plugin::PluginCommandHandler; use crate::command_handler::profile::ProfileCommandHandler; use crate::command_handler::profile::config::ProfileConfigCommandHandler; use crate::command_handler::repl::ReplHandler; -use crate::command_handler::worker::AgentCommandHandler; use crate::context::Context; use crate::error::{ContextInitHintError, HintError, NonSuccessfulExit, PipedExitCode}; use crate::log::{Output, log_anyhow_error, logln, set_log_output}; @@ -60,6 +60,7 @@ use std::sync::Arc; use tracing::{Level, debug}; mod account; +mod agent; mod api; mod api_token; mod app; @@ -77,7 +78,6 @@ mod resource_definition; mod retry_policy; mod secret; pub(crate) mod template; -mod worker; // NOTE: We are explicitly not using #[async_trait] here to be able to NOT have a Send bound // on the `handler_server_commands` method. Having a Send bound there causes "Send is not generic enough" From 476e819319d461b648531602fd963b18c44fa319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:48:41 +0200 Subject: [PATCH 28/70] move the agent-instance model modules from worker to agent_instance --- cli/golem-cli/src/command.rs | 16 +++--- .../src/command_handler/agent/mod.rs | 16 +++--- .../src/command_handler/agent/stream.rs | 2 +- .../command_handler/agent/stream_output.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 7 ++- cli/golem-cli/src/command_handler/card.rs | 2 +- .../src/command_handler/component/mod.rs | 2 +- .../src/command_handler/interactive.rs | 2 +- cli/golem-cli/src/model/agent/stream.rs | 4 +- .../model/{worker.rs => agent_instance.rs} | 0 cli/golem-cli/src/model/cli_output.rs | 50 +++++++++---------- cli/golem-cli/src/model/component.rs | 2 +- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/mod.rs | 2 +- cli/golem-cli/src/model/text/action_result.rs | 2 +- .../text/{worker.rs => agent_instance.rs} | 6 +-- cli/golem-cli/src/model/text/mod.rs | 2 +- 17 files changed, 61 insertions(+), 58 deletions(-) rename cli/golem-cli/src/model/{worker.rs => agent_instance.rs} (100%) rename cli/golem-cli/src/model/text/{worker.rs => agent_instance.rs} (99%) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 429e00ba51..a92b8a23c5 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -35,12 +35,12 @@ use crate::command::worker::AgentSubcommand; use crate::config::ProfileName; use crate::error::ShowClapHelpTarget; use crate::model::GuestLanguage; +use crate::model::agent_instance::{AgentUpdateMode, RawAgentId}; use crate::model::app::ComponentPresetName; use crate::model::cli_command_metadata::{CliCommandMetadata, CliMetadataFilter}; use crate::model::environment::EnvironmentReference; use crate::model::format::Format; use crate::model::repl::ReplLanguage; -use crate::model::worker::{AgentUpdateMode, RawAgentId}; use crate::{command_name, version}; use anyhow::{Context as AnyhowContext, anyhow}; use clap::error::{ContextKind, ContextValue, ErrorKind}; @@ -941,8 +941,8 @@ pub enum GolemCliSubcommand { pub mod shared_args { use crate::model::GuestLanguage; + use crate::model::agent_instance::{AgentUpdateMode, RawAgentId}; use crate::model::app::AppBuildStep; - use crate::model::worker::{AgentUpdateMode, RawAgentId}; use clap::Args; use golem_common::model::account::AccountId; use golem_common::model::component::ComponentName; @@ -1169,7 +1169,7 @@ pub mod environment { pub mod component { use crate::command::shared_args::{OptionalComponentName, OptionalComponentNames}; - use crate::model::worker::AgentUpdateMode; + use crate::model::agent_instance::AgentUpdateMode; use clap::Subcommand; use golem_common::model::component::ComponentRevision; @@ -1281,7 +1281,7 @@ pub mod worker { use crate::command::shared_args::{ AgentFunctionArgument, AgentFunctionName, AgentIdArgs, PostDeployArgs, StreamArgs, }; - use crate::model::worker::{AgentListMode, AgentUpdateMode}; + use crate::model::agent_instance::{AgentListMode, AgentUpdateMode}; use chrono::{DateTime, Utc}; use clap::Subcommand; use golem_client::model::ScanCursor; @@ -2341,7 +2341,7 @@ pub mod account { pub mod card { use crate::command::shared_args::AccountIdOptionalArg; - use crate::model::worker::RawAgentId; + use crate::model::agent_instance::RawAgentId; use clap::Subcommand; use golem_common::model::card::CardId; @@ -2510,7 +2510,7 @@ mod test { help_target_to_subcommand_names, }; use crate::error::ShowClapHelpTarget; - use crate::model::worker::AgentUpdateMode; + use crate::model::agent_instance::AgentUpdateMode; use clap::builder::StyledStr; use clap::{Command, CommandFactory}; use itertools::Itertools; @@ -2734,7 +2734,7 @@ mod test { #[test] fn update_agents_accepts_auto_as_update_mode_alias() { - use crate::model::worker::AgentUpdateMode; + use crate::model::agent_instance::AgentUpdateMode; use clap::Parser; let result = @@ -2755,7 +2755,7 @@ mod test { #[test] fn update_agents_accepts_automatic_as_update_mode() { - use crate::model::worker::AgentUpdateMode; + use crate::model::agent_instance::AgentUpdateMode; use clap::Parser; let result = GolemCliCommand::try_parse_from([ diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index b35f97d845..a337a5c2d2 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -36,27 +36,27 @@ use crate::model::text::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, }; +use crate::model::text::agent_instance::{ + AgentCreateView, AgentFilesView, AgentGetView, AgentOplogEntryView, FileNodeView, + format_agent_id_match, format_timestamp, +}; use crate::model::text::fmt::{log_fuzzy_match, log_text_view}; use crate::model::text::help::{ AgentNameHelp, ArgumentError, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, ParameterErrorTableView, }; -use crate::model::text::worker::{ - AgentCreateView, AgentFilesView, AgentGetView, AgentOplogEntryView, FileNodeView, - format_agent_id_match, format_timestamp, -}; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; use colored::Colorize; use crate::agent_id_display::SourceLanguage; -use crate::model::environment::{ - EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, -}; -use crate::model::worker::{ +use crate::model::agent_instance::{ AgentIdMatch, AgentListMode, AgentMetadata, AgentMetadataView, AgentUpdateMode, AgentsMetadataResponseView, RawAgentId, }; +use crate::model::environment::{ + EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, +}; use golem_client::api::{AgentClient, ComponentClient, WorkerClient}; use golem_client::model::ScanCursor; use golem_client::model::{ diff --git a/cli/golem-cli/src/command_handler/agent/stream.rs b/cli/golem-cli/src/command_handler/agent/stream.rs index 0b620ef295..a03ba014c1 100644 --- a/cli/golem-cli/src/command_handler/agent/stream.rs +++ b/cli/golem-cli/src/command_handler/agent/stream.rs @@ -14,8 +14,8 @@ use crate::command_handler::agent::parse_worker_error; use crate::command_handler::agent::stream_output::AgentStreamOutput; +use crate::model::agent_instance::AgentLogStreamOptions; use crate::model::format::Format; -use crate::model::worker::AgentLogStreamOptions; use anyhow::{Context, anyhow}; use bytes::Bytes; use futures_util::future::Either; diff --git a/cli/golem-cli/src/command_handler/agent/stream_output.rs b/cli/golem-cli/src/command_handler/agent/stream_output.rs index 9cdfac75bc..0eafd60ea1 100644 --- a/cli/golem-cli/src/command_handler/agent/stream_output.rs +++ b/cli/golem-cli/src/command_handler/agent/stream_output.rs @@ -15,8 +15,8 @@ use crate::command_handler::log::print_command_output_document; use crate::log::log_error; use crate::model::agent::stream::AgentStreamEvent; +use crate::model::agent_instance::AgentLogStreamOptions; use crate::model::format::Format; -use crate::model::worker::AgentLogStreamOptions; use colored::Colorize; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; use std::cmp::Ordering; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 18e8565dfe..ff215a1d12 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -41,6 +41,7 @@ use crate::log::{ log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; use crate::model::agent::view::AgentTypeView; +use crate::model::agent_instance::AgentUpdateMode; use crate::model::app::{ AppBuildStep, ApplicationComponentSelectMode, BuildConfig, CleanMode, DynamicHelpSections, WithSource, @@ -59,7 +60,6 @@ use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; use crate::model::text::help::AvailableComponentNamesHelp; use crate::model::text::server::ToFormattedServerContext; use crate::model::text::template::TemplateListView; -use crate::model::worker::AgentUpdateMode; use crate::model::{GuestLanguage, TemplateDescription}; use anyhow::{anyhow, bail}; use colored::Colorize; @@ -2677,7 +2677,10 @@ impl AppCommandHandler { format!( " - {} matched by {}", option.bold(), - patterns.iter().map(|p| p.as_str().bold().to_string()).join(", ") + patterns + .iter() + .map(|p| p.as_str().bold().to_string()) + .join(", ") ) }) .join("\n") diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index c9d7053adf..511ab37e27 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -19,8 +19,8 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_warn_action; +use crate::model::agent_instance::RawAgentId; use crate::model::text::card::{CardGetView, CardListView, CardRevokeResult}; -use crate::model::worker::RawAgentId; use anyhow::bail; use golem_client::api::{CardClient, WorkerClient}; use golem_common::model::account::AccountId; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 9e287206cc..60de6129cf 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -25,6 +25,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_error, log_warn_action, logln}; use crate::model::GuestLanguage; +use crate::model::agent_instance::AgentUpdateMode; use crate::model::app::BuildConfig; use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; use crate::model::app_raw; @@ -50,7 +51,6 @@ use crate::model::text::component::{ use crate::model::text::fmt::log_text_view; use crate::model::text::help::ComponentNameHelp; use crate::model::text::plugin::PluginNameAndVersion; -use crate::model::worker::AgentUpdateMode; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; use futures_util::future::OptionFuture; diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index d63fe0250c..a4cde37975 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -17,9 +17,9 @@ use crate::config::{AuthenticationConfig, Profile, ProfileConfig, ProfileName}; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::log::{LogColorize, log_error, log_warn, log_warn_action, logln}; +use crate::model::agent_instance::RawAgentId; use crate::model::format::Format; use crate::model::repl::ReplLanguage; -use crate::model::worker::RawAgentId; use anyhow::bail; use colored::Colorize; use golem_client::model::Account; diff --git a/cli/golem-cli/src/model/agent/stream.rs b/cli/golem-cli/src/model/agent/stream.rs index b5e85debef..c8ae6f1ade 100644 --- a/cli/golem-cli/src/model/agent/stream.rs +++ b/cli/golem-cli/src/model/agent/stream.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::model::agent_instance::AgentLogStreamOptions; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::format_stderr; -use crate::model::worker::AgentLogStreamOptions; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; use serde::{Deserialize, Serialize}; @@ -259,7 +259,7 @@ impl AgentStreamEvent { #[cfg(test)] mod tests { use super::{AgentStreamEvent, AgentStreamEventKind}; - use crate::model::worker::AgentLogStreamOptions; + use crate::model::agent_instance::AgentLogStreamOptions; use golem_common::model::{IdempotencyKey, Timestamp}; use std::str::FromStr; use test_r::test; diff --git a/cli/golem-cli/src/model/worker.rs b/cli/golem-cli/src/model/agent_instance.rs similarity index 100% rename from cli/golem-cli/src/model/worker.rs rename to cli/golem-cli/src/model/agent_instance.rs diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index 5eb44a54bf..d4ed3ca1ca 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -902,7 +902,7 @@ mod tests { #[test] fn agent_list_structured_output_masks_secret_config_paths() { - let output = crate::model::worker::AgentsMetadataResponseView { + let output = crate::model::agent_instance::AgentsMetadataResponseView { agents: vec![sample_agent_metadata_view()], cursors: BTreeMap::new(), }; @@ -920,7 +920,7 @@ mod tests { #[test] fn agent_get_structured_output_masks_secret_config_paths() { - let value = hidden_structured_output(crate::model::text::worker::AgentGetView { + let value = hidden_structured_output(crate::model::text::agent_instance::AgentGetView { metadata: sample_agent_metadata_view(), precise: true, }); @@ -1013,10 +1013,10 @@ mod tests { } } - fn sample_agent_metadata_view() -> crate::model::worker::AgentMetadataView { - crate::model::worker::AgentMetadataView { + fn sample_agent_metadata_view() -> crate::model::agent_instance::AgentMetadataView { + crate::model::agent_instance::AgentMetadataView { component_name: golem_common::model::component::ComponentName("component".to_string()), - agent_id: crate::model::worker::RawAgentId("agent()".to_string()), + agent_id: crate::model::agent_instance::RawAgentId("agent()".to_string()), created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), env: BTreeMap::new().into_iter().collect(), @@ -1373,7 +1373,7 @@ mod tests { agent_types: vec![agent_type], }) .expect("agent-type.list should serialize"), - to_structured_output_value(crate::model::text::worker::AgentOplogEntryView { + to_structured_output_value(crate::model::text::agent_instance::AgentOplogEntryView { index: 0, entry: sample_public_oplog_entries() .into_iter() @@ -2851,11 +2851,11 @@ mod tests { fn arb_agent_files_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::worker::AgentFilesView { nodes }), + .prop_map(|nodes| crate::model::text::agent_instance::AgentFilesView { nodes }), ) } - fn arb_file_node() -> BoxedStrategy { + fn arb_file_node() -> BoxedStrategy { ( arb_small_string(), arb_small_string(), @@ -2864,7 +2864,7 @@ mod tests { arb_small_u64(), ) .prop_map(|(name, last_modified, kind, permissions, size)| { - crate::model::text::worker::FileNodeView { + crate::model::text::agent_instance::FileNodeView { name, last_modified, kind, @@ -2877,7 +2877,10 @@ mod tests { fn arb_agent_get_result() -> OutputDocumentStrategy { serialized_output((arb_agent_metadata_view(), any::()).prop_map( - |(metadata, precise)| crate::model::text::worker::AgentGetView { metadata, precise }, + |(metadata, precise)| crate::model::text::agent_instance::AgentGetView { + metadata, + precise, + }, )) } @@ -2914,12 +2917,9 @@ mod tests { proptest::collection::vec(arb_agent_metadata_view(), 0..5), proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), ) - .prop_map( - |(agents, cursors)| crate::model::worker::AgentsMetadataResponseView { - agents, - cursors, - }, - ) + .prop_map(|(agents, cursors)| { + crate::model::agent_instance::AgentsMetadataResponseView { agents, cursors } + }) .prop_map(|output| { to_structured_output_value(output).expect("generated DTO should serialize") }) @@ -2928,9 +2928,9 @@ mod tests { fn arb_agent_new_result() -> OutputDocumentStrategy { serialized_output((arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_id)| crate::model::text::worker::AgentCreateView { + |(component_name, agent_id)| crate::model::text::agent_instance::AgentCreateView { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_id), + agent_id: crate::model::agent_instance::RawAgentId(agent_id), }, )) } @@ -2945,7 +2945,7 @@ mod tests { ], ) .prop_map(|(index, entry)| { - crate::model::text::worker::AgentOplogEntryView { index, entry } + crate::model::text::agent_instance::AgentOplogEntryView { index, entry } }), ) } @@ -3040,7 +3040,7 @@ mod tests { /// (`AgentUpdateMeta` / `AgentRedeploymentMeta`). fn arb_agent_transition_fields() -> BoxedStrategy<( golem_common::model::component::ComponentName, - crate::model::worker::RawAgentId, + crate::model::agent_instance::RawAgentId, golem_common::model::component::ComponentRevision, golem_common::model::component::ComponentRevision, Option, @@ -3058,7 +3058,7 @@ mod tests { |(component_name, agent_id, from_revision, revision, from_version, version)| { ( golem_common::model::component::ComponentName(component_name), - crate::model::worker::RawAgentId(agent_id), + crate::model::agent_instance::RawAgentId(agent_id), golem_common::model::component::ComponentRevision::new(from_revision) .expect("generated revision should be valid"), golem_common::model::component::ComponentRevision::new(revision) @@ -3112,13 +3112,13 @@ mod tests { .prop_map(|(component_name, agent_id)| { crate::model::text::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_id), + agent_id: crate::model::agent_instance::RawAgentId(agent_id), } }) .boxed() } - fn arb_agent_metadata_view() -> BoxedStrategy { + fn arb_agent_metadata_view() -> BoxedStrategy { ( ( arb_small_string(), @@ -3171,9 +3171,9 @@ mod tests { exported_resource_instances, ) = right; - crate::model::worker::AgentMetadataView { + crate::model::agent_instance::AgentMetadataView { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::worker::RawAgentId(agent_id), + agent_id: crate::model::agent_instance::RawAgentId(agent_id), created_by: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), ), diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index c4671a3dd5..9d80fa6beb 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -14,12 +14,12 @@ use crate::agent_id_display::SourceLanguage; use crate::agent_id_display::render_type_for_language; +use crate::model::agent_instance::RawAgentId; use crate::model::app_raw; use crate::model::environment::ResolvedEnvironmentIdentity; use crate::model::masking::{ Masked, MaskingConfig, mask_sensitive_map, mask_typed_agent_config_entries, }; -use crate::model::worker::RawAgentId; use chrono::{DateTime, Utc}; use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; use golem_common::model::agent::{AgentConfigSource, AgentTypeName}; diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index 1f1691d3ed..21e5979c01 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -16,13 +16,13 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::command::shared_args::{ForceBuildArg, PostDeployArgs}; use crate::error::service::ServiceError; use crate::model::GuestLanguage; +use crate::model::agent_instance::RawAgentId; use crate::model::component::{ render_agent_constructor, render_input_schema, render_output_schema, }; use crate::model::masking::{ MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, }; -use crate::model::worker::RawAgentId; use golem_client::model::{AgentSecretDto, RetryPolicyDto}; use golem_common::model::agent::{ AgentConfigSource, HttpEndpointDetails, HttpMethod, HttpMountDetails, PathSegment, diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 7564001414..1c25f783a0 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod agent; +pub mod agent_instance; pub mod app; pub mod app_raw; pub mod cascade; @@ -31,7 +32,6 @@ pub mod plugin_manifest; pub mod repl; pub mod template; pub mod text; -pub mod worker; use crate::app::template::AppTemplate; use crate::config::AuthenticationConfig; diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs index aeaa2bcb48..03024eacbb 100644 --- a/cli/golem-cli/src/model/text/action_result.rs +++ b/cli/golem-cli/src/model/text/action_result.rs @@ -22,9 +22,9 @@ //! to stderr (see `Context::new`) and these structured payloads are //! emitted on stdout so that automation can rely on a stable schema. +use crate::model::agent_instance::RawAgentId; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::{NoTextOutput, TextOutput}; -use crate::model::worker::RawAgentId; use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/agent_instance.rs similarity index 99% rename from cli/golem-cli/src/model/text/worker.rs rename to cli/golem-cli/src/model/text/agent_instance.rs index 9799da89f5..13a218f4e8 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/agent_instance.rs @@ -14,15 +14,15 @@ use crate::agent_id_display::SourceLanguage; use crate::log::{LogColorize, logln}; +use crate::model::agent_instance::{ + AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, +}; use crate::model::cli_output::StructuredOutput; use crate::model::deploy::TryUpdateAllWorkersResult; use crate::model::environment::EnvironmentReference; use crate::model::invoke_result_view::InvokeResultView; use crate::model::masking::{Masked, MaskingConfig}; use crate::model::text::fmt::*; -use crate::model::worker::{ - AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, -}; use base64::Engine; use base64::prelude::BASE64_STANDARD; use chrono::DateTime; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 38ae635cff..0d0bf75c6c 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -15,6 +15,7 @@ pub mod account; pub mod action_result; pub mod agent; +pub mod agent_instance; pub mod card; pub mod component; pub mod deployment; @@ -33,4 +34,3 @@ pub mod secret; pub mod server; pub mod template; pub mod token; -pub mod worker; From 49f445db303ffbd9dcf4003d838b0a343177ac56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 16:58:42 +0200 Subject: [PATCH 29/70] fold the agent-instance model into the agent module and re-export it --- cli/golem-cli/src/command.rs | 16 +++++----- .../src/command_handler/agent/mod.rs | 2 +- .../src/command_handler/agent/stream.rs | 2 +- .../command_handler/agent/stream_output.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/command_handler/card.rs | 2 +- .../src/command_handler/component/mod.rs | 2 +- .../src/command_handler/interactive.rs | 2 +- .../{agent_instance.rs => agent/instance.rs} | 5 --- cli/golem-cli/src/model/agent/mod.rs | 3 ++ cli/golem-cli/src/model/agent/stream.rs | 4 +-- cli/golem-cli/src/model/cli_output.rs | 31 ++++++++++--------- cli/golem-cli/src/model/component.rs | 2 +- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/mod.rs | 1 - cli/golem-cli/src/model/text/action_result.rs | 2 +- .../src/model/text/agent_instance.rs | 2 +- 17 files changed, 41 insertions(+), 41 deletions(-) rename cli/golem-cli/src/model/{agent_instance.rs => agent/instance.rs} (99%) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index a92b8a23c5..5ad03fce63 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -35,7 +35,7 @@ use crate::command::worker::AgentSubcommand; use crate::config::ProfileName; use crate::error::ShowClapHelpTarget; use crate::model::GuestLanguage; -use crate::model::agent_instance::{AgentUpdateMode, RawAgentId}; +use crate::model::agent::{AgentUpdateMode, RawAgentId}; use crate::model::app::ComponentPresetName; use crate::model::cli_command_metadata::{CliCommandMetadata, CliMetadataFilter}; use crate::model::environment::EnvironmentReference; @@ -941,7 +941,7 @@ pub enum GolemCliSubcommand { pub mod shared_args { use crate::model::GuestLanguage; - use crate::model::agent_instance::{AgentUpdateMode, RawAgentId}; + use crate::model::agent::{AgentUpdateMode, RawAgentId}; use crate::model::app::AppBuildStep; use clap::Args; use golem_common::model::account::AccountId; @@ -1169,7 +1169,7 @@ pub mod environment { pub mod component { use crate::command::shared_args::{OptionalComponentName, OptionalComponentNames}; - use crate::model::agent_instance::AgentUpdateMode; + use crate::model::agent::AgentUpdateMode; use clap::Subcommand; use golem_common::model::component::ComponentRevision; @@ -1281,7 +1281,7 @@ pub mod worker { use crate::command::shared_args::{ AgentFunctionArgument, AgentFunctionName, AgentIdArgs, PostDeployArgs, StreamArgs, }; - use crate::model::agent_instance::{AgentListMode, AgentUpdateMode}; + use crate::model::agent::{AgentListMode, AgentUpdateMode}; use chrono::{DateTime, Utc}; use clap::Subcommand; use golem_client::model::ScanCursor; @@ -2341,7 +2341,7 @@ pub mod account { pub mod card { use crate::command::shared_args::AccountIdOptionalArg; - use crate::model::agent_instance::RawAgentId; + use crate::model::agent::RawAgentId; use clap::Subcommand; use golem_common::model::card::CardId; @@ -2510,7 +2510,7 @@ mod test { help_target_to_subcommand_names, }; use crate::error::ShowClapHelpTarget; - use crate::model::agent_instance::AgentUpdateMode; + use crate::model::agent::AgentUpdateMode; use clap::builder::StyledStr; use clap::{Command, CommandFactory}; use itertools::Itertools; @@ -2734,7 +2734,7 @@ mod test { #[test] fn update_agents_accepts_auto_as_update_mode_alias() { - use crate::model::agent_instance::AgentUpdateMode; + use crate::model::agent::AgentUpdateMode; use clap::Parser; let result = @@ -2755,7 +2755,7 @@ mod test { #[test] fn update_agents_accepts_automatic_as_update_mode() { - use crate::model::agent_instance::AgentUpdateMode; + use crate::model::agent::AgentUpdateMode; use clap::Parser; let result = GolemCliCommand::try_parse_from([ diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index a337a5c2d2..3c392b2eba 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -50,7 +50,7 @@ use chrono::{DateTime, Utc}; use colored::Colorize; use crate::agent_id_display::SourceLanguage; -use crate::model::agent_instance::{ +use crate::model::agent::{ AgentIdMatch, AgentListMode, AgentMetadata, AgentMetadataView, AgentUpdateMode, AgentsMetadataResponseView, RawAgentId, }; diff --git a/cli/golem-cli/src/command_handler/agent/stream.rs b/cli/golem-cli/src/command_handler/agent/stream.rs index a03ba014c1..7e36a96244 100644 --- a/cli/golem-cli/src/command_handler/agent/stream.rs +++ b/cli/golem-cli/src/command_handler/agent/stream.rs @@ -14,7 +14,7 @@ use crate::command_handler::agent::parse_worker_error; use crate::command_handler::agent::stream_output::AgentStreamOutput; -use crate::model::agent_instance::AgentLogStreamOptions; +use crate::model::agent::AgentLogStreamOptions; use crate::model::format::Format; use anyhow::{Context, anyhow}; use bytes::Bytes; diff --git a/cli/golem-cli/src/command_handler/agent/stream_output.rs b/cli/golem-cli/src/command_handler/agent/stream_output.rs index 0eafd60ea1..42ac65d7ef 100644 --- a/cli/golem-cli/src/command_handler/agent/stream_output.rs +++ b/cli/golem-cli/src/command_handler/agent/stream_output.rs @@ -14,8 +14,8 @@ use crate::command_handler::log::print_command_output_document; use crate::log::log_error; +use crate::model::agent::AgentLogStreamOptions; use crate::model::agent::stream::AgentStreamEvent; -use crate::model::agent_instance::AgentLogStreamOptions; use crate::model::format::Format; use colored::Colorize; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index ff215a1d12..36137a6d29 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -40,8 +40,8 @@ use crate::log::{ log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; +use crate::model::agent::AgentUpdateMode; use crate::model::agent::view::AgentTypeView; -use crate::model::agent_instance::AgentUpdateMode; use crate::model::app::{ AppBuildStep, ApplicationComponentSelectMode, BuildConfig, CleanMode, DynamicHelpSections, WithSource, diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 511ab37e27..a31160aaad 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -19,7 +19,7 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_warn_action; -use crate::model::agent_instance::RawAgentId; +use crate::model::agent::RawAgentId; use crate::model::text::card::{CardGetView, CardListView, CardRevokeResult}; use anyhow::bail; use golem_client::api::{CardClient, WorkerClient}; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 60de6129cf..cf32f7107e 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -25,7 +25,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_error, log_warn_action, logln}; use crate::model::GuestLanguage; -use crate::model::agent_instance::AgentUpdateMode; +use crate::model::agent::AgentUpdateMode; use crate::model::app::BuildConfig; use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; use crate::model::app_raw; diff --git a/cli/golem-cli/src/command_handler/interactive.rs b/cli/golem-cli/src/command_handler/interactive.rs index a4cde37975..604d214f47 100644 --- a/cli/golem-cli/src/command_handler/interactive.rs +++ b/cli/golem-cli/src/command_handler/interactive.rs @@ -17,7 +17,7 @@ use crate::config::{AuthenticationConfig, Profile, ProfileConfig, ProfileName}; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::log::{LogColorize, log_error, log_warn, log_warn_action, logln}; -use crate::model::agent_instance::RawAgentId; +use crate::model::agent::RawAgentId; use crate::model::format::Format; use crate::model::repl::ReplLanguage; use anyhow::bail; diff --git a/cli/golem-cli/src/model/agent_instance.rs b/cli/golem-cli/src/model/agent/instance.rs similarity index 99% rename from cli/golem-cli/src/model/agent_instance.rs rename to cli/golem-cli/src/model/agent/instance.rs index c2a583dfc5..c2c76b564e 100644 --- a/cli/golem-cli/src/model/agent_instance.rs +++ b/cli/golem-cli/src/model/agent/instance.rs @@ -20,7 +20,6 @@ use crate::model::environment::{ }; use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; use clap::ValueEnum; -use clap_verbosity_flag::Verbosity; use colored::control::SHOULD_COLORIZE; use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; use golem_common::model::account::AccountId; @@ -270,10 +269,6 @@ impl Masked for AgentsMetadataResponseView { } } -pub trait HasVerbosity { - fn verbosity(&self) -> Verbosity; -} - #[derive(Debug, Clone)] pub struct AgentLogStreamOptions { pub colors: bool, diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index d11839aacc..a3b557a399 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -13,5 +13,8 @@ // limitations under the License. pub mod extraction; +pub mod instance; pub mod stream; pub mod view; + +pub use instance::*; diff --git a/cli/golem-cli/src/model/agent/stream.rs b/cli/golem-cli/src/model/agent/stream.rs index c8ae6f1ade..44b345d159 100644 --- a/cli/golem-cli/src/model/agent/stream.rs +++ b/cli/golem-cli/src/model/agent/stream.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::agent_instance::AgentLogStreamOptions; +use crate::model::agent::AgentLogStreamOptions; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::format_stderr; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; @@ -259,7 +259,7 @@ impl AgentStreamEvent { #[cfg(test)] mod tests { use super::{AgentStreamEvent, AgentStreamEventKind}; - use crate::model::agent_instance::AgentLogStreamOptions; + use crate::model::agent::AgentLogStreamOptions; use golem_common::model::{IdempotencyKey, Timestamp}; use std::str::FromStr; use test_r::test; diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index d4ed3ca1ca..f3c2c09759 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -902,7 +902,7 @@ mod tests { #[test] fn agent_list_structured_output_masks_secret_config_paths() { - let output = crate::model::agent_instance::AgentsMetadataResponseView { + let output = crate::model::agent::AgentsMetadataResponseView { agents: vec![sample_agent_metadata_view()], cursors: BTreeMap::new(), }; @@ -1013,10 +1013,10 @@ mod tests { } } - fn sample_agent_metadata_view() -> crate::model::agent_instance::AgentMetadataView { - crate::model::agent_instance::AgentMetadataView { + fn sample_agent_metadata_view() -> crate::model::agent::AgentMetadataView { + crate::model::agent::AgentMetadataView { component_name: golem_common::model::component::ComponentName("component".to_string()), - agent_id: crate::model::agent_instance::RawAgentId("agent()".to_string()), + agent_id: crate::model::agent::RawAgentId("agent()".to_string()), created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), env: BTreeMap::new().into_iter().collect(), @@ -2917,9 +2917,12 @@ mod tests { proptest::collection::vec(arb_agent_metadata_view(), 0..5), proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), ) - .prop_map(|(agents, cursors)| { - crate::model::agent_instance::AgentsMetadataResponseView { agents, cursors } - }) + .prop_map( + |(agents, cursors)| crate::model::agent::AgentsMetadataResponseView { + agents, + cursors, + }, + ) .prop_map(|output| { to_structured_output_value(output).expect("generated DTO should serialize") }) @@ -2930,7 +2933,7 @@ mod tests { serialized_output((arb_small_string(), arb_small_string()).prop_map( |(component_name, agent_id)| crate::model::text::agent_instance::AgentCreateView { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent_instance::RawAgentId(agent_id), + agent_id: crate::model::agent::RawAgentId(agent_id), }, )) } @@ -3040,7 +3043,7 @@ mod tests { /// (`AgentUpdateMeta` / `AgentRedeploymentMeta`). fn arb_agent_transition_fields() -> BoxedStrategy<( golem_common::model::component::ComponentName, - crate::model::agent_instance::RawAgentId, + crate::model::agent::RawAgentId, golem_common::model::component::ComponentRevision, golem_common::model::component::ComponentRevision, Option, @@ -3058,7 +3061,7 @@ mod tests { |(component_name, agent_id, from_revision, revision, from_version, version)| { ( golem_common::model::component::ComponentName(component_name), - crate::model::agent_instance::RawAgentId(agent_id), + crate::model::agent::RawAgentId(agent_id), golem_common::model::component::ComponentRevision::new(from_revision) .expect("generated revision should be valid"), golem_common::model::component::ComponentRevision::new(revision) @@ -3112,13 +3115,13 @@ mod tests { .prop_map(|(component_name, agent_id)| { crate::model::text::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent_instance::RawAgentId(agent_id), + agent_id: crate::model::agent::RawAgentId(agent_id), } }) .boxed() } - fn arb_agent_metadata_view() -> BoxedStrategy { + fn arb_agent_metadata_view() -> BoxedStrategy { ( ( arb_small_string(), @@ -3171,9 +3174,9 @@ mod tests { exported_resource_instances, ) = right; - crate::model::agent_instance::AgentMetadataView { + crate::model::agent::AgentMetadataView { component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent_instance::RawAgentId(agent_id), + agent_id: crate::model::agent::RawAgentId(agent_id), created_by: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), ), diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index 9d80fa6beb..38a565aa6c 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -14,7 +14,7 @@ use crate::agent_id_display::SourceLanguage; use crate::agent_id_display::render_type_for_language; -use crate::model::agent_instance::RawAgentId; +use crate::model::agent::RawAgentId; use crate::model::app_raw; use crate::model::environment::ResolvedEnvironmentIdentity; use crate::model::masking::{ diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index 21e5979c01..32c5fd02e6 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -16,7 +16,7 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::command::shared_args::{ForceBuildArg, PostDeployArgs}; use crate::error::service::ServiceError; use crate::model::GuestLanguage; -use crate::model::agent_instance::RawAgentId; +use crate::model::agent::RawAgentId; use crate::model::component::{ render_agent_constructor, render_input_schema, render_output_schema, }; diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 1c25f783a0..9fd87d5267 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -13,7 +13,6 @@ // limitations under the License. pub mod agent; -pub mod agent_instance; pub mod app; pub mod app_raw; pub mod cascade; diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs index 03024eacbb..76f55aac3f 100644 --- a/cli/golem-cli/src/model/text/action_result.rs +++ b/cli/golem-cli/src/model/text/action_result.rs @@ -22,7 +22,7 @@ //! to stderr (see `Context::new`) and these structured payloads are //! emitted on stdout so that automation can rely on a stable schema. -use crate::model::agent_instance::RawAgentId; +use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::{NoTextOutput, TextOutput}; use golem_common::model::component::{ComponentName, ComponentRevision}; diff --git a/cli/golem-cli/src/model/text/agent_instance.rs b/cli/golem-cli/src/model/text/agent_instance.rs index 13a218f4e8..c83b054d26 100644 --- a/cli/golem-cli/src/model/text/agent_instance.rs +++ b/cli/golem-cli/src/model/text/agent_instance.rs @@ -14,7 +14,7 @@ use crate::agent_id_display::SourceLanguage; use crate::log::{LogColorize, logln}; -use crate::model::agent_instance::{ +use crate::model::agent::{ AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, }; use crate::model::cli_output::StructuredOutput; From f2de966bcef8f22dff3c40f6957721df5f4188cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 29 Jul 2026 17:02:16 +0200 Subject: [PATCH 30/70] fold the agent-instance text views into the text::agent module --- .../src/command_handler/agent/mod.rs | 2 +- cli/golem-cli/src/model/cli_output.rs | 19 ++++++++----------- .../{agent_instance.rs => agent/instance.rs} | 0 .../src/model/text/{agent.rs => agent/mod.rs} | 4 ++++ cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 13 insertions(+), 13 deletions(-) rename cli/golem-cli/src/model/text/{agent_instance.rs => agent/instance.rs} (100%) rename cli/golem-cli/src/model/text/{agent.rs => agent/mod.rs} (98%) diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 3c392b2eba..734cb7d394 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -36,7 +36,7 @@ use crate::model::text::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, }; -use crate::model::text::agent_instance::{ +use crate::model::text::agent::{ AgentCreateView, AgentFilesView, AgentGetView, AgentOplogEntryView, FileNodeView, format_agent_id_match, format_timestamp, }; diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs index f3c2c09759..c82d366ea1 100644 --- a/cli/golem-cli/src/model/cli_output.rs +++ b/cli/golem-cli/src/model/cli_output.rs @@ -920,7 +920,7 @@ mod tests { #[test] fn agent_get_structured_output_masks_secret_config_paths() { - let value = hidden_structured_output(crate::model::text::agent_instance::AgentGetView { + let value = hidden_structured_output(crate::model::text::agent::AgentGetView { metadata: sample_agent_metadata_view(), precise: true, }); @@ -1373,7 +1373,7 @@ mod tests { agent_types: vec![agent_type], }) .expect("agent-type.list should serialize"), - to_structured_output_value(crate::model::text::agent_instance::AgentOplogEntryView { + to_structured_output_value(crate::model::text::agent::AgentOplogEntryView { index: 0, entry: sample_public_oplog_entries() .into_iter() @@ -2851,11 +2851,11 @@ mod tests { fn arb_agent_files_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::agent_instance::AgentFilesView { nodes }), + .prop_map(|nodes| crate::model::text::agent::AgentFilesView { nodes }), ) } - fn arb_file_node() -> BoxedStrategy { + fn arb_file_node() -> BoxedStrategy { ( arb_small_string(), arb_small_string(), @@ -2864,7 +2864,7 @@ mod tests { arb_small_u64(), ) .prop_map(|(name, last_modified, kind, permissions, size)| { - crate::model::text::agent_instance::FileNodeView { + crate::model::text::agent::FileNodeView { name, last_modified, kind, @@ -2877,10 +2877,7 @@ mod tests { fn arb_agent_get_result() -> OutputDocumentStrategy { serialized_output((arb_agent_metadata_view(), any::()).prop_map( - |(metadata, precise)| crate::model::text::agent_instance::AgentGetView { - metadata, - precise, - }, + |(metadata, precise)| crate::model::text::agent::AgentGetView { metadata, precise }, )) } @@ -2931,7 +2928,7 @@ mod tests { fn arb_agent_new_result() -> OutputDocumentStrategy { serialized_output((arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_id)| crate::model::text::agent_instance::AgentCreateView { + |(component_name, agent_id)| crate::model::text::agent::AgentCreateView { component_name: golem_common::model::component::ComponentName(component_name), agent_id: crate::model::agent::RawAgentId(agent_id), }, @@ -2948,7 +2945,7 @@ mod tests { ], ) .prop_map(|(index, entry)| { - crate::model::text::agent_instance::AgentOplogEntryView { index, entry } + crate::model::text::agent::AgentOplogEntryView { index, entry } }), ) } diff --git a/cli/golem-cli/src/model/text/agent_instance.rs b/cli/golem-cli/src/model/text/agent/instance.rs similarity index 100% rename from cli/golem-cli/src/model/text/agent_instance.rs rename to cli/golem-cli/src/model/text/agent/instance.rs diff --git a/cli/golem-cli/src/model/text/agent.rs b/cli/golem-cli/src/model/text/agent/mod.rs similarity index 98% rename from cli/golem-cli/src/model/text/agent.rs rename to cli/golem-cli/src/model/text/agent/mod.rs index 4531053edb..4caa81890f 100644 --- a/cli/golem-cli/src/model/text/agent.rs +++ b/cli/golem-cli/src/model/text/agent/mod.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod instance; + +pub use instance::*; + use crate::model::agent::view::AgentTypeView; use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 0d0bf75c6c..77b91ea13b 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -15,7 +15,6 @@ pub mod account; pub mod action_result; pub mod agent; -pub mod agent_instance; pub mod card; pub mod component; pub mod deployment; From 6a9abe47cc09dc4629f91b9d927e6e66f964a951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 12:21:22 +0200 Subject: [PATCH 31/70] extract the cli_output test module into cli_output/tests.rs --- cli/golem-cli/src/model/cli_output.rs | 5957 ------------------- cli/golem-cli/src/model/cli_output/mod.rs | 251 + cli/golem-cli/src/model/cli_output/tests.rs | 5619 +++++++++++++++++ 3 files changed, 5870 insertions(+), 5957 deletions(-) delete mode 100644 cli/golem-cli/src/model/cli_output.rs create mode 100644 cli/golem-cli/src/model/cli_output/mod.rs create mode 100644 cli/golem-cli/src/model/cli_output/tests.rs diff --git a/cli/golem-cli/src/model/cli_output.rs b/cli/golem-cli/src/model/cli_output.rs deleted file mode 100644 index c82d366ea1..0000000000 --- a/cli/golem-cli/src/model/cli_output.rs +++ /dev/null @@ -1,5957 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::model::masking::MaskingConfig; -use anyhow::{anyhow, bail}; -use serde::Serialize; -use serde::Serializer; -use serde_json::{Map, Value}; -use std::collections::{BTreeSet, VecDeque}; - -pub const CLI_OUTPUT_TYPE_FIELD: &str = "$type"; -const CLI_OUTPUT_TYPES_FIELD: &str = "x-golem-cli-output-types"; -pub const COMMAND_OUTPUT_SCHEMA_JSON: &str = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/command-output-schema/command-output.schema.json" -)); - -pub trait StructuredOutput: Serialize { - const KIND: &'static str; - - fn type_name() -> String { - Self::KIND.to_string() - } - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - Self: Sized, - { - let _ = config; - self.serialize(serializer) - } -} - -pub fn command_output_schema_value() -> anyhow::Result { - serde_json::from_str(COMMAND_OUTPUT_SCHEMA_JSON) - .map_err(|err| anyhow!("Embedded command output schema must parse: {err}")) -} - -pub fn command_output_type_names() -> anyhow::Result { - let schema = command_output_schema_value()?; - let entries = schema_output_type_entries(&schema)?; - let names = entries - .iter() - .filter_map(|entry| entry.get("type")) - .filter_map(Value::as_str) - .map(|name| Value::String(name.to_string())) - .collect::>(); - Ok(Value::Array(names)) -} - -pub fn focused_command_output_schema(output_types: &[String]) -> anyhow::Result { - if output_types.is_empty() { - bail!("At least one output type must be specified"); - } - - let schema = command_output_schema_value()?; - let definitions = schema - .get("definitions") - .and_then(Value::as_object) - .ok_or_else(|| anyhow!("Command output schema is missing definitions"))?; - let output_type_entries = schema_output_type_entries(&schema)?; - let known_output_types = output_type_entries - .iter() - .filter_map(|entry| entry.get("type")) - .filter_map(Value::as_str) - .collect::>(); - - let mut selected = BTreeSet::::new(); - let mut reachable = BTreeSet::::new(); - let mut queue = VecDeque::::new(); - for output_type in output_types { - if !known_output_types.contains(output_type.as_str()) { - bail!( - "Unknown output type: {output_type}; run `golem output-schema --types` to list known output types" - ); - } - if !definitions.contains_key(output_type) { - bail!("Command output schema is missing definition {output_type}"); - } - if selected.insert(output_type.clone()) && reachable.insert(output_type.clone()) { - queue.push_back(output_type.clone()); - } - } - - while let Some(name) = queue.pop_front() { - let definition = definitions - .get(&name) - .ok_or_else(|| anyhow!("Command output schema is missing definition {name}"))?; - let mut refs = BTreeSet::new(); - collect_definition_refs(definition, &mut refs); - for reference in refs { - if !definitions.contains_key(&reference) { - bail!("Command output schema references missing definition {reference}"); - } - if reachable.insert(reference.clone()) { - queue.push_back(reference); - } - } - } - - let mut focused = Map::new(); - if let Some(value) = schema.get("$schema") { - focused.insert("$schema".to_string(), value.clone()); - } - if let Some(value) = schema.get("title") { - focused.insert("title".to_string(), value.clone()); - } - focused.insert( - "description".to_string(), - Value::String( - "Focused structured output schema for selected Golem CLI output types.".to_string(), - ), - ); - focused.insert( - "oneOf".to_string(), - Value::Array( - selected - .iter() - .map(|output_type| json_ref(output_type)) - .collect(), - ), - ); - - let mut pruned_definitions = Map::new(); - for name in &reachable { - pruned_definitions.insert( - name.clone(), - definitions - .get(name) - .ok_or_else(|| anyhow!("Command output schema is missing definition {name}"))? - .clone(), - ); - } - focused.insert("definitions".to_string(), Value::Object(pruned_definitions)); - - focused.insert( - CLI_OUTPUT_TYPES_FIELD.to_string(), - Value::Array( - output_type_entries - .iter() - .filter(|entry| { - entry - .get("type") - .and_then(Value::as_str) - .is_some_and(|output_type| selected.contains(output_type)) - }) - .cloned() - .collect(), - ), - ); - - Ok(Value::Object(focused)) -} - -fn schema_output_type_entries(schema: &Value) -> anyhow::Result<&Vec> { - schema - .get(CLI_OUTPUT_TYPES_FIELD) - .and_then(Value::as_array) - .ok_or_else(|| anyhow!("Command output schema is missing {CLI_OUTPUT_TYPES_FIELD}")) -} - -fn collect_definition_refs(value: &Value, refs: &mut BTreeSet) { - match value { - Value::Object(object) => { - if let Some(reference) = object.get("$ref").and_then(Value::as_str) - && let Some(name) = reference.strip_prefix("#/definitions/") - { - refs.insert(name.to_string()); - } - for value in object.values() { - collect_definition_refs(value, refs); - } - } - Value::Array(values) => { - for value in values { - collect_definition_refs(value, refs); - } - } - _ => {} - } -} - -fn json_ref(definition_name: &str) -> Value { - let mut reference = Map::new(); - reference.insert( - "$ref".to_string(), - Value::String(format!("#/definitions/{definition_name}")), - ); - Value::Object(reference) -} - -pub fn to_structured_output_value( - output: Output, -) -> anyhow::Result { - to_structured_output_value_masked(output, MaskingConfig::hide_secrets()) -} - -pub fn to_structured_output_value_masked( - output: Output, - config: MaskingConfig, -) -> anyhow::Result { - let value = output.serialize_masked(serde_json::value::Serializer, config)?; - let type_value = Value::String(Output::type_name()); - - match value { - Value::Object(fields) => Ok(Value::Object(with_structured_output_type::( - fields, type_value, - )?)), - value => { - let mut fields = Map::new(); - fields.insert(CLI_OUTPUT_TYPE_FIELD.to_string(), type_value); - fields.insert("value".to_string(), value); - Ok(Value::Object(fields)) - } - } -} - -fn with_structured_output_type( - fields: Map, - type_value: Value, -) -> anyhow::Result> { - let mut result = Map::new(); - result.insert(CLI_OUTPUT_TYPE_FIELD.to_string(), type_value); - - for (key, value) in fields { - if key == CLI_OUTPUT_TYPE_FIELD { - bail!( - "CLI output model {} must not define reserved field {CLI_OUTPUT_TYPE_FIELD}", - Output::KIND, - ); - } - result.insert(key, value); - } - - Ok(result) -} - -#[cfg(test)] -mod tests { - #![allow(dead_code)] - - use crate::model::cli_output::{ - CLI_OUTPUT_TYPE_FIELD, StructuredOutput, command_output_type_names, - focused_command_output_schema, to_structured_output_value, - to_structured_output_value_masked, - }; - use crate::model::masking::MaskingConfig; - use crate::model::text::diff::DeployPlanView; - use golem_common::model::card::{CardId, PolymorphicCard}; - use proptest::prelude::*; - use quote::ToTokens; - use serde_json::{Value, json}; - use std::collections::{BTreeMap, BTreeSet}; - use std::path::{Path, PathBuf}; - use syn::{Expr, ImplItem, Item, ItemImpl, Lit, Type}; - use test_r::test; - use uuid::uuid; - use walkdir::WalkDir; - - type OutputDocumentStrategy = BoxedStrategy; - - struct StructuredOutputTestEntry { - rust_type: &'static str, - output_type: &'static str, - examples: fn() -> Vec, - arbitrary: fn() -> OutputDocumentStrategy, - } - - macro_rules! registry_entry { - ($rust_type:literal, $output_type:literal, $arbitrary:expr) => { - StructuredOutputTestEntry { - rust_type: $rust_type, - output_type: $output_type, - examples: || { - let mut runner = proptest::test_runner::TestRunner::deterministic(); - vec![ - ($arbitrary)() - .new_tree(&mut runner) - .expect("example strategy should produce a value") - .current(), - ] - }, - arbitrary: $arbitrary, - } - }; - } - - static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ - registry_entry!( - "AccountDeleteResult", - "account.delete", - arb_account_delete_result - ), - registry_entry!("AccountGetView", "account.get", arb_account_get_result), - registry_entry!("AccountNewView", "account.new", arb_account_new_result), - registry_entry!( - "PermissionShareDeleteResult", - "account.permission-share.delete", - arb_permission_share_delete_result - ), - registry_entry!( - "PermissionShareGetView", - "account.permission-share.get", - arb_permission_share_get_result - ), - registry_entry!( - "PermissionShareListView", - "account.permission-share.list", - arb_permission_share_list_result - ), - registry_entry!( - "PermissionShareNewView", - "account.permission-share.new", - arb_permission_share_new_result - ), - registry_entry!( - "PermissionShareUpdateView", - "account.permission-share.update", - arb_permission_share_update_result - ), - registry_entry!( - "AccountUpdateView", - "account.update", - arb_account_update_result - ), - registry_entry!("CardGetView", "card.get", arb_card_get_result), - registry_entry!("CardListView", "card.list", arb_card_list_result), - registry_entry!("CardRevokeResult", "card.revoke", arb_card_revoke_result), - registry_entry!("AgentTypeView", "agent-type.get", arb_agent_type_get_result), - registry_entry!( - "AgentTypeListView", - "agent-type.list", - arb_agent_type_list_result - ), - registry_entry!( - "AgentCancelInvocationResult", - "agent.cancel-invocation", - arb_agent_cancel_invocation_result - ), - registry_entry!("AgentDeleteResult", "agent.delete", arb_agent_delete_result), - registry_entry!( - "AgentDeleteAllResult", - "agent.delete-all", - arb_agent_delete_all_result - ), - registry_entry!( - "AgentFileContentsResult", - "agent.file-contents", - arb_agent_file_contents_result - ), - registry_entry!("AgentFilesView", "agent.files", arb_agent_files_result), - registry_entry!("AgentGetView", "agent.get", arb_agent_get_result), - registry_entry!( - "AgentInterruptResult", - "agent.interrupt", - arb_agent_interrupt_result - ), - registry_entry!("InvokeResultView", "agent.invoke", arb_agent_invoke_result), - registry_entry!( - "AgentsMetadataResponseView", - "agent.list", - arb_agent_list_result - ), - registry_entry!("AgentCreateView", "agent.new", arb_agent_new_result), - registry_entry!("AgentOplogEntryView", "agent.oplog", arb_agent_oplog_result), - registry_entry!( - "AgentPluginToggleResult", - "agent.plugin-toggle", - arb_agent_plugin_toggle_result - ), - registry_entry!( - "AgentRedeployResult", - "agent.redeploy", - arb_agent_redeploy_result - ), - registry_entry!("AgentResumeResult", "agent.resume", arb_agent_resume_result), - registry_entry!("AgentRevertResult", "agent.revert", arb_agent_revert_result), - registry_entry!( - "AgentSimulateCrashResult", - "agent.simulate-crash", - arb_agent_simulate_crash_result - ), - registry_entry!("AgentStreamEvent", "agent.stream", arb_agent_stream_event), - registry_entry!( - "TryUpdateAllWorkersResult", - "agent.update", - arb_agent_update_result - ), - registry_entry!( - "TokenDeleteResult", - "api-token.delete", - arb_token_delete_result - ), - registry_entry!("TokenListView", "api-token.list", arb_token_list_result), - registry_entry!("TokenNewView", "api-token.new", arb_token_new_result), - registry_entry!( - "HttpApiDeploymentGetView", - "api.deployment.get", - arb_api_deployment_get_result - ), - registry_entry!( - "HttpApiDeploymentListView", - "api.deployment.list", - arb_api_deployment_list_result - ), - registry_entry!( - "DomainRegistrationDeleteResult", - "api.domain.delete", - arb_api_domain_delete_result - ), - registry_entry!( - "HttpApiDomainListView", - "api.domain.list", - arb_api_domain_list_result - ), - registry_entry!( - "DomainRegistrationNewView", - "api.domain.register", - arb_api_domain_register_result - ), - registry_entry!( - "HttpSecuritySchemeCreateView", - "api.security-scheme.create", - arb_api_security_scheme_create_result - ), - registry_entry!( - "HttpSecuritySchemeDeleteView", - "api.security-scheme.delete", - arb_api_security_scheme_delete_result - ), - registry_entry!( - "HttpSecuritySchemeGetView", - "api.security-scheme.get", - arb_api_security_scheme_get_result - ), - registry_entry!( - "HttpSecuritySchemeListView", - "api.security-scheme.list", - arb_api_security_scheme_list_result - ), - registry_entry!( - "HttpSecuritySchemeUpdateView", - "api.security-scheme.update", - arb_api_security_scheme_update_result - ), - registry_entry!("BuildResult", "build", arb_build_result), - registry_entry!("CleanResult", "clean", arb_clean_result), - registry_entry!("DeployPlanView", "deploy.plan", arb_deploy_plan_result), - registry_entry!("DeployResultView", "deploy", arb_deploy_result), - registry_entry!( - "GenerateBridgeResult", - "generate-bridge", - arb_generate_bridge_result - ), - registry_entry!("NewAppResult", "new", arb_new_app_result), - registry_entry!("TemplateListView", "templates", arb_template_list_result), - registry_entry!( - "ComponentGetView", - "component.get", - arb_component_get_result - ), - registry_entry!( - "ComponentListView", - "component.list", - arb_component_list_result - ), - registry_entry!( - "ComponentManifestTraceView", - "component.manifest-trace", - arb_component_manifest_trace_result - ), - registry_entry!( - "DeploymentNewView", - "deploy.deployment", - arb_deployment_create_result - ), - registry_entry!("DeploymentDiff", "deploy.diff", arb_deployment_diff_result), - registry_entry!( - "DeploymentListView", - "deploy.deployments", - arb_deployment_list_result - ), - registry_entry!( - "EnvironmentListView", - "environment.list", - arb_environment_list_result - ), - registry_entry!( - "EnvironmentSyncDeploymentOptionsResult", - "environment.sync-deployment-options", - arb_environment_sync_deployment_options_result - ), - registry_entry!( - "EnvironmentSetupPlanView", - "deploy.environment-setup-plan", - arb_environment_setup_plan_result - ), - registry_entry!( - "PluginRegistrationGetView", - "plugin.get", - arb_plugin_get_result - ), - registry_entry!("PluginListView", "plugin.list", arb_plugin_list_result), - registry_entry!( - "PluginRegistrationRegisterView", - "plugin.register", - arb_plugin_register_result - ), - registry_entry!( - "PluginUnregisterResult", - "plugin.unregister", - arb_plugin_unregister_result - ), - registry_entry!( - "ProfileConfigSetFormatResult", - "profile.config.set-format", - arb_profile_config_set_format_result - ), - registry_entry!( - "ProfileDeleteResult", - "profile.delete", - arb_profile_delete_result - ), - registry_entry!("ProfileView", "profile.get", arb_profile_get_result), - registry_entry!("ProfileListView", "profile.list", arb_profile_list_result), - registry_entry!( - "ProfileCreateResult", - "profile.new", - arb_profile_create_result - ), - registry_entry!( - "ProfileSwitchResult", - "profile.switch", - arb_profile_switch_result - ), - registry_entry!( - "ResourceDefinitionCreateView", - "resource.create", - arb_resource_create_result - ), - registry_entry!( - "ResourceDefinitionDeleteView", - "resource.delete", - arb_resource_delete_result - ), - registry_entry!( - "ResourceDefinitionGetView", - "resource.get", - arb_resource_get_result - ), - registry_entry!( - "ResourceDefinitionListView", - "resource.list", - arb_resource_list_result - ), - registry_entry!( - "ResourceDefinitionUpdateView", - "resource.update", - arb_resource_update_result - ), - registry_entry!( - "RetryPolicyCreateView", - "retry-policy.create", - arb_retry_policy_create_result - ), - registry_entry!( - "RetryPolicyDeleteView", - "retry-policy.delete", - arb_retry_policy_delete_result - ), - registry_entry!( - "RetryPolicyGetView", - "retry-policy.get", - arb_retry_policy_get_result - ), - registry_entry!( - "RetryPolicyListView", - "retry-policy.list", - arb_retry_policy_list_result - ), - registry_entry!( - "RetryPolicyUpdateView", - "retry-policy.update", - arb_retry_policy_update_result - ), - registry_entry!( - "SecretCreateView", - "secret.create", - arb_secret_create_result - ), - registry_entry!( - "SecretDeleteView", - "secret.delete", - arb_secret_delete_result - ), - registry_entry!("SecretGetView", "secret.get", arb_secret_get_result), - registry_entry!("SecretListView", "secret.list", arb_secret_list_result), - registry_entry!( - "SecretUpdateView", - "secret.update-value", - arb_secret_update_value_result - ), - ]; - - #[derive(Debug, Clone)] - struct OutputImpl { - rust_type: String, - kind: String, - file: PathBuf, - tuple_field_type: Option, - } - - impl OutputImpl { - fn type_name(&self) -> String { - self.kind.clone() - } - } - - #[derive(Default)] - struct SourceSummary { - outputs: Vec, - tuple_field_types_by_struct: BTreeMap, - } - - #[test] - fn cli_output_schema_source_kinds_are_consistent() { - let summary = collect_source_summary(); - let mut errors = Vec::new(); - - if let Ok(path) = std::env::var("GOLEM_CLI_OUTPUT_SUMMARY_MD") { - if let Some(parent) = Path::new(&path).parent() { - std::fs::create_dir_all(parent) - .unwrap_or_else(|err| panic!("failed to create {}: {err}", parent.display())); - } - std::fs::write(&path, render_markdown_summary(&summary.outputs)) - .unwrap_or_else(|err| panic!("failed to write {path}: {err}")); - } - - let mut by_type_name = BTreeMap::>::new(); - for output in &summary.outputs { - by_type_name - .entry(output.type_name()) - .or_default() - .push(output); - } - - for (type_name, outputs) in by_type_name { - if outputs.len() > 1 { - errors.push(format!( - "duplicate CLI output $type {type_name}: {}", - outputs - .iter() - .map(|output| output.rust_type.as_str()) - .collect::>() - .join(", ") - )); - } - } - - for output in &summary.outputs { - if !is_valid_kind(&output.kind) { - errors.push(format!( - "{} has invalid KIND {:?}", - output.rust_type, output.kind - )); - } - - if let Some(tuple_field_type) = &output.tuple_field_type - && is_known_non_object_type(tuple_field_type) - { - errors.push(format!( - "{} is a StructuredOutput tuple wrapper around non-object type `{}`; use a named output struct instead", - output.rust_type, tuple_field_type, - )); - } - } - - assert!(errors.is_empty(), "\n{}", errors.join("\n")); - } - - #[test] - fn cli_output_schema_matches_source_registry() { - let source_entries = source_output_entries(); - let schema = load_command_output_schema(); - let schema_entries = schema_output_entries(&schema); - let registry_entries = registry_output_entries(); - - assert_eq!(registry_entries, source_entries); - assert_eq!(schema_entries, source_entries); - - let definitions = schema - .get("definitions") - .and_then(Value::as_object) - .expect("schema must have object definitions"); - let one_of_refs = schema - .get("oneOf") - .and_then(Value::as_array) - .expect("schema must have array oneOf") - .iter() - .map(|entry| { - entry - .get("$ref") - .and_then(Value::as_str) - .expect("oneOf entry must have string $ref") - .strip_prefix("#/definitions/") - .expect("oneOf $ref must point to #/definitions") - .to_string() - }) - .collect::>(); - - let schema_types = schema_entries.keys().cloned().collect::>(); - let definition_types = definitions.keys().cloned().collect::>(); - - let missing_definitions = schema_types - .difference(&definition_types) - .cloned() - .collect::>(); - assert!( - missing_definitions.is_empty(), - "each output type must have a schema definition, missing: {missing_definitions:?}" - ); - assert_eq!( - one_of_refs, schema_types, - "oneOf refs must match output types" - ); - - jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - } - - #[test] - fn cli_output_schema_types_lists_only_type_names() { - let types = command_output_type_names().expect("type names should render"); - let types = types.as_array().expect("types output must be an array"); - - assert!( - types.iter().all(Value::is_string), - "types output must contain only strings" - ); - assert!(types.iter().any(|value| value == "agent.oplog")); - assert!(types.iter().any(|value| value == "agent.stream")); - } - - #[test] - fn cli_output_schema_output_definitions_have_agent_metadata() { - let schema = load_command_output_schema(); - let definitions = schema_definitions(&schema); - - for output_type in schema_output_entries(&schema).keys() { - let definition = definitions - .get(output_type) - .unwrap_or_else(|| panic!("missing definition for {output_type}")); - for field in ["description", "x-golem-output-mode", "x-golem-command"] { - assert!( - definition.get(field).is_some(), - "{output_type} must define top-level {field} metadata" - ); - } - - let output_mode = definition - .get("x-golem-output-mode") - .and_then(Value::as_str) - .unwrap_or_else(|| panic!("{output_type} must define string x-golem-output-mode")); - assert!( - matches!(output_mode, "single" | "stream" | "multi-document"), - "{output_type} has invalid x-golem-output-mode {output_mode:?}" - ); - - let primary_command = definition - .get("x-golem-command") - .and_then(Value::as_str) - .unwrap_or_else(|| panic!("{output_type} must define string x-golem-command")); - assert!( - !primary_command.trim().is_empty(), - "{output_type} must define non-empty x-golem-command" - ); - - if let Some(commands) = definition.get("x-golem-commands") { - let commands = commands - .as_array() - .unwrap_or_else(|| panic!("{output_type} x-golem-commands must be an array")); - assert!( - !commands.is_empty(), - "{output_type} x-golem-commands must not be empty" - ); - assert!( - commands.iter().all(|command| command - .as_str() - .is_some_and(|command| !command.trim().is_empty())), - "{output_type} x-golem-commands must contain only non-empty strings" - ); - assert!( - commands.iter().any(|command| command == primary_command), - "{output_type} x-golem-commands must include x-golem-command" - ); - } - } - } - - #[test] - fn cli_output_schema_focus_prunes_unrelated_definitions() { - let schema = focused_command_output_schema(&["agent.oplog".to_string()]) - .expect("focused schema should render"); - let definitions = schema_definitions(&schema); - - assert!(definitions.contains_key("agent.oplog")); - assert!(definitions.contains_key("PublicOplogEntry")); - assert!(!definitions.contains_key("agent.list")); - assert!(!definitions.contains_key("component.list")); - - let entries = schema_output_entries(&schema); - assert_eq!( - entries.keys().collect::>(), - vec![&"agent.oplog".to_string()] - ); - - let validator = jsonschema::options() - .build(&schema) - .expect("focused command output schema must be valid JSON schema"); - let example = (arb_agent_oplog_result()) - .new_tree(&mut proptest::test_runner::TestRunner::deterministic()) - .expect("oplog strategy should produce value") - .current(); - assert!( - validator.is_valid(&example), - "focused schema should accept agent.oplog example: {:?}", - validator - .iter_errors(&example) - .map(|error| error.to_string()) - .collect::>() - ); - } - - #[test] - fn cli_output_schema_focus_supports_multiple_types() { - let schema = - focused_command_output_schema(&["agent.oplog".to_string(), "agent.stream".to_string()]) - .expect("focused schema should render"); - let definitions = schema_definitions(&schema); - - assert!(definitions.contains_key("agent.oplog")); - assert!(definitions.contains_key("agent.stream")); - assert!(!definitions.contains_key("agent.list")); - - let entries = schema_output_entries(&schema); - assert_eq!( - entries.keys().cloned().collect::>(), - BTreeSet::from_iter(["agent.oplog".to_string(), "agent.stream".to_string()]) - ); - } - - #[test] - fn cli_output_schema_focus_deduplicates_requested_types() { - let schema = - focused_command_output_schema(&["agent.oplog".to_string(), "agent.oplog".to_string()]) - .expect("focused schema should render"); - - let one_of = schema - .get("oneOf") - .and_then(Value::as_array) - .expect("focused schema must have oneOf"); - assert_eq!(one_of.len(), 1); - - let validator = jsonschema::options() - .build(&schema) - .expect("focused command output schema must be valid JSON schema"); - let example = (arb_agent_oplog_result()) - .new_tree(&mut proptest::test_runner::TestRunner::deterministic()) - .expect("oplog strategy should produce value") - .current(); - assert!(validator.is_valid(&example)); - } - - #[test] - fn cli_output_schema_focus_rejects_helper_definition_names() { - let error = focused_command_output_schema(&["JsonValue".to_string()]) - .expect_err("helper definition should not be accepted as an output type"); - - assert!(error.to_string().contains("Unknown output type: JsonValue")); - } - - #[test] - fn cli_output_schema_focus_rejects_unknown_type() { - let error = focused_command_output_schema(&["unknown".to_string()]) - .expect_err("unknown output type should fail"); - - assert!(error.to_string().contains("Unknown output type: unknown")); - } - - #[test] - fn agent_list_structured_output_masks_secret_config_paths() { - let output = crate::model::agent::AgentsMetadataResponseView { - agents: vec![sample_agent_metadata_view()], - cursors: BTreeMap::new(), - }; - - let value = hidden_structured_output(output); - - assert_eq!(value["agents"][0]["config"][0]["value"], json!("***")); - assert_eq!( - value["agents"][0]["defaultConfig"][0]["value"], - json!("***") - ); - assert!(!value.to_string().contains("runtime-secret")); - assert!(!value.to_string().contains("default-secret")); - } - - #[test] - fn agent_get_structured_output_masks_secret_config_paths() { - let value = hidden_structured_output(crate::model::text::agent::AgentGetView { - metadata: sample_agent_metadata_view(), - precise: true, - }); - - assert_eq!(value["metadata"]["config"][0]["value"], json!("***")); - assert_eq!(value["metadata"]["defaultConfig"][0]["value"], json!("***")); - assert_no_plaintext(&value, &["runtime-secret", "default-secret"]); - } - - #[test] - fn component_get_and_list_structured_outputs_mask_secret_payloads() { - let component = sample_component_view(); - - let get = hidden_structured_output(crate::model::text::component::ComponentGetView( - component.clone(), - )); - let list = hidden_structured_output(crate::model::text::component::ComponentListView { - components: vec![component], - }); - - for value in [get, list] { - assert_no_plaintext( - &value, - &[ - "component-env-secret", - "component-config-secret", - "component-plugin-secret", - ], - ); - assert!(value.to_string().contains("***")); - } - } - - #[test] - fn component_manifest_trace_structured_output_masks_secret_payloads() { - let value = - hidden_structured_output(crate::model::text::component::ComponentManifestTraceView { - component_name: golem_common::model::component::ComponentName( - "component".to_string(), - ), - properties: sample_component_layer_properties(), - }); - - assert_no_plaintext( - &value, - &[ - "manifest-config-secret", - "manifest-env-secret", - "manifest-plugin-secret", - ], - ); - assert!(value.to_string().contains("***")); - } - - #[test] - fn deploy_diff_and_plan_structured_outputs_mask_secret_payloads() { - let diff = sample_deployment_diff_with_secret_updates(); - let diff_value = hidden_structured_output(diff.clone()); - let plan_value = hidden_structured_output(DeployPlanView { - deployment_diff: &diff, - environment_setup: None, - }); - - for value in [diff_value, plan_value] { - assert_no_plaintext( - &value, - &[ - "deploy-env-secret-old", - "deploy-env-secret-new", - "deploy-config-secret-old", - "deploy-config-secret-new", - ], - ); - assert!(value.to_string().contains("(output: Output) -> Value { - to_structured_output_value_masked(output, MaskingConfig::hide_secrets()) - .expect("output should serialize with hidden secrets") - } - - fn assert_no_plaintext(value: &Value, plaintexts: &[&str]) { - let serialized = value.to_string(); - for plaintext in plaintexts { - assert!( - !serialized.contains(plaintext), - "structured output leaked plaintext {plaintext}: {serialized}" - ); - } - } - - fn sample_agent_metadata_view() -> crate::model::agent::AgentMetadataView { - crate::model::agent::AgentMetadataView { - component_name: golem_common::model::component::ComponentName("component".to_string()), - agent_id: crate::model::agent::RawAgentId("agent()".to_string()), - created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), - environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), - env: BTreeMap::new().into_iter().collect(), - default_env: BTreeMap::new().into_iter().collect(), - config: vec![golem_common::model::worker::AgentConfigEntryDto { - path: vec!["db".to_string(), "password".to_string()], - value: golem_common::base_model::json::NormalizedJsonValue(json!("runtime-secret")), - }], - default_config: vec![golem_common::model::worker::AgentConfigEntryDto { - path: vec!["db".to_string(), "password".to_string()], - value: golem_common::base_model::json::NormalizedJsonValue(json!("default-secret")), - }], - status: golem_common::model::AgentStatus::Idle, - component_revision: golem_common::model::component::ComponentRevision::new(1).unwrap(), - retry_count: 0, - pending_invocation_count: 0, - updates: vec![], - created_at: "2024-01-01T00:00:00Z".parse().unwrap(), - last_error: None, - component_size: 0, - total_linear_memory_size: 0, - exported_resource_instances: BTreeMap::new().into_iter().collect(), - source_language: crate::agent_id_display::SourceLanguage::default(), - secret_config_paths: BTreeSet::from_iter(["db.password".to_string()]), - } - } - - fn sample_component_view() -> crate::model::component::ComponentView { - let agent_type_name = golem_common::model::agent::AgentTypeName("agent".to_string()); - crate::model::component::ComponentView { - component_name: golem_common::model::component::ComponentName("component".to_string()), - component_id: golem_common::model::component::ComponentId(uuid::Uuid::nil()), - component_version: Some("1.0.0".to_string()), - component_revision: 1, - component_size: 0, - created_at: fixed_datetime(), - environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), - exports: vec![], - agent_types: vec![sample_agent_type_schema(agent_type_name.clone())], - agent_type_provision_configs: BTreeMap::from_iter([( - agent_type_name, - golem_common::model::component_metadata::AgentTypeProvisionConfig { - env: BTreeMap::from_iter([( - "API_TOKEN".to_string(), - "component-env-secret".to_string(), - )]), - config: vec![golem_common::model::worker::TypedAgentConfigEntry { - path: vec!["db".to_string(), "password".to_string()], - value: typed_schema_string("component-config-secret"), - }], - plugins: vec![golem_common::model::component::InstalledPlugin { - environment_plugin_grant_id: - golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId( - uuid::Uuid::nil(), - ), - priority: golem_common::model::component::PluginPriority(1), - parameters: BTreeMap::from_iter([( - "apiKey".to_string(), - "component-plugin-secret".to_string(), - )]), - plugin_registration_id: - golem_common::model::plugin_registration::PluginRegistrationId( - uuid::Uuid::nil(), - ), - plugin_name: "plugin".to_string(), - plugin_version: "1.0.0".to_string(), - oplog_processor_component_id: None, - oplog_processor_component_revision: None, - }], - files: vec![], - initial_permissions: PolymorphicCard { - card_id: CardId(uuid!("a741846a-2562-4065-8a06-fd9dbee52198")), - parent_ids: vec![CardId(uuid!("cd6d7717-4df1-4e9a-ac26-5bf524c1a732"))], - lower_negative: Vec::new(), - lower_positive: Vec::new(), - upper_negative: Vec::new(), - upper_positive: Vec::new(), - system_card: false, - created_at: fixed_datetime(), - expires_at: None, - }, - }, - )]), - } - } - - fn sample_agent_type_schema( - agent_type_name: golem_common::model::agent::AgentTypeName, - ) -> golem_common::schema::agent::AgentTypeSchema { - golem_common::schema::agent::AgentTypeSchema { - type_name: agent_type_name, - description: String::new(), - source_language: String::new(), - schema: golem_common::schema::SchemaGraph::empty(), - constructor: golem_common::schema::agent::AgentConstructorSchema { - name: None, - description: String::new(), - prompt_hint: None, - input_schema: golem_common::schema::agent::InputSchema::parameters([ - golem_common::schema::agent::NamedField::user_supplied( - "value", - golem_common::schema::SchemaType::string(), - ), - ]), - }, - methods: vec![golem_common::schema::agent::AgentMethodSchema { - name: "method".to_string(), - description: String::new(), - prompt_hint: None, - input_schema: golem_common::schema::agent::InputSchema::parameters([ - golem_common::schema::agent::NamedField::auto_injected( - "principal", - golem_common::schema::agent::AutoInjectedKind::Principal, - golem_common::schema::SchemaType::string(), - ), - ]), - output_schema: golem_common::schema::agent::OutputSchema::Single(Box::new( - golem_common::schema::SchemaType::u64(), - )), - http_endpoint: vec![], - read_only: None, - }], - dependencies: vec![], - mode: golem_common::model::agent::AgentMode::Durable, - http_mount: None, - snapshotting: golem_common::model::agent::Snapshotting::Disabled( - golem_common::model::Empty {}, - ), - config: vec![golem_common::schema::agent::AgentConfigDeclarationSchema { - source: golem_common::model::agent::AgentConfigSource::Secret, - path: vec!["db".to_string(), "password".to_string()], - value_type: golem_common::schema::SchemaType::string(), - }], - } - } - - fn typed_schema_string(value: &str) -> golem_common::schema::TypedSchemaValue { - golem_common::schema::TypedSchemaValue::new( - golem_common::schema::SchemaGraph::anonymous(golem_common::schema::SchemaType::string()), - golem_common::schema::SchemaValue::String(value.to_string()), - ) - } - - fn sample_component_layer_properties() -> crate::model::app::ComponentLayerProperties { - use crate::model::cascade::property::Property; - - let layer = crate::model::app::ComponentLayerId::ComponentCommon( - golem_common::model::component::ComponentName("component".to_string()), - ); - let mut properties = crate::model::app::ComponentLayerProperties::default(); - properties.config.apply_layer( - &layer, - None, - Some(json!({ "db": { "password": "manifest-config-secret" } })), - ); - properties.env.apply_layer( - &layer, - None, - ( - crate::model::cascade::property::map::MapMergeMode::Upsert, - indexmap::IndexMap::from_iter([( - "API_TOKEN".to_string(), - "manifest-env-secret".to_string(), - )]), - ), - ); - properties.plugins.apply_layer( - &layer, - None, - ( - crate::model::cascade::property::vec::VecMergeMode::Append, - vec![crate::model::app_raw::PluginInstallation { - account: None, - name: "plugin".to_string(), - version: "1.0.0".to_string(), - parameters: std::collections::HashMap::from_iter([( - "apiKey".to_string(), - "manifest-plugin-secret".to_string(), - )]), - }], - ), - ); - properties - } - - fn sample_deployment_diff_with_secret_updates() -> golem_common::model::diff::DeploymentDiff { - use golem_common::model::diff::{Diffable, HashOf}; - - let mut current = golem_common::model::diff::Deployment::default(); - let mut new = golem_common::model::diff::Deployment::default(); - let wasm_hash = fixed_hash("wasm"); - - current.components.insert( - "component".to_string(), - HashOf::form_value(golem_common::model::diff::Component { - wasm_hash, - agent_type_provision_configs: BTreeMap::from_iter([( - "agent".to_string(), - HashOf::form_value(sample_diff_provision_config( - "deploy-env-secret-old", - "deploy-config-secret-old", - )), - )]), - }), - ); - new.components.insert( - "component".to_string(), - HashOf::form_value(golem_common::model::diff::Component { - wasm_hash, - agent_type_provision_configs: BTreeMap::from_iter([( - "agent".to_string(), - HashOf::form_value(sample_diff_provision_config( - "deploy-env-secret-new", - "deploy-config-secret-new", - )), - )]), - }), - ); - - golem_common::model::diff::Deployment::diff(&new, ¤t) - .expect("sample deployments should diff") - .expect("sample deployments should differ") - } - - fn sample_diff_provision_config( - env_secret: &str, - config_secret: &str, - ) -> golem_common::model::diff::AgentTypeProvisionConfig { - golem_common::model::diff::AgentTypeProvisionConfig { - env: BTreeMap::from_iter([("API_TOKEN".to_string(), env_secret.to_string())]), - config: BTreeMap::from_iter([( - "db.password".to_string(), - golem_common::base_model::json::NormalizedJsonValue(json!(config_secret)), - )]), - files_by_path: BTreeMap::new(), - plugins_by_grant_id: BTreeMap::new(), - initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { - lower_positive: Vec::new(), - lower_negative: Vec::new(), - upper_positive: Vec::new(), - upper_negative: Vec::new(), - }, - } - } - - fn fixed_hash(input: &str) -> golem_common::model::diff::Hash { - golem_common::model::diff::Hash::new(blake3::hash(input.as_bytes())) - } - - #[test] - fn cli_output_schema_validates_schema_native_secret_outputs() { - let schema = load_command_output_schema(); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - - let secret = golem_client::model::AgentSecretDto { - id: golem_common::model::agent_secret::AgentSecretId(uuid::Uuid::nil()), - environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), - path: golem_common::model::agent_secret::CanonicalAgentSecretPath(vec![ - "token".to_string(), - ]), - revision: golem_common::model::agent_secret::AgentSecretRevision::new(1) - .expect("static secret revision should be valid"), - secret_type: golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::string(), - ), - secret_value: Some(golem_common::schema::SchemaValue::String( - "super-secret".to_string(), - )), - }; - - let outputs = vec![ - to_structured_output_value_masked( - crate::model::text::secret::SecretCreateView(secret.clone().into()), - MaskingConfig::hide_secrets(), - ) - .expect("secret.create should serialize"), - to_structured_output_value_masked( - crate::model::text::secret::SecretDeleteView(secret.clone().into()), - MaskingConfig::hide_secrets(), - ) - .expect("secret.delete should serialize"), - to_structured_output_value_masked( - crate::model::text::secret::SecretGetView(secret.clone().into()), - MaskingConfig::hide_secrets(), - ) - .expect("secret.get should serialize"), - to_structured_output_value_masked( - crate::model::text::secret::SecretUpdateView(secret.clone().into()), - MaskingConfig::hide_secrets(), - ) - .expect("secret.update-value should serialize"), - to_structured_output_value_masked( - crate::model::text::secret::SecretListView { - secrets: vec![secret.into()], - environment_name: "generated-environment".to_string(), - show_ids: false, - }, - MaskingConfig::hide_secrets(), - ) - .expect("secret.list should serialize"), - ]; - - for output in outputs { - assert!( - validator.is_valid(&output), - "schema should accept schema-native secret output: {:?}", - validator - .iter_errors(&output) - .map(|error| error.to_string()) - .collect::>() - ); - - let secret_view = output - .get("secrets") - .and_then(Value::as_array) - .and_then(|secrets| secrets.first()) - .unwrap_or(&output); - - assert_eq!(secret_view["secretType"]["root"]["kind"], json!("string")); - assert_eq!(secret_view["secretValue"]["kind"], json!("string")); - assert_eq!(secret_view["secretValue"]["value"], json!("***")); - } - } - - #[test] - fn cli_output_schema_validates_schema_native_component_and_agent_outputs() { - let schema = load_command_output_schema(); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - let mut runner = proptest::test_runner::TestRunner::deterministic(); - - let component = arb_component_view() - .new_tree(&mut runner) - .expect("component strategy should produce a value") - .current(); - let agent_type = arb_deployed_registered_agent_type() - .new_tree(&mut runner) - .expect("agent type strategy should produce a value") - .current(); - - let outputs = vec![ - to_structured_output_value(crate::model::text::component::ComponentGetView( - component.clone(), - )) - .expect("component.get should serialize"), - to_structured_output_value(crate::model::text::component::ComponentListView { - components: vec![component], - }) - .expect("component.list should serialize"), - to_structured_output_value(crate::model::text::agent::AgentTypeListView { - agent_types: vec![agent_type], - }) - .expect("agent-type.list should serialize"), - to_structured_output_value(crate::model::text::agent::AgentOplogEntryView { - index: 0, - entry: sample_public_oplog_entries() - .into_iter() - .next() - .expect("sample oplog entries should not be empty"), - }) - .expect("agent.oplog should serialize"), - ]; - - for output in outputs { - assert!( - validator.is_valid(&output), - "schema should accept schema-native output: {:?}", - validator - .iter_errors(&output) - .map(|error| error.to_string()) - .collect::>() - ); - } - } - - #[test] - fn cli_output_schema_validates_discriminated_documents() { - let schema = load_command_output_schema(); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - - let definitions = schema_definitions(&schema); - for output_type in schema_output_entries(&schema).keys() { - if !is_discriminator_only_definition( - definitions - .get(output_type) - .unwrap_or_else(|| panic!("missing definition for {output_type}")), - ) { - continue; - } - - let value = json!({ CLI_OUTPUT_TYPE_FIELD: output_type }); - assert!( - validator.is_valid(&value), - "schema should accept minimal document for {output_type}: {:?}", - validator - .iter_errors(&value) - .map(|error| error.to_string()) - .collect::>() - ); - } - - let missing_type = json!({ "ok": true }); - assert!( - !validator.is_valid(&missing_type), - "schema must reject output documents without {CLI_OUTPUT_TYPE_FIELD}" - ); - - let unknown_type = json!({ CLI_OUTPUT_TYPE_FIELD: "unknown" }); - assert!( - !validator.is_valid(&unknown_type), - "schema must reject unknown output document types" - ); - } - - #[test] - fn cli_output_schema_exact_registered_schemas_reject_extra_fields() { - let schema = load_command_output_schema(); - let definitions = schema_definitions(&schema); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - - for entry in STRUCTURED_OUTPUT_TEST_REGISTRY.iter().filter(|entry| { - !definition_allows_additional_properties( - definitions - .get(entry.output_type) - .unwrap_or_else(|| panic!("missing definition for {}", entry.output_type)), - ) - }) { - for mut example in (entry.examples)() { - let Some(object) = example.as_object_mut() else { - panic!("example for {} must be an object", entry.output_type); - }; - object.insert("unexpectedExtraField".to_string(), json!(true)); - - assert!( - !validator.is_valid(&example), - "exact schema should reject extra fields for {}", - entry.output_type, - ); - } - } - } - - #[test] - fn cli_output_schema_validates_registered_examples() { - let schema = load_command_output_schema(); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - - for entry in STRUCTURED_OUTPUT_TEST_REGISTRY { - for example in (entry.examples)() { - assert!( - validator.is_valid(&example), - "schema should accept example for {}: {:?}", - entry.output_type, - validator - .iter_errors(&example) - .map(|error| error.to_string()) - .collect::>() - ); - } - } - } - - proptest! { - #[test] - fn cli_output_schema_accepts_registered_generated_examples(value in arb_registered_output_document()) { - let schema = load_command_output_schema(); - let validator = jsonschema::options() - .build(&schema) - .expect("command output schema must be a valid JSON schema"); - - prop_assert!( - validator.is_valid(&value), - "schema should accept generated example: {:?}", - validator - .iter_errors(&value) - .map(|error| error.to_string()) - .collect::>() - ); - } - } - - fn load_command_output_schema() -> Value { - serde_json::from_str(crate::model::cli_output::COMMAND_OUTPUT_SCHEMA_JSON) - .expect("embedded command output schema must parse") - } - - fn schema_output_entries(schema: &Value) -> BTreeMap { - schema - .get("x-golem-cli-output-types") - .and_then(Value::as_array) - .expect("schema must have array x-golem-cli-output-types") - .iter() - .map(|entry| { - let output_type = entry - .get("type") - .and_then(Value::as_str) - .expect("schema output entry must have string type") - .to_string(); - let rust_type = entry - .get("rustType") - .and_then(Value::as_str) - .expect("schema output entry must have string rustType") - .to_string(); - (output_type, rust_type) - }) - .collect() - } - - fn schema_definitions(schema: &Value) -> &serde_json::Map { - schema - .get("definitions") - .and_then(Value::as_object) - .expect("schema must have object definitions") - } - - fn is_discriminator_only_definition(definition: &Value) -> bool { - definition - .get("required") - .and_then(Value::as_array) - .is_some_and(|required| { - required.len() == 1 - && required - .first() - .and_then(Value::as_str) - .is_some_and(|field| field == CLI_OUTPUT_TYPE_FIELD) - }) - } - - fn definition_allows_additional_properties(definition: &Value) -> bool { - definition - .get("additionalProperties") - .and_then(Value::as_bool) - .unwrap_or(true) - } - - fn registry_output_entries() -> BTreeMap { - STRUCTURED_OUTPUT_TEST_REGISTRY - .iter() - .map(|entry| (entry.output_type.to_string(), entry.rust_type.to_string())) - .collect() - } - - fn source_output_entries() -> BTreeMap { - collect_source_summary() - .outputs - .into_iter() - .map(|output| (output.type_name(), output.rust_type)) - .collect() - } - - fn collect_source_summary() -> SourceSummary { - let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); - let mut summary = SourceSummary::default(); - - for entry in WalkDir::new(&source_root) - .into_iter() - .filter_entry(|entry| !is_ignored_path(entry.path())) - .filter_map(Result::ok) - .filter(|entry| entry.file_type().is_file()) - .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "rs")) - { - let file_path = entry.path(); - let source = std::fs::read_to_string(file_path) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", file_path.display())); - let parsed = syn::parse_file(&source) - .unwrap_or_else(|err| panic!("failed to parse {}: {err}", file_path.display())); - let relative_path = file_path - .strip_prefix(Path::new(env!("CARGO_MANIFEST_DIR"))) - .unwrap_or(file_path) - .to_path_buf(); - - collect_items(&parsed.items, &relative_path, &mut summary); - } - - summary.outputs.sort_by(|left, right| { - left.kind - .cmp(&right.kind) - .then(left.rust_type.cmp(&right.rust_type)) - }); - - for output in &mut summary.outputs { - output.tuple_field_type = summary - .tuple_field_types_by_struct - .get(&output.rust_type) - .cloned(); - } - - summary - } - - fn is_ignored_path(path: &Path) -> bool { - path.components().any(|component| { - let component = component.as_os_str(); - component == "target" || component == ".git" - }) - } - - fn collect_items(items: &[Item], file: &Path, summary: &mut SourceSummary) { - for item in items { - match item { - Item::Struct(item) => collect_struct(item, file, summary), - Item::Impl(item) => collect_impl(item, file, summary), - Item::Mod(item) => { - if let Some((_, items)) = &item.content { - collect_items(items, file, summary); - } - } - _ => {} - } - } - } - - fn collect_struct(item: &syn::ItemStruct, file: &Path, summary: &mut SourceSummary) { - if let syn::Fields::Unnamed(fields) = &item.fields - && fields.unnamed.len() == 1 - && let Some(ty) = fields - .unnamed - .first() - .map(|field| field.ty.to_token_stream().to_string()) - { - summary - .tuple_field_types_by_struct - .insert(item.ident.to_string(), ty); - } - - let _ = file; - } - - fn collect_impl(item: &ItemImpl, file: &Path, summary: &mut SourceSummary) { - let Some(trait_name) = item - .trait_ - .as_ref() - .and_then(|(_, path, _)| path.segments.last()) - .map(|segment| segment.ident.to_string()) - else { - return; - }; - - let Some(rust_type) = type_name(&item.self_ty) else { - return; - }; - - if trait_name.as_str() == "StructuredOutput" { - let mut kind = None; - - for impl_item in &item.items { - if let ImplItem::Const(constant) = impl_item - && constant.ident == "KIND" - { - kind = string_literal(&constant.expr); - } - } - - summary.outputs.push(OutputImpl { - rust_type, - kind: kind.unwrap_or_else(|| "".to_string()), - file: file.to_path_buf(), - tuple_field_type: None, - }); - } - } - - fn type_name(ty: &Type) -> Option { - match ty { - Type::Path(path) => path - .path - .segments - .last() - .map(|segment| segment.ident.to_string()), - _ => None, - } - } - - fn string_literal(expr: &Expr) -> Option { - match expr { - Expr::Lit(lit) => match &lit.lit { - Lit::Str(value) => Some(value.value()), - _ => None, - }, - _ => None, - } - } - - fn is_valid_kind(kind: &str) -> bool { - let parts = kind.split('.').collect::>(); - - !parts.is_empty() - && parts.iter().all(|part| { - !part.is_empty() - && part.bytes().all(|byte| { - byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-' - }) - && part - .bytes() - .next() - .is_some_and(|byte| byte.is_ascii_lowercase()) - }) - } - - fn is_known_non_object_type(ty: &str) -> bool { - let compact = ty.replace(' ', ""); - - if compact.starts_with("Vec<") - || compact.starts_with("Option<") - || compact.starts_with("HashSet<") - || compact.starts_with("BTreeSet<") - || compact.starts_with('[') - { - return true; - } - - matches!( - compact.as_str(), - "String" - | "str" - | "&str" - | "bool" - | "u8" - | "u16" - | "u32" - | "u64" - | "usize" - | "i8" - | "i16" - | "i32" - | "i64" - | "isize" - | "f32" - | "f64" - ) - } - - fn render_markdown_summary(summary: &[OutputImpl]) -> String { - let mut output = String::new(); - - output.push_str("# CLI Output Source Summary\n\n"); - output.push_str( - "Generated from Rust source. Review `$type` names and Rust type mappings.\n\n", - ); - - output.push_str("## Outputs\n\n"); - output.push_str("| `$type` | Rust Type | Source |\n"); - output.push_str("|---|---|---|\n"); - - for item in summary { - output.push_str(&format!( - "| `{}` | `{}` | `{}` |\n", - escape_table_cell(&item.type_name()), - escape_table_cell(&item.rust_type), - item.file.display(), - )); - } - - output - } - - fn escape_table_cell(value: &str) -> String { - value.replace('|', "\\|").replace('\n', " ") - } - - fn arb_registered_output_document() -> BoxedStrategy { - let strategies = STRUCTURED_OUTPUT_TEST_REGISTRY - .iter() - .map(|entry| (entry.arbitrary)()) - .collect::>(); - proptest::strategy::Union::new(strategies).boxed() - } - - fn serialized_output(strategy: impl Strategy + 'static) -> OutputDocumentStrategy - where - T: StructuredOutput + 'static, - { - strategy - .prop_map(|output| { - to_structured_output_value(output).expect("generated DTO should serialize") - }) - .boxed() - } - - fn empty_deployment_diff() -> golem_common::model::diff::DeploymentDiff { - golem_common::model::diff::DeploymentDiff { - components: BTreeMap::new(), - http_api_deployments: BTreeMap::new(), - mcp_deployments: BTreeMap::new(), - } - } - - fn arb_small_string() -> BoxedStrategy { - any::() - .prop_map(|value| uuid::Uuid::from_u128(value).to_string()) - .boxed() - } - - fn arb_uuid() -> BoxedStrategy { - any::().prop_map(uuid::Uuid::from_u128).boxed() - } - - fn arb_small_u64() -> BoxedStrategy { - (0u64..1000).boxed() - } - - fn arb_timestamp_string() -> BoxedStrategy { - arb_datetime().prop_map(|value| value.to_rfc3339()).boxed() - } - - fn arb_timestamp() -> BoxedStrategy { - arb_timestamp_string() - .prop_map(|value| value.parse().expect("generated timestamp should parse")) - .boxed() - } - - fn arb_url_string() -> BoxedStrategy { - arb_small_string() - .prop_map(|path| format!("https://example.com/{path}")) - .boxed() - } - - fn arb_datetime() -> BoxedStrategy> { - (0i64..4_102_444_800i64) - .prop_map(|seconds| { - chrono::DateTime::from_timestamp(seconds, 0) - .expect("generated timestamp should be in range") - }) - .boxed() - } - - fn fixed_datetime() -> chrono::DateTime { - chrono::DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z") - .expect("fixed timestamp should parse") - .with_timezone(&chrono::Utc) - } - - fn arb_hash() -> BoxedStrategy { - any::() - .prop_map(|value| { - golem_common::model::diff::Hash::new(blake3::hash(&value.to_le_bytes())) - }) - .boxed() - } - - fn arb_agent_status() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::AgentStatus::Running), - Just(golem_common::model::AgentStatus::Idle), - Just(golem_common::model::AgentStatus::Suspended), - Just(golem_common::model::AgentStatus::Interrupted), - Just(golem_common::model::AgentStatus::Retrying), - Just(golem_common::model::AgentStatus::Failed), - Just(golem_common::model::AgentStatus::Exited), - ] - .boxed() - } - - fn sample_public_oplog_entries() -> Vec { - use golem_common::base_model::retry_policy::{ - ApiImmediatePolicy, ApiPredicate, ApiPredicateTrue, ApiRetryPolicy, - }; - use golem_common::model::component::{ComponentId, ComponentRevision, PluginPriority}; - use golem_common::model::environment::EnvironmentId; - use golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId; - use golem_common::model::invocation_context::{SpanId, TraceId}; - use golem_common::model::oplog::public_oplog_entry::*; - use golem_common::model::oplog::*; - use golem_common::model::regions::OplogRegion; - use golem_common::model::{AgentId, Empty, IdempotencyKey, Timestamp}; - use golem_common::schema::{SchemaGraph, SchemaType, SchemaValue, TypedSchemaValue}; - use std::iter::FromIterator; - use uuid::Uuid; - - fn timestamp() -> Timestamp { - Timestamp::from(0) - } - - fn component_id() -> ComponentId { - ComponentId(Uuid::parse_str("13a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap()) - } - - fn agent_id(name: &str) -> AgentId { - AgentId { - component_id: component_id(), - agent_id: name.to_string(), - } - } - - fn plugin(priority: i32) -> PluginInstallationDescription { - PluginInstallationDescription { - environment_plugin_grant_id: EnvironmentPluginGrantId::new(), - plugin_priority: PluginPriority(priority), - plugin_name: "generated-plugin".to_string(), - plugin_version: "1.0.0".to_string(), - parameters: BTreeMap::from_iter([("key".to_string(), "value".to_string())]), - } - } - - fn typed_string_value(value: &str) -> TypedSchemaValue { - TypedSchemaValue::new( - SchemaGraph::anonymous(SchemaType::string()), - SchemaValue::String(value.to_string()), - ) - } - - fn typed_u64_list_value(values: Vec) -> TypedSchemaValue { - TypedSchemaValue::new( - SchemaGraph::anonymous(SchemaType::list(SchemaType::u64())), - SchemaValue::List { - elements: values.into_iter().map(SchemaValue::U64).collect(), - }, - ) - } - - fn span_context() -> Vec> { - vec![vec![PublicSpanData::LocalSpan(PublicLocalSpanData { - span_id: SpanId::generate(), - start: timestamp(), - parent_id: None, - linked_context: Some(1), - attributes: vec![PublicAttribute { - key: "component".to_string(), - value: PublicAttributeValue::String(StringAttributeValue { - value: "generated".to_string(), - }), - }], - inherited: true, - })]] - } - - fn method_invocation() -> PublicAgentInvocation { - PublicAgentInvocation::AgentMethodInvocation(AgentMethodInvocationParameters { - idempotency_key: IdempotencyKey::new("method-key".to_string()), - method_name: "generated-method".to_string(), - function_input: typed_string_value("input"), - trace_id: TraceId::generate(), - trace_states: vec!["trace-state".to_string()], - invocation_context: span_context(), - }) - } - - fn raw_snapshot() -> PublicSnapshotData { - PublicSnapshotData::Raw(RawSnapshotData { - data: vec![1, 2, 3], - mime_type: "application/octet-stream".to_string(), - }) - } - - fn json_snapshot() -> PublicSnapshotData { - PublicSnapshotData::Json(JsonSnapshotData { - data: json!({ "counter": 42 }), - }) - } - - fn multipart_snapshot() -> PublicSnapshotData { - PublicSnapshotData::Multipart(MultipartSnapshotData { - mime_type: "multipart/mixed; boundary=generated".to_string(), - parts: vec![ - MultipartSnapshotPart { - name: "state".to_string(), - content_type: "application/json".to_string(), - data: MultipartPartData::Json(JsonSnapshotData { - data: json!({ "state": "ok" }), - }), - }, - MultipartSnapshotPart { - name: "bytes".to_string(), - content_type: "application/octet-stream".to_string(), - data: MultipartPartData::Raw(RawSnapshotData { - data: vec![4, 5, 6], - mime_type: "application/octet-stream".to_string(), - }), - }, - ], - }) - } - - let retry_policy_state = PublicRetryPolicyState::AndThen(PublicRetryPolicyStateAndThen { - left: Box::new(PublicRetryPolicyState::Counter( - PublicRetryPolicyStateCounter { count: 2 }, - )), - right: Box::new(PublicRetryPolicyState::Terminal(Empty {})), - on_right: true, - }); - - let retry_policy = PublicNamedRetryPolicy { - name: "generated-retry".to_string(), - priority: 10, - predicate: ApiPredicate::True(ApiPredicateTrue {}), - policy: ApiRetryPolicy::Immediate(ApiImmediatePolicy {}), - }; - - vec![ - PublicOplogEntry::Create(CreateParams { - timestamp: timestamp(), - agent_id: agent_id("generated-agent"), - agent_mode: golem_common::model::agent::AgentMode::Durable, - component_revision: ComponentRevision::new(1).unwrap(), - env: BTreeMap::from_iter([("ENV".to_string(), "value".to_string())]), - created_by: golem_common::model::account::AccountId::new(), - local_agent_config: vec![PublicTypedAgentConfigEntry { - path: vec!["config".to_string()], - value: typed_string_value("configured"), - }], - environment_id: EnvironmentId::new(), - parent: Some(agent_id("parent-agent")), - component_size: 10, - initial_total_linear_memory_size: 20, - initial_active_plugins: BTreeSet::from_iter([plugin(0)]), - original_phantom_id: Some( - Uuid::parse_str("23a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap(), - ), - instance_id: Uuid::parse_str("33a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap(), - }), - PublicOplogEntry::Start(StartParams { - timestamp: timestamp(), - parent_start_index: Some(OplogIndex::from_u64(1)), - function_name: "wasi:keyvalue/store.{get}".to_string(), - request: Some(typed_string_value("request")), - durable_function_type: PublicDurableFunctionType::WriteRemoteBatched( - WriteRemoteBatchedParameters { - index: Some(OplogIndex::from_u64(1)), - }, - ), - }), - PublicOplogEntry::End(EndParams { - timestamp: timestamp(), - start_index: OplogIndex::from_u64(1), - response: Some(typed_u64_list_value(vec![1])), - forced_commit: false, - }), - PublicOplogEntry::Cancelled(CancelledParams { - timestamp: timestamp(), - start_index: OplogIndex::from_u64(2), - partial: Some(typed_string_value("partial")), - }), - PublicOplogEntry::AgentInvocationStarted(AgentInvocationStartedParams { - timestamp: timestamp(), - invocation: method_invocation(), - }), - PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { - timestamp: timestamp(), - result: PublicAgentInvocationResult::AgentMethod(AgentInvocationOutputParameters { - output: typed_string_value("output"), - }), - method_name: Some("generated-method".to_string()), - consumed_fuel: 100, - component_revision: ComponentRevision::new(1).unwrap(), - }), - PublicOplogEntry::Suspend(SuspendParams { - timestamp: timestamp(), - }), - PublicOplogEntry::Error(ErrorParams { - timestamp: timestamp(), - error: "generated error".to_string(), - retry_from: OplogIndex::INITIAL, - inside_atomic_region: false, - retry_policy_state: Some(retry_policy_state), - }), - PublicOplogEntry::NoOp(NoOpParams { - timestamp: timestamp(), - }), - PublicOplogEntry::Jump(JumpParams { - timestamp: timestamp(), - jump: OplogRegion { - start: OplogIndex::from_u64(1), - end: OplogIndex::from_u64(2), - }, - }), - PublicOplogEntry::Interrupted(InterruptedParams { - timestamp: timestamp(), - }), - PublicOplogEntry::Exited(ExitedParams { - timestamp: timestamp(), - }), - PublicOplogEntry::BeginAtomicRegion(BeginAtomicRegionParams { - timestamp: timestamp(), - }), - PublicOplogEntry::EndAtomicRegion(EndAtomicRegionParams { - timestamp: timestamp(), - begin_index: OplogIndex::from_u64(1), - }), - PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { - timestamp: timestamp(), - invocation: PublicAgentInvocation::AgentInitialization( - AgentInitializationParameters { - idempotency_key: IdempotencyKey::new("init-key".to_string()), - constructor_parameters: typed_string_value("constructor"), - trace_id: TraceId::generate(), - trace_states: vec![], - invocation_context: span_context(), - }, - ), - }), - PublicOplogEntry::PendingUpdate(PendingUpdateParams { - timestamp: timestamp(), - target_revision: ComponentRevision::new(2).unwrap(), - description: PublicUpdateDescription::SnapshotBased( - SnapshotBasedUpdateParameters { - payload: vec![7, 8, 9], - mime_type: "application/octet-stream".to_string(), - }, - ), - }), - PublicOplogEntry::SuccessfulUpdate(SuccessfulUpdateParams { - timestamp: timestamp(), - target_revision: ComponentRevision::new(2).unwrap(), - new_component_size: 30, - new_active_plugins: BTreeSet::from_iter([plugin(1)]), - }), - PublicOplogEntry::FailedUpdate(FailedUpdateParams { - timestamp: timestamp(), - target_revision: ComponentRevision::new(3).unwrap(), - details: None, - }), - PublicOplogEntry::GrowMemory(GrowMemoryParams { - timestamp: timestamp(), - delta: 64, - }), - PublicOplogEntry::FilesystemStorageUsageUpdate(FilesystemStorageUsageUpdateParams { - timestamp: timestamp(), - delta: -5, - }), - PublicOplogEntry::CreateResource(CreateResourceParams { - timestamp: timestamp(), - id: AgentResourceId(1), - name: "resource".to_string(), - owner: "owner".to_string(), - }), - PublicOplogEntry::DropResource(DropResourceParams { - timestamp: timestamp(), - id: AgentResourceId(1), - name: "resource".to_string(), - owner: "owner".to_string(), - }), - PublicOplogEntry::Log(LogParams { - timestamp: timestamp(), - level: LogLevel::Info, - context: "generated".to_string(), - message: "message".to_string(), - }), - PublicOplogEntry::Restart(RestartParams { - timestamp: timestamp(), - }), - PublicOplogEntry::ActivatePlugin(ActivatePluginParams { - timestamp: timestamp(), - plugin: plugin(2), - }), - PublicOplogEntry::DeactivatePlugin(DeactivatePluginParams { - timestamp: timestamp(), - plugin: plugin(3), - }), - PublicOplogEntry::Revert(RevertParams { - timestamp: timestamp(), - dropped_region: OplogRegion { - start: OplogIndex::from_u64(5), - end: OplogIndex::from_u64(10), - }, - }), - PublicOplogEntry::CancelPendingInvocation(CancelPendingInvocationParams { - timestamp: timestamp(), - idempotency_key: IdempotencyKey::new("cancel-key".to_string()), - }), - PublicOplogEntry::StartSpan(StartSpanParams { - timestamp: timestamp(), - span_id: SpanId::generate(), - parent_id: Some(SpanId::generate()), - linked_context: Some(SpanId::generate()), - attributes: vec![PublicAttribute { - key: "http.method".to_string(), - value: PublicAttributeValue::String(StringAttributeValue { - value: "GET".to_string(), - }), - }], - }), - PublicOplogEntry::FinishSpan(FinishSpanParams { - timestamp: timestamp(), - span_id: SpanId::generate(), - }), - PublicOplogEntry::SetSpanAttribute(SetSpanAttributeParams { - timestamp: timestamp(), - span_id: SpanId::generate(), - key: "http.status_code".to_string(), - value: PublicAttributeValue::String(StringAttributeValue { - value: "200".to_string(), - }), - }), - PublicOplogEntry::ChangePersistenceLevel(ChangePersistenceLevelParams { - timestamp: timestamp(), - persistence_level: PersistenceLevel::Smart, - }), - PublicOplogEntry::BeginRemoteTransaction(BeginRemoteTransactionParams { - timestamp: timestamp(), - transaction_id: golem_common::model::TransactionId::new("txn-1".to_string()), - }), - PublicOplogEntry::PreCommitRemoteTransaction(PreCommitRemoteTransactionParams { - timestamp: timestamp(), - begin_index: OplogIndex::from_u64(11), - }), - PublicOplogEntry::PreRollbackRemoteTransaction(PreRollbackRemoteTransactionParams { - timestamp: timestamp(), - begin_index: OplogIndex::from_u64(11), - }), - PublicOplogEntry::CommittedRemoteTransaction(CommittedRemoteTransactionParams { - timestamp: timestamp(), - begin_index: OplogIndex::from_u64(11), - }), - PublicOplogEntry::RolledBackRemoteTransaction(RolledBackRemoteTransactionParams { - timestamp: timestamp(), - begin_index: OplogIndex::from_u64(11), - }), - PublicOplogEntry::Snapshot(SnapshotParams { - timestamp: timestamp(), - data: raw_snapshot(), - }), - PublicOplogEntry::Snapshot(SnapshotParams { - timestamp: timestamp(), - data: json_snapshot(), - }), - PublicOplogEntry::Snapshot(SnapshotParams { - timestamp: timestamp(), - data: multipart_snapshot(), - }), - PublicOplogEntry::OplogProcessorCheckpoint(OplogProcessorCheckpointParams { - timestamp: timestamp(), - plugin: plugin(4), - target_agent_id: agent_id("target-agent"), - confirmed_up_to: OplogIndex::from_u64(20), - sending_up_to: OplogIndex::from_u64(21), - last_batch_start: OplogIndex::from_u64(19), - }), - PublicOplogEntry::SetRetryPolicy(SetRetryPolicyParams { - timestamp: timestamp(), - policy: retry_policy, - }), - PublicOplogEntry::RemoveRetryPolicy(RemoveRetryPolicyParams { - timestamp: timestamp(), - name: "generated-retry".to_string(), - }), - PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { - timestamp: timestamp(), - result: PublicAgentInvocationResult::SaveSnapshot(SaveSnapshotResultParameters { - snapshot: json_snapshot(), - }), - method_name: Some("generated-method".to_string()), - consumed_fuel: 101, - component_revision: ComponentRevision::new(4).unwrap(), - }), - PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { - timestamp: timestamp(), - invocation: PublicAgentInvocation::LoadSnapshot(LoadSnapshotParameters { - snapshot: multipart_snapshot(), - }), - }), - PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { - timestamp: timestamp(), - invocation: PublicAgentInvocation::SaveSnapshot(Empty {}), - }), - PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { - timestamp: timestamp(), - invocation: PublicAgentInvocation::ProcessOplogEntries( - ProcessOplogEntriesParameters { - idempotency_key: IdempotencyKey::new("process-key".to_string()), - }, - ), - }), - PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { - timestamp: timestamp(), - invocation: PublicAgentInvocation::ManualUpdate(ManualUpdateParameters { - target_revision: ComponentRevision::new(5).unwrap(), - }), - }), - PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { - timestamp: timestamp(), - result: PublicAgentInvocationResult::LoadSnapshot(FallibleResultParameters { - error: Some("load failed".to_string()), - }), - method_name: Some("generated-method".to_string()), - consumed_fuel: 102, - component_revision: ComponentRevision::new(5).unwrap(), - }), - PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { - timestamp: timestamp(), - result: PublicAgentInvocationResult::ProcessOplogEntries( - ProcessOplogEntriesResultParameters { error: None }, - ), - method_name: Some("generated-method".to_string()), - consumed_fuel: 103, - component_revision: ComponentRevision::new(6).unwrap(), - }), - PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { - timestamp: timestamp(), - result: PublicAgentInvocationResult::ManualUpdate(Empty {}), - method_name: Some("generated-method".to_string()), - consumed_fuel: 104, - component_revision: ComponentRevision::new(7).unwrap(), - }), - PublicOplogEntry::PendingUpdate(PendingUpdateParams { - timestamp: timestamp(), - target_revision: ComponentRevision::new(8).unwrap(), - description: PublicUpdateDescription::Automatic(Empty {}), - }), - ] - } - - fn arb_build_result() -> OutputDocumentStrategy { - serialized_output( - any::() - .prop_map(|built| crate::model::text::action_result::BuildResult { built }), - ) - } - - fn arb_clean_result() -> OutputDocumentStrategy { - serialized_output( - any::() - .prop_map(|cleaned| crate::model::text::action_result::CleanResult { cleaned }), - ) - } - - fn arb_deploy_result() -> OutputDocumentStrategy { - serialized_output( - any::().prop_map(|deployed| { - crate::model::text::action_result::DeployResultView { deployed } - }), - ) - } - - fn arb_generate_bridge_result() -> OutputDocumentStrategy { - serialized_output(any::().prop_map(|generated| { - crate::model::text::action_result::GenerateBridgeResult { generated } - })) - } - - fn arb_agent_type_get_result() -> OutputDocumentStrategy { - serialized_output( - (arb_small_string(), arb_small_string(), arb_small_string()).prop_map( - |(agent_type, constructor, description)| crate::model::agent::view::AgentTypeView { - agent_type, - constructor, - description, - }, - ), - ) - } - - fn arb_agent_type_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_deployed_registered_agent_type(), 0..3).prop_map( - |agent_types| crate::model::text::agent::AgentTypeListView { agent_types }, - ), - ) - } - - fn arb_deployed_registered_agent_type() - -> BoxedStrategy { - ( - arb_agent_type(), - arb_uuid(), - arb_small_u64(), - arb_small_string(), - arb_uuid(), - arb_small_string(), - proptest::option::of(arb_small_string()), - ) - .prop_map( - |( - agent_type, - component_id, - component_revision, - component_name, - account_id, - account_email, - webhook_prefix_authority_and_path, - )| golem_common::model::agent::DeployedRegisteredAgentType { - agent_type, - implemented_by: golem_common::model::agent::RegisteredAgentTypeImplementer { - component_id: golem_common::model::component::ComponentId(component_id), - component_revision: golem_common::model::component::ComponentRevision::new( - component_revision, - ) - .expect("generated revision should be valid"), - component_name, - account_id: golem_common::model::account::AccountId(account_id), - account_email: golem_common::model::account::AccountEmail::new( - account_email, - ), - }, - webhook_prefix_authority_and_path, - }, - ) - .boxed() - } - - fn arb_agent_type() -> BoxedStrategy { - ( - arb_agent_type_name(), - arb_small_string(), - arb_small_string(), - arb_agent_constructor(), - proptest::collection::vec(arb_agent_method(), 1..3), - proptest::collection::vec(arb_agent_dependency(), 1..2), - arb_agent_mode(), - proptest::option::of(arb_http_mount_details()), - arb_snapshotting(), - proptest::collection::vec(arb_agent_config_declaration(), 1..3), - ) - .prop_map( - |( - type_name, - description, - source_language, - constructor, - methods, - dependencies, - mode, - http_mount, - snapshotting, - config, - )| { - let methods = if mode == golem_common::model::agent::AgentMode::Ephemeral { - methods - .into_iter() - .map(|mut method| { - method.read_only = None; - method - }) - .collect() - } else { - methods - }; - - golem_common::schema::agent::AgentTypeSchema { - type_name, - description, - source_language, - schema: golem_common::schema::SchemaGraph::empty(), - constructor, - methods, - dependencies, - mode, - http_mount, - snapshotting, - config, - } - }, - ) - .boxed() - } - - fn arb_agent_constructor() -> BoxedStrategy - { - ( - proptest::option::of(arb_small_string()), - arb_small_string(), - proptest::option::of(arb_small_string()), - arb_input_schema(), - ) - .prop_map(|(name, description, prompt_hint, input_schema)| { - golem_common::schema::agent::AgentConstructorSchema { - name, - description, - prompt_hint, - input_schema, - } - }) - .boxed() - } - - fn arb_agent_method() -> BoxedStrategy { - ( - arb_small_string(), - arb_small_string(), - proptest::option::of(arb_small_string()), - arb_input_schema(), - arb_output_schema(), - proptest::collection::vec(arb_http_endpoint_details(), 0..3), - proptest::option::of(arb_read_only_config()), - ) - .prop_map( - |( - name, - description, - prompt_hint, - input_schema, - output_schema, - http_endpoint, - read_only, - )| { - golem_common::schema::agent::AgentMethodSchema { - name, - description, - prompt_hint, - input_schema, - output_schema, - http_endpoint, - read_only, - } - }, - ) - .boxed() - } - - fn arb_agent_dependency() -> BoxedStrategy { - ( - arb_small_string(), - proptest::option::of(arb_small_string()), - arb_agent_constructor(), - proptest::collection::vec(arb_agent_method(), 1..2), - ) - .prop_map(|(type_name, description, constructor, methods)| { - golem_common::schema::agent::AgentDependencySchema { - type_name, - description, - schema: golem_common::schema::SchemaGraph::empty(), - constructor, - methods, - } - }) - .boxed() - } - - fn arb_agent_mode() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::agent::AgentMode::Durable), - Just(golem_common::model::agent::AgentMode::Ephemeral), - ] - .boxed() - } - - /// Structural [`SchemaType`] strategy covering every schema-native type - /// case. Derived from the shared `golem-schema` graph strategy (we take the - /// generated graph's root), so new `SchemaType` variants are exercised - /// automatically. - fn arb_schema_type() -> BoxedStrategy { - golem_common::schema::proptest_strategies::schema_graph_strategy() - .prop_map(|graph| graph.root) - .boxed() - } - - fn arb_metadata_envelope() -> BoxedStrategy { - ( - proptest::option::of(arb_small_string()), - proptest::collection::vec(arb_small_string(), 0..2), - proptest::collection::vec(arb_small_string(), 0..2), - proptest::option::of(arb_small_string()), - proptest::option::of(prop_oneof![ - Just(golem_common::schema::Role::Multimodal), - Just(golem_common::schema::Role::UnstructuredText), - Just(golem_common::schema::Role::UnstructuredBinary), - arb_small_string().prop_map(golem_common::schema::Role::Other), - ]), - ) - .prop_map(|(doc, aliases, examples, deprecated, role)| { - golem_common::schema::MetadataEnvelope { - doc, - aliases, - examples, - deprecated, - role, - } - }) - .boxed() - } - - fn arb_field_source() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::schema::FieldSource::UserSupplied), - Just(golem_common::schema::FieldSource::AutoInjected( - golem_common::schema::AutoInjectedKind::Principal, - )), - ] - .boxed() - } - - fn arb_named_field() -> BoxedStrategy { - ( - arb_small_string(), - arb_field_source(), - arb_schema_type(), - arb_metadata_envelope(), - ) - .prop_map( - |(name, source, schema, metadata)| golem_common::schema::NamedField { - name, - source, - schema, - metadata, - }, - ) - .boxed() - } - - fn arb_input_schema() -> BoxedStrategy { - proptest::collection::vec(arb_named_field(), 0..3) - .prop_map(golem_common::schema::agent::InputSchema::Parameters) - .boxed() - } - - fn arb_output_schema() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::schema::agent::OutputSchema::Unit), - arb_schema_type().prop_map(|schema_type| { - golem_common::schema::agent::OutputSchema::Single(Box::new(schema_type)) - }), - ] - .boxed() - } - - fn arb_read_only_config() -> BoxedStrategy { - (arb_cache_policy(), any::()) - .prop_map( - |(cache_policy, uses_principal)| golem_common::model::agent::ReadOnlyConfig { - cache_policy, - uses_principal, - }, - ) - .boxed() - } - - fn arb_cache_policy() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::agent::CachePolicy::NoCache( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::CachePolicy::UntilWrite( - golem_common::model::Empty {} - )), - arb_small_u64().prop_map(|duration_nanos| { - golem_common::model::agent::CachePolicy::Ttl( - golem_common::model::agent::CachePolicyTtl { duration_nanos }, - ) - }), - ] - .boxed() - } - - fn arb_snapshotting() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::agent::Snapshotting::Disabled( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::Snapshotting::Enabled( - golem_common::model::agent::SnapshottingConfig::Default( - golem_common::model::Empty {}, - ) - )), - arb_small_u64().prop_map(|duration_nanos| { - golem_common::model::agent::Snapshotting::Enabled( - golem_common::model::agent::SnapshottingConfig::Periodic( - golem_common::model::agent::SnapshottingPeriodic { duration_nanos }, - ), - ) - }), - any::().prop_map(|count| { - golem_common::model::agent::Snapshotting::Enabled( - golem_common::model::agent::SnapshottingConfig::EveryNInvocation( - golem_common::model::agent::SnapshottingEveryNInvocation { count }, - ), - ) - }), - ] - .boxed() - } - - fn arb_agent_config_declaration() - -> BoxedStrategy { - ( - prop_oneof![ - Just(golem_common::model::agent::AgentConfigSource::Local), - Just(golem_common::model::agent::AgentConfigSource::Secret), - ], - proptest::collection::vec(arb_small_string(), 1..3), - arb_schema_type(), - ) - .prop_map(|(source, path, value_type)| { - golem_common::schema::agent::AgentConfigDeclarationSchema { - source, - path, - value_type, - } - }) - .boxed() - } - - fn arb_http_mount_details() -> BoxedStrategy { - ( - proptest::collection::vec(arb_path_segment(), 0..2), - proptest::option::of(any::().prop_map(|required| { - golem_common::model::agent::AgentHttpAuthDetails { required } - })), - any::(), - proptest::collection::vec(arb_small_string(), 0..2), - proptest::collection::vec(arb_path_segment(), 0..2), - ) - .prop_map( - |(path_prefix, auth_details, phantom_agent, allowed_patterns, webhook_suffix)| { - golem_common::model::agent::HttpMountDetails { - path_prefix, - auth_details, - phantom_agent, - cors_options: golem_common::model::agent::CorsOptions { allowed_patterns }, - webhook_suffix, - } - }, - ) - .boxed() - } - - fn arb_http_endpoint_details() -> BoxedStrategy - { - ( - arb_http_method(), - proptest::collection::vec(arb_path_segment(), 0..2), - proptest::collection::vec( - (arb_small_string(), arb_small_string()).prop_map( - |(header_name, variable_name)| golem_common::model::agent::HeaderVariable { - header_name, - variable_name, - }, - ), - 0..2, - ), - proptest::collection::vec( - (arb_small_string(), arb_small_string()).prop_map( - |(query_param_name, variable_name)| golem_common::model::agent::QueryVariable { - query_param_name, - variable_name, - }, - ), - 0..2, - ), - proptest::option::of(any::().prop_map(|required| { - golem_common::model::agent::AgentHttpAuthDetails { required } - })), - proptest::collection::vec(arb_small_string(), 0..2), - ) - .prop_map( - |( - http_method, - path_suffix, - header_vars, - query_vars, - auth_details, - allowed_patterns, - )| { - golem_common::model::agent::HttpEndpointDetails { - http_method, - path_suffix, - header_vars, - query_vars, - auth_details, - cors_options: golem_common::model::agent::CorsOptions { allowed_patterns }, - } - }, - ) - .boxed() - } - - fn arb_http_method() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::agent::HttpMethod::Get( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Head( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Post( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Put( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Delete( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Connect( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Options( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Trace( - golem_common::model::Empty {} - )), - Just(golem_common::model::agent::HttpMethod::Patch( - golem_common::model::Empty {} - )), - arb_small_string().prop_map(|value| { - golem_common::model::agent::HttpMethod::Custom( - golem_common::model::agent::CustomHttpMethod { value }, - ) - }), - ] - .boxed() - } - - fn arb_path_segment() -> BoxedStrategy { - prop_oneof![ - arb_small_string().prop_map(|value| { - golem_common::model::agent::PathSegment::Literal( - golem_common::model::agent::LiteralSegment { value }, - ) - }), - arb_small_string().prop_map(|variable_name| { - golem_common::model::agent::PathSegment::PathVariable( - golem_common::model::agent::PathVariable { variable_name }, - ) - }), - arb_small_string().prop_map(|variable_name| { - golem_common::model::agent::PathSegment::RemainingPathVariable( - golem_common::model::agent::PathVariable { variable_name }, - ) - }), - prop_oneof![ - Just(golem_common::model::agent::SystemVariable::AgentType), - Just(golem_common::model::agent::SystemVariable::AgentVersion), - ] - .prop_map(|value| { - golem_common::model::agent::PathSegment::SystemVariable( - golem_common::model::agent::SystemVariableSegment { value }, - ) - }), - ] - .boxed() - } - - fn arb_agent_files_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::agent::AgentFilesView { nodes }), - ) - } - - fn arb_file_node() -> BoxedStrategy { - ( - arb_small_string(), - arb_small_string(), - arb_timestamp_string(), - arb_timestamp_string(), - arb_small_u64(), - ) - .prop_map(|(name, last_modified, kind, permissions, size)| { - crate::model::text::agent::FileNodeView { - name, - last_modified, - kind, - permissions, - size, - } - }) - .boxed() - } - - fn arb_agent_get_result() -> OutputDocumentStrategy { - serialized_output((arb_agent_metadata_view(), any::()).prop_map( - |(metadata, precise)| crate::model::text::agent::AgentGetView { metadata, precise }, - )) - } - - fn arb_agent_invoke_result() -> OutputDocumentStrategy { - ( - arb_small_string(), - prop_oneof![Just(0u8), Just(1u8)], - arb_value_and_type(), - arb_format_string(), - ) - .prop_map(|(idempotency_key, shape, result_json, result_format)| { - to_structured_output_value(crate::model::invoke_result_view::InvokeResultView { - idempotency_key, - result_json: (shape == 1).then_some(result_json), - result: None, - result_format: (shape != 0).then_some(result_format.to_string()), - is_void_result: shape == 0, - }) - .expect("generated invoke result should serialize") - }) - .boxed() - } - - /// Structural [`TypedSchemaValue`] strategy covering every schema-native - /// value and type case. Reuses the shared `golem-schema` strategy so new - /// `SchemaValue` / `SchemaType` / `SchemaGraph` shapes are exercised - /// automatically. - fn arb_value_and_type() -> BoxedStrategy { - golem_common::schema::proptest_strategies::typed_schema_value_strategy().boxed() - } - - fn arb_agent_list_result() -> OutputDocumentStrategy { - ( - proptest::collection::vec(arb_agent_metadata_view(), 0..5), - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), - ) - .prop_map( - |(agents, cursors)| crate::model::agent::AgentsMetadataResponseView { - agents, - cursors, - }, - ) - .prop_map(|output| { - to_structured_output_value(output).expect("generated DTO should serialize") - }) - .boxed() - } - - fn arb_agent_new_result() -> OutputDocumentStrategy { - serialized_output((arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_id)| crate::model::text::agent::AgentCreateView { - component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent::RawAgentId(agent_id), - }, - )) - } - - fn arb_agent_oplog_result() -> OutputDocumentStrategy { - serialized_output( - ( - arb_small_u64(), - prop_oneof![ - proptest::sample::select(sample_public_oplog_entries()), - arb_typed_value_oplog_entry(), - ], - ) - .prop_map(|(index, entry)| { - crate::model::text::agent::AgentOplogEntryView { index, entry } - }), - ) - } - - /// Oplog entry carrying a structurally-comprehensive [`TypedSchemaValue`] - /// (full `SchemaGraph` + `SchemaValue`). This is the non-masked path that - /// exercises every schema-native value/graph case against the schema's - /// `TypedSchemaValue` definition, so new variants are caught automatically. - fn arb_typed_value_oplog_entry() -> BoxedStrategy - { - golem_common::schema::proptest_strategies::typed_schema_value_strategy() - .prop_map(|response| { - golem_common::model::oplog::PublicOplogEntry::End( - golem_common::model::oplog::public_oplog_entry::EndParams { - timestamp: golem_common::model::Timestamp::from(0), - start_index: golem_common::model::oplog::OplogIndex::from_u64(1), - response: Some(response), - forced_commit: false, - }, - ) - }) - .boxed() - } - - fn arb_agent_stream_event() -> OutputDocumentStrategy { - serialized_output( - ( - arb_timestamp(), - arb_agent_stream_event_kind(), - arb_small_string(), - arb_small_string(), - arb_small_string(), - proptest::option::of(arb_small_string()), - proptest::option::of(arb_small_string()), - proptest::option::of(arb_small_u64()), - proptest::option::of(arb_small_string()), - ) - .prop_map( - |( - timestamp, - kind, - level, - context, - message, - function_name, - idempotency_key, - number_of_missed_messages, - error, - )| crate::model::agent::stream::AgentStreamEvent { - timestamp, - kind, - level, - context, - message, - function_name, - idempotency_key, - number_of_missed_messages, - error, - }, - ), - ) - } - - fn arb_agent_stream_event_kind() - -> BoxedStrategy { - prop_oneof![ - Just(crate::model::agent::stream::AgentStreamEventKind::Log), - Just(crate::model::agent::stream::AgentStreamEventKind::Stdout), - Just(crate::model::agent::stream::AgentStreamEventKind::Stderr), - Just(crate::model::agent::stream::AgentStreamEventKind::StreamClosed), - Just(crate::model::agent::stream::AgentStreamEventKind::StreamError), - Just(crate::model::agent::stream::AgentStreamEventKind::InvocationStarted), - Just(crate::model::agent::stream::AgentStreamEventKind::InvocationFinished), - Just(crate::model::agent::stream::AgentStreamEventKind::MissedMessages), - ] - .boxed() - } - - fn arb_agent_update_result() -> OutputDocumentStrategy { - serialized_output( - ( - proptest::collection::vec(arb_agent_update_meta(), 0..5), - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), - ) - .prop_map(|(agents, errors)| { - crate::model::deploy::TryUpdateAllWorkersResult { agents, errors } - }), - ) - } - - /// Shared field generator for the two identical revision-transition metas - /// (`AgentUpdateMeta` / `AgentRedeploymentMeta`). - fn arb_agent_transition_fields() -> BoxedStrategy<( - golem_common::model::component::ComponentName, - crate::model::agent::RawAgentId, - golem_common::model::component::ComponentRevision, - golem_common::model::component::ComponentRevision, - Option, - Option, - )> { - ( - arb_small_string(), - arb_small_string(), - arb_small_u64(), - arb_small_u64(), - proptest::option::of(arb_small_string()), - proptest::option::of(arb_small_string()), - ) - .prop_map( - |(component_name, agent_id, from_revision, revision, from_version, version)| { - ( - golem_common::model::component::ComponentName(component_name), - crate::model::agent::RawAgentId(agent_id), - golem_common::model::component::ComponentRevision::new(from_revision) - .expect("generated revision should be valid"), - golem_common::model::component::ComponentRevision::new(revision) - .expect("generated revision should be valid"), - from_version, - version, - ) - }, - ) - .boxed() - } - - fn arb_agent_update_meta() -> BoxedStrategy { - arb_agent_transition_fields() - .prop_map( - |(component_name, agent_id, from_revision, revision, from_version, version)| { - crate::model::deploy::AgentUpdateMeta { - component_name, - agent_id, - from_revision, - revision, - from_version, - version, - } - }, - ) - .boxed() - } - - fn arb_agent_redeployment_meta() - -> BoxedStrategy { - arb_agent_transition_fields() - .prop_map( - |(component_name, agent_id, from_revision, revision, from_version, version)| { - crate::model::text::action_result::AgentRedeploymentMeta { - component_name, - agent_id, - from_revision, - revision, - from_version, - version, - } - }, - ) - .boxed() - } - - fn arb_agent_deletion_meta() - -> BoxedStrategy { - (arb_small_string(), arb_small_string()) - .prop_map(|(component_name, agent_id)| { - crate::model::text::action_result::AgentDeletionMeta { - component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent::RawAgentId(agent_id), - } - }) - .boxed() - } - - fn arb_agent_metadata_view() -> BoxedStrategy { - ( - ( - arb_small_string(), - arb_small_string(), - arb_small_string(), - arb_small_string(), - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), - proptest::collection::vec(arb_agent_config_entry_dto(), 0..4), - proptest::collection::vec(arb_agent_config_entry_dto(), 0..4), - arb_agent_status(), - ), - ( - arb_small_u64(), - any::(), - arb_small_u64(), - proptest::collection::vec(arb_update_record(), 0..4), - arb_timestamp_string(), - proptest::option::of(arb_small_string()), - arb_small_u64(), - arb_small_u64(), - proptest::collection::btree_map( - arb_small_string(), - arb_agent_resource_description(), - 0..4, - ), - ), - ) - .prop_map(|(left, right)| { - let ( - component_name, - agent_id, - created_by, - environment_id, - env, - default_env, - config, - default_config, - status, - ) = left; - let ( - component_revision, - retry_count, - pending_invocation_count, - updates, - created_at, - last_error, - component_size, - total_linear_memory_size, - exported_resource_instances, - ) = right; - - crate::model::agent::AgentMetadataView { - component_name: golem_common::model::component::ComponentName(component_name), - agent_id: crate::model::agent::RawAgentId(agent_id), - created_by: golem_common::model::account::AccountId( - uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), - ), - environment_id: golem_common::model::environment::EnvironmentId( - uuid::Uuid::parse_str(&environment_id) - .expect("generated UUID should parse"), - ), - env: env.into_iter().collect(), - default_env: default_env.into_iter().collect(), - config, - default_config, - status, - component_revision: golem_common::model::component::ComponentRevision::new( - component_revision, - ) - .expect("generated revision should be valid"), - retry_count, - pending_invocation_count, - updates, - created_at: created_at - .parse() - .expect("generated timestamp should parse"), - last_error, - component_size, - total_linear_memory_size, - exported_resource_instances: exported_resource_instances.into_iter().collect(), - source_language: crate::agent_id_display::SourceLanguage::default(), - secret_config_paths: BTreeSet::new(), - } - }) - .boxed() - } - - fn arb_agent_config_entry_dto() - -> BoxedStrategy { - ( - proptest::collection::vec(arb_small_string(), 1..4), - arb_json_value(2), - ) - .prop_map( - |(path, value)| golem_common::model::worker::AgentConfigEntryDto { - path, - value: golem_common::base_model::json::NormalizedJsonValue(value), - }, - ) - .boxed() - } - - fn arb_update_record() -> BoxedStrategy { - prop_oneof![ - (arb_timestamp(), arb_small_u64()).prop_map(|(timestamp, target_revision)| { - golem_common::model::worker::UpdateRecord::PendingUpdate( - golem_common::model::worker::PendingUpdate { - timestamp, - target_revision: golem_common::model::component::ComponentRevision::new( - target_revision, - ) - .expect("generated revision should be valid"), - }, - ) - }), - (arb_timestamp(), arb_small_u64()).prop_map(|(timestamp, target_revision)| { - golem_common::model::worker::UpdateRecord::SuccessfulUpdate( - golem_common::model::worker::SuccessfulUpdate { - timestamp, - target_revision: golem_common::model::component::ComponentRevision::new( - target_revision, - ) - .expect("generated revision should be valid"), - }, - ) - }), - ( - arb_timestamp(), - arb_small_u64(), - proptest::option::of(arb_small_string()), - ) - .prop_map(|(timestamp, target_revision, details)| { - golem_common::model::worker::UpdateRecord::FailedUpdate( - golem_common::model::worker::FailedUpdate { - timestamp, - target_revision: - golem_common::model::component::ComponentRevision::new( - target_revision, - ) - .expect("generated revision should be valid"), - details, - }, - ) - }), - ] - .boxed() - } - - fn arb_agent_resource_description() - -> BoxedStrategy { - (arb_timestamp(), arb_small_string(), arb_small_string()) - .prop_map(|(created_at, resource_owner, resource_name)| { - golem_common::model::AgentResourceDescription { - created_at, - resource_owner, - resource_name, - } - }) - .boxed() - } - - fn arb_agent_delete_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(deleted, agent)| { - crate::model::text::action_result::AgentDeleteResult { - deleted, - agent_id: agent, - } - }), - ) - } - - fn arb_agent_file_contents_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - arb_small_string(), - arb_small_string(), - arb_small_string(), - arb_small_u64(), - ) - .prop_map(|(saved, agent, path, output_path, bytes)| { - crate::model::text::action_result::AgentFileContentsResult { - saved, - agent_id: agent, - path, - output_path: output_path.into(), - bytes: bytes as usize, - } - }), - ) - } - - fn arb_agent_interrupt_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(interrupted, agent)| { - crate::model::text::action_result::AgentInterruptResult { - interrupted, - agent_id: agent, - } - }), - ) - } - - fn arb_agent_resume_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(resumed, agent)| { - crate::model::text::action_result::AgentResumeResult { - resumed, - agent_id: agent, - } - }), - ) - } - - fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(simulated, agent)| { - crate::model::text::action_result::AgentSimulateCrashResult { - simulated, - agent_id: agent, - } - }), - ) - } - - fn arb_account_delete_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(deleted, account_id)| { - crate::model::text::account::AccountDeleteResult { - deleted, - account_id: golem_common::model::account::AccountId( - uuid::Uuid::parse_str(&account_id).expect("generated UUID should parse"), - ), - } - }), - ) - } - - fn arb_account_get_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountGetView)) - } - - fn arb_account_new_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountNewView)) - } - - fn arb_account_update_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountUpdateView)) - } - - fn arb_account() -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - arb_small_string(), - arb_small_string(), - arb_uuid(), - proptest::collection::vec(arb_account_role(), 0..4), - arb_uuid(), - ) - .prop_map( - |(id, revision, name, email, plan_id, roles, account_root_card_id)| { - golem_client::model::Account { - id: golem_common::model::account::AccountId(id), - revision: golem_common::model::account::AccountRevision::new(revision) - .expect("generated revision should be valid"), - name, - email: golem_common::model::account::AccountEmail::new(email), - plan_id: golem_common::model::plan::PlanId(plan_id), - roles, - account_root_card_id: golem_common::model::card::CardId( - account_root_card_id, - ), - } - }, - ) - .boxed() - } - - fn arb_account_role() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::auth::AccountRole::Admin), - Just(golem_common::model::auth::AccountRole::MarketingAdmin), - Just(golem_common::model::auth::AccountRole::BuiltinPluginOwner), - ] - .boxed() - } - - fn arb_permission_share_delete_result() -> OutputDocumentStrategy { - serialized_output((any::(), arb_small_string()).prop_map( - |(deleted, permission_share_id)| { - crate::model::text::account::PermissionShareDeleteResult { - deleted, - permission_share_id: golem_common::model::permission_share::PermissionShareId( - uuid::Uuid::parse_str(&permission_share_id) - .expect("generated UUID should parse"), - ), - } - }, - )) - } - - fn arb_permission_share_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareGetView), - ) - } - - fn arb_permission_share_new_result() -> OutputDocumentStrategy { - serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareNewView), - ) - } - - fn arb_permission_share_update_result() -> OutputDocumentStrategy { - serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareUpdateView), - ) - } - - fn arb_permission_share_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_permission_share(), 0..5).prop_map(|permission_shares| { - crate::model::text::account::PermissionShareListView { permission_shares } - }), - ) - } - - fn arb_permission_share() -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - arb_uuid(), - arb_uuid(), - arb_small_string(), - proptest::option::of(arb_uuid()), - arb_permission_share_data(), - ) - .prop_map( - |( - id, - revision, - owner_account_id, - target_account_id, - name, - current_card_id, - data, - )| { - golem_client::model::PermissionShare { - id: golem_common::model::permission_share::PermissionShareId(id), - revision: - golem_common::model::permission_share::PermissionShareRevision::new( - revision, - ) - .expect("generated revision should be valid"), - owner_account_id: golem_common::model::account::AccountId(owner_account_id), - target_account_id: golem_common::model::account::AccountId( - target_account_id, - ), - name: golem_common::model::permission_share::PermissionShareName(name), - current_card_id: current_card_id.map(golem_common::model::card::CardId), - data, - } - }, - ) - .boxed() - } - - fn arb_permission_share_data() - -> BoxedStrategy { - ( - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - ) - .prop_map( - |(lower_positive, lower_negative, upper_positive, upper_negative)| { - golem_common::model::permission_share::PermissionShareData { - lower_positive, - lower_negative, - upper_positive, - upper_negative, - } - }, - ) - .boxed() - } - - fn arb_card_get_result() -> OutputDocumentStrategy { - serialized_output(arb_stored_card().prop_map(crate::model::text::card::CardGetView)) - } - - fn arb_card_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_stored_card(), 0..5) - .prop_map(|cards| crate::model::text::card::CardListView { cards }), - ) - } - - fn arb_card_revoke_result() -> OutputDocumentStrategy { - serialized_output(proptest::collection::vec(arb_uuid(), 0..5).prop_map( - |revoked_card_ids| crate::model::text::card::CardRevokeResult { revoked_card_ids }, - )) - } - - fn arb_stored_card() -> BoxedStrategy { - prop_oneof![ - arb_card().prop_map(golem_client::model::StoredCard::Concrete), - arb_client_polymorphic_card().prop_map(golem_client::model::StoredCard::Polymorphic), - ] - .boxed() - } - - fn arb_card() -> BoxedStrategy { - ( - arb_uuid(), - proptest::collection::vec(arb_uuid(), 0..3), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - arb_datetime(), - proptest::option::of(arb_datetime()), - proptest::bool::ANY, - proptest::option::of(arb_card_managed_by()), - ) - .prop_map( - |( - card_id, - parent_ids, - lower_positive, - lower_negative, - upper_positive, - upper_negative, - created_at, - expires_at, - system_card, - managed_by, - )| golem_client::model::Card { - card_id, - parent_ids, - lower_positive, - lower_negative, - upper_positive, - upper_negative, - created_at, - expires_at, - system_card, - managed_by, - }, - ) - .boxed() - } - - fn arb_client_polymorphic_card() -> BoxedStrategy { - ( - arb_uuid(), - proptest::collection::vec(arb_uuid(), 0..3), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - proptest::collection::vec(arb_small_string(), 0..4), - arb_datetime(), - proptest::option::of(arb_datetime()), - proptest::bool::ANY, - ) - .prop_map( - |( - card_id, - parent_ids, - lower_positive, - lower_negative, - upper_positive, - upper_negative, - created_at, - expires_at, - system_card, - )| golem_client::model::PolymorphicCard { - card_id, - parent_ids, - lower_positive, - lower_negative, - upper_positive, - upper_negative, - created_at, - expires_at, - system_card, - }, - ) - .boxed() - } - - fn arb_card_managed_by() -> BoxedStrategy { - prop_oneof![ - arb_uuid().prop_map(|account_id| { - golem_client::model::CardManagedBy::AccountRoot( - golem_client::model::CardManagedByAccountRoot { account_id }, - ) - }), - arb_uuid().prop_map(|environment_id| { - golem_client::model::CardManagedBy::EnvironmentDefault( - golem_client::model::CardManagedByEnvironmentDefault { environment_id }, - ) - }), - arb_uuid().prop_map(|permission_share_id| { - golem_client::model::CardManagedBy::PermissionShare( - golem_client::model::CardManagedByPermissionShare { - permission_share_id, - }, - ) - }), - (arb_uuid(), arb_small_u64(), arb_small_string()).prop_map( - |(component_id, component_revision, agent_type)| { - golem_client::model::CardManagedBy::AgentInitial( - golem_client::model::CardManagedByAgentInitial { - component_id, - component_revision, - agent_type, - }, - ) - } - ), - ] - .boxed() - } - - fn arb_agent_cancel_invocation_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string(), arb_small_string()).prop_map( - |(canceled, agent, idempotency_key)| { - crate::model::text::action_result::AgentCancelInvocationResult { - canceled, - agent_id: agent, - idempotency_key, - } - }, - ), - ) - } - - fn arb_agent_delete_all_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - proptest::collection::vec(arb_agent_deletion_meta(), 0..5), - ) - .prop_map(|(deleted, agents)| { - crate::model::text::action_result::AgentDeleteAllResult { deleted, agents } - }), - ) - } - - fn arb_agent_redeploy_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - proptest::collection::vec(arb_agent_redeployment_meta(), 0..5), - ) - .prop_map(|(redeployed, agents)| { - crate::model::text::action_result::AgentRedeployResult { redeployed, agents } - }), - ) - } - - fn arb_agent_revert_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - arb_small_string(), - proptest::option::of(arb_small_u64()), - proptest::option::of(arb_small_u64()), - ) - .prop_map( - |(reverted, agent, last_oplog_index, number_of_invocations)| { - crate::model::text::action_result::AgentRevertResult { - reverted, - agent_id: agent, - last_oplog_index, - number_of_invocations, - } - }, - ), - ) - } - - fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - arb_small_string(), - arb_small_string(), - 0i32..1000, - ) - .prop_map(|(activated, agent, plugin, priority)| { - crate::model::text::action_result::AgentPluginToggleResult { - activated, - agent_id: agent, - plugin, - priority, - } - }), - ) - } - - fn arb_token_delete_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(deleted, token_id)| { - crate::model::text::token::TokenDeleteResult { - deleted, - token_id: golem_common::model::auth::TokenId( - uuid::Uuid::parse_str(&token_id).expect("generated UUID should parse"), - ), - } - }), - ) - } - - fn arb_token_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_token(), 0..5) - .prop_map(|tokens| crate::model::text::token::TokenListView { tokens }), - ) - } - - fn arb_token_new_result() -> OutputDocumentStrategy { - serialized_output(arb_token_with_secret().prop_map(crate::model::text::token::TokenNewView)) - } - - fn arb_token() -> BoxedStrategy { - (arb_uuid(), arb_uuid(), arb_datetime(), arb_datetime()) - .prop_map( - |(id, account_id, created_at, expires_at)| golem_common::model::auth::Token { - id: golem_common::model::auth::TokenId(id), - account_id: golem_common::model::account::AccountId(account_id), - created_at, - expires_at, - }, - ) - .boxed() - } - - fn arb_token_with_secret() -> BoxedStrategy { - ( - arb_uuid(), - arb_uuid(), - arb_small_string(), - arb_datetime(), - arb_datetime(), - ) - .prop_map(|(id, account_id, secret, created_at, expires_at)| { - golem_common::model::auth::TokenWithSecret { - id: golem_common::model::auth::TokenId(id), - secret: golem_common::model::auth::TokenSecret::trusted(secret), - account_id: golem_common::model::account::AccountId(account_id), - created_at, - expires_at, - } - }) - .boxed() - } - - fn arb_api_domain_delete_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string(), arb_small_string()).prop_map( - |(deleted, domain, id)| { - crate::model::text::http_api_domain::DomainRegistrationDeleteResult { - deleted, - domain: golem_common::model::domain_registration::Domain(domain), - id: golem_common::model::domain_registration::DomainRegistrationId( - uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), - ), - } - }, - ), - ) - } - - fn arb_api_domain_register_result() -> OutputDocumentStrategy { - serialized_output( - arb_domain_registration() - .prop_map(crate::model::text::http_api_domain::DomainRegistrationNewView), - ) - } - - fn arb_api_domain_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_domain_registration(), 0..5).prop_map(|domains| { - crate::model::text::http_api_domain::HttpApiDomainListView { domains } - }), - ) - } - - fn arb_domain_registration() - -> BoxedStrategy { - (arb_uuid(), arb_uuid(), arb_small_string()) - .prop_map(|(id, environment_id, domain)| { - golem_common::model::domain_registration::DomainRegistration { - id: golem_common::model::domain_registration::DomainRegistrationId(id), - environment_id: golem_common::model::environment::EnvironmentId(environment_id), - domain: golem_common::model::domain_registration::Domain(domain), - } - }) - .boxed() - } - - fn arb_api_deployment_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_http_api_deployment() - .prop_map(crate::model::text::http_api_deployment::HttpApiDeploymentGetView), - ) - } - - fn arb_api_deployment_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_http_api_deployment(), 0..5).prop_map(|deployments| { - crate::model::text::http_api_deployment::HttpApiDeploymentListView { deployments } - }), - ) - } - - fn arb_http_api_deployment() -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - arb_uuid(), - arb_small_string(), - proptest::collection::btree_map( - arb_agent_type_name(), - arb_http_api_deployment_agent_options(), - 0..4, - ), - arb_small_string(), - arb_small_string(), - arb_datetime(), - ) - .prop_map(|(id, revision, environment_id, domain, agents, webhooks_prefix, openapi_endpoint_prefix, created_at)| { - golem_client::model::HttpApiDeployment { - id: golem_common::model::http_api_deployment::HttpApiDeploymentId(id), - revision: golem_common::model::http_api_deployment::HttpApiDeploymentRevision::new(revision) - .expect("generated revision should be valid"), - environment_id: golem_common::model::environment::EnvironmentId(environment_id), - domain: golem_common::model::domain_registration::Domain(domain.clone()), - hash: golem_common::model::diff::Hash::new(blake3::hash(domain.as_bytes())), - agents, - webhooks_prefix, - openapi_endpoint_prefix, - created_at, - } - }) - .boxed() - } - - fn arb_agent_type_name() -> BoxedStrategy { - arb_small_string() - .prop_map(golem_common::model::agent::AgentTypeName) - .boxed() - } - - fn arb_http_api_deployment_agent_options() - -> BoxedStrategy { - proptest::option::of(prop_oneof![ - arb_small_string().prop_map(|header_name| { - golem_common::model::http_api_deployment::HttpApiDeploymentAgentSecurity::TestSessionHeader( - golem_common::model::http_api_deployment::TestSessionHeaderAgentSecurity { header_name }, - ) - }), - arb_small_string().prop_map(|security_scheme| { - golem_common::model::http_api_deployment::HttpApiDeploymentAgentSecurity::SecurityScheme( - golem_common::model::http_api_deployment::SecuritySchemeAgentSecurity { - security_scheme: golem_common::model::security_scheme::SecuritySchemeName(security_scheme), - }, - ) - }), - ]) - .prop_map(|security| { - golem_common::model::http_api_deployment::HttpApiDeploymentAgentOptions { - security, - } - }) - .boxed() - } - - fn arb_api_security_scheme_create_result() -> OutputDocumentStrategy { - serialized_output( - arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeCreateView), - ) - } - - fn arb_api_security_scheme_delete_result() -> OutputDocumentStrategy { - serialized_output( - arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeDeleteView), - ) - } - - fn arb_api_security_scheme_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeGetView), - ) - } - - fn arb_api_security_scheme_update_result() -> OutputDocumentStrategy { - serialized_output( - arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeUpdateView), - ) - } - - fn arb_api_security_scheme_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_security_scheme(), 0..5).prop_map(|security_schemes| { - crate::model::text::http_api_security::HttpSecuritySchemeListView { - security_schemes, - } - }), - ) - } - - fn arb_security_scheme() -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - arb_small_string(), - arb_uuid(), - arb_security_scheme_provider(), - arb_small_string(), - arb_url_string(), - proptest::collection::vec(arb_small_string(), 0..5), - ) - .prop_map( - |( - id, - revision, - name, - environment_id, - provider_type, - client_id, - redirect_url, - scopes, - )| { - golem_client::model::SecuritySchemeDto { - id: golem_common::model::security_scheme::SecuritySchemeId(id), - revision: - golem_common::model::security_scheme::SecuritySchemeRevision::new( - revision, - ) - .expect("generated revision should be valid"), - name: golem_common::model::security_scheme::SecuritySchemeName(name), - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - provider_type, - client_id, - redirect_url, - scopes, - } - }, - ) - .boxed() - } - - fn arb_security_scheme_provider() - -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::security_scheme::Provider::Google( - golem_common::model::Empty {} - )), - Just(golem_common::model::security_scheme::Provider::Facebook( - golem_common::model::Empty {} - )), - Just(golem_common::model::security_scheme::Provider::Microsoft( - golem_common::model::Empty {} - )), - Just(golem_common::model::security_scheme::Provider::Gitlab( - golem_common::model::Empty {} - )), - (arb_small_string(), arb_url_string()).prop_map(|(name, issuer_url)| { - golem_common::model::security_scheme::Provider::Custom( - golem_common::model::security_scheme::CustomProvider { name, issuer_url }, - ) - }), - ] - .boxed() - } - - fn arb_new_app_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string(), arb_small_string()).prop_map( - |(created, application_name, application_dir)| { - crate::model::text::action_result::NewAppResult { - created, - application_name, - application_dir: PathBuf::from(application_dir), - } - }, - ), - ) - } - - fn arb_template_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_template_description(), 0..5) - .prop_map(|templates| crate::model::text::template::TemplateListView { templates }), - ) - } - - fn arb_template_description() -> BoxedStrategy { - (arb_small_string(), arb_guest_language(), arb_small_string()) - .prop_map( - |(name, language, description)| crate::model::TemplateDescription { - name, - language, - description, - }, - ) - .boxed() - } - - fn arb_guest_language() -> BoxedStrategy { - prop_oneof![ - Just(crate::model::GuestLanguage::TypeScript), - Just(crate::model::GuestLanguage::Rust), - Just(crate::model::GuestLanguage::Scala), - Just(crate::model::GuestLanguage::MoonBit) - ] - .boxed() - } - - fn arb_component_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_component_view().prop_map(crate::model::text::component::ComponentGetView), - ) - } - - fn arb_component_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_component_view(), 0..5).prop_map(|components| { - crate::model::text::component::ComponentListView { components } - }), - ) - } - - fn arb_component_manifest_trace_result() -> OutputDocumentStrategy { - serialized_output( - (arb_small_string(), arb_component_layer_properties()).prop_map( - |(component_name, properties)| { - crate::model::text::component::ComponentManifestTraceView { - component_name: golem_common::model::component::ComponentName( - component_name, - ), - properties, - } - }, - ), - ) - } - - fn arb_component_layer_properties() -> BoxedStrategy - { - ( - ( - arb_component_layer_id(), - arb_component_layer_id(), - proptest::option::of(arb_small_string()), - proptest::option::of(arb_small_string()), - proptest::option::of(arb_small_string()), - arb_vec_merge_mode(), - proptest::collection::vec(arb_manifest_build_command(), 1..3), - arb_map_merge_mode(), - proptest::collection::hash_map( - arb_small_string(), - proptest::collection::vec(arb_manifest_external_command(), 1..3), - 1..3, - ), - ), - ( - arb_vec_merge_mode(), - proptest::collection::vec(arb_small_string(), 1..3), - arb_json_value(1), - arb_json_value(1), - arb_map_merge_mode(), - proptest::collection::hash_map(arb_small_string(), arb_small_string(), 1..3), - arb_vec_merge_mode(), - proptest::collection::vec(arb_manifest_plugin_installation(), 1..3), - arb_vec_merge_mode(), - proptest::collection::vec(arb_manifest_initial_component_file(), 1..3), - ), - ) - .prop_map(|(left, right)| { - use crate::model::cascade::property::Property; - - let ( - layer_id, - second_layer_id, - selection, - component_wasm, - output_wasm, - _build_mode, - build, - _custom_commands_mode, - custom_commands, - ) = left; - let ( - _clean_mode, - clean, - config_first, - config_second, - _env_mode, - env, - _plugins_mode, - plugins, - _files_mode, - files, - ) = right; - - let mut properties = crate::model::app::ComponentLayerProperties::default(); - let selection = selection.as_ref(); - - properties - .component_wasm - .apply_layer(&layer_id, selection, None); - properties - .component_wasm - .apply_layer(&second_layer_id, selection, component_wasm); - properties - .output_wasm - .apply_layer(&second_layer_id, selection, output_wasm); - properties.build.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::vec::VecMergeMode::Append, - build, - ), - ); - properties.clean.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::vec::VecMergeMode::Prepend, - clean, - ), - ); - properties.custom_commands.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::map::MapMergeMode::Upsert, - custom_commands.clone().into_iter().collect(), - ), - ); - properties.custom_commands.apply_layer( - &second_layer_id, - selection, - ( - crate::model::cascade::property::map::MapMergeMode::Replace, - custom_commands.clone().into_iter().collect(), - ), - ); - properties.custom_commands.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::map::MapMergeMode::Remove, - custom_commands.into_iter().collect(), - ), - ); - properties - .config - .apply_layer(&layer_id, selection, Some(config_first)); - properties - .config - .apply_layer(&second_layer_id, selection, Some(config_second)); - properties - .config - .apply_layer(&layer_id, selection, Some(json!("replacement"))); - properties.env.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::map::MapMergeMode::Upsert, - env.clone().into_iter().collect(), - ), - ); - properties.env.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::map::MapMergeMode::Remove, - env.into_iter().collect(), - ), - ); - properties.plugins.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::vec::VecMergeMode::Replace, - plugins, - ), - ); - properties.files.apply_layer( - &layer_id, - selection, - ( - crate::model::cascade::property::vec::VecMergeMode::Append, - files.clone(), - ), - ); - properties.files.apply_layer( - &second_layer_id, - selection, - ( - crate::model::cascade::property::vec::VecMergeMode::Replace, - files, - ), - ); - - properties - }) - .boxed() - } - - fn arb_component_layer_id() -> BoxedStrategy { - prop_oneof![ - arb_small_string().prop_map(crate::model::app::ComponentLayerId::TemplateCommon), - arb_small_string() - .prop_map(crate::model::app::ComponentLayerId::TemplateEnvironmentPresets), - arb_small_string().prop_map(crate::model::app::ComponentLayerId::TemplateCustomPresets), - arb_small_string().prop_map(|name| { - crate::model::app::ComponentLayerId::ComponentCommon( - golem_common::model::component::ComponentName(name), - ) - }), - arb_small_string().prop_map(|name| { - crate::model::app::ComponentLayerId::ComponentEnvironmentPresets( - golem_common::model::component::ComponentName(name), - ) - }), - arb_small_string().prop_map(|name| { - crate::model::app::ComponentLayerId::ComponentCustomPresets( - golem_common::model::component::ComponentName(name), - ) - }), - ] - .boxed() - } - - fn arb_vec_merge_mode() -> BoxedStrategy { - prop_oneof![ - Just(crate::model::cascade::property::vec::VecMergeMode::Append), - Just(crate::model::cascade::property::vec::VecMergeMode::Prepend), - Just(crate::model::cascade::property::vec::VecMergeMode::Replace), - ] - .boxed() - } - - fn arb_map_merge_mode() -> BoxedStrategy { - prop_oneof![ - Just(crate::model::cascade::property::map::MapMergeMode::Upsert), - Just(crate::model::cascade::property::map::MapMergeMode::Replace), - Just(crate::model::cascade::property::map::MapMergeMode::Remove), - ] - .boxed() - } - - fn arb_manifest_build_command() -> BoxedStrategy { - prop_oneof![ - arb_manifest_external_command().prop_map(crate::model::app_raw::BuildCommand::External), - ( - arb_small_string(), - arb_small_string(), - proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), - proptest::option::of(arb_small_string()), - ) - .prop_map(|(generate_quickjs_crate, wit, js_modules, world)| { - crate::model::app_raw::BuildCommand::QuickJSCrate( - crate::model::app_raw::GenerateQuickJSCrate { - generate_quickjs_crate, - wit, - js_modules, - world, - }, - ) - }), - ( - arb_small_string(), - arb_small_string(), - proptest::option::of(arb_small_string()) - ) - .prop_map(|(generate_quickjs_dts, wit, world)| { - crate::model::app_raw::BuildCommand::QuickJSDTS( - crate::model::app_raw::GenerateQuickJSDTS { - generate_quickjs_dts, - wit, - world, - }, - ) - }), - (arb_small_string(), arb_small_string(), arb_small_string()).prop_map( - |(inject_to_prebuilt_quickjs, module, into)| { - crate::model::app_raw::BuildCommand::InjectToPrebuiltQuickJs( - crate::model::app_raw::InjectToPrebuiltQuickJs { - inject_to_prebuilt_quickjs, - module, - into, - }, - ) - } - ), - (arb_small_string(), arb_small_string()).prop_map(|(preinitialize_js, into)| { - crate::model::app_raw::BuildCommand::PreinitializeJs( - crate::model::app_raw::PreinitializeJs { - preinitialize_js, - into, - }, - ) - }), - ] - .boxed() - } - - fn arb_manifest_external_command() -> BoxedStrategy { - ( - arb_small_string(), - proptest::option::of(arb_small_string()), - proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), - proptest::collection::vec(arb_small_string(), 0..3), - proptest::collection::vec(arb_small_string(), 0..3), - proptest::collection::vec(arb_small_string(), 0..3), - proptest::collection::vec(arb_small_string(), 0..3), - ) - .prop_map(|(command, dir, env, rmdirs, mkdirs, sources, targets)| { - crate::model::app_raw::ExternalCommand { - command, - dir, - env: env.into_iter().collect(), - rmdirs, - mkdirs, - sources, - targets, - } - }) - .boxed() - } - - fn arb_manifest_plugin_installation() -> BoxedStrategy - { - ( - proptest::option::of(arb_small_string()), - arb_small_string(), - arb_small_string(), - proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), - ) - .prop_map(|(account, name, version, parameters)| { - crate::model::app_raw::PluginInstallation { - account, - name, - version, - parameters, - } - }) - .boxed() - } - - fn arb_manifest_initial_component_file() - -> BoxedStrategy { - ( - arb_small_string(), - arb_small_string(), - proptest::option::of(arb_agent_file_permissions()), - ) - .prop_map(|(source_path, target_path, permissions)| { - crate::model::app_raw::InitialComponentFile { - source_path, - target_path: golem_common::model::component::CanonicalFilePath::from_abs_str( - &format!("/{target_path}"), - ) - .expect("generated path should be valid"), - permissions, - } - }) - .boxed() - } - - fn arb_component_view() -> BoxedStrategy { - ( - arb_small_string(), - arb_uuid(), - proptest::option::of(arb_small_string()), - arb_small_u64(), - arb_small_u64(), - Just(fixed_datetime()), - arb_uuid(), - proptest::collection::vec(arb_small_string(), 0..5), - proptest::collection::vec(arb_agent_type(), 0..3), - proptest::collection::btree_map( - arb_agent_type_name(), - arb_agent_type_provision_config(), - 0..3, - ), - ) - .prop_map( - |( - component_name, - component_id, - component_version, - component_revision, - component_size, - created_at, - environment_id, - exports, - agent_types, - agent_type_provision_configs, - )| { - crate::model::component::ComponentView { - component_name: golem_common::model::component::ComponentName( - component_name, - ), - component_id: golem_common::model::component::ComponentId(component_id), - component_version, - component_revision, - component_size, - created_at, - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - exports, - agent_types, - agent_type_provision_configs, - } - }, - ) - .boxed() - } - - fn arb_agent_type_provision_config() - -> BoxedStrategy { - ( - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), - proptest::collection::vec(arb_typed_agent_config_entry(), 0..3), - proptest::collection::vec(arb_installed_plugin(), 0..2), - proptest::collection::vec(arb_initial_agent_file(), 0..2), - arb_polymorphic_card(), - ) - .prop_map(|(env, config, plugins, files, initial_permission)| { - golem_common::model::component_metadata::AgentTypeProvisionConfig { - env, - config, - plugins, - files, - initial_permissions: initial_permission, - } - }) - .boxed() - } - - fn arb_typed_agent_config_entry() - -> BoxedStrategy { - ( - proptest::collection::vec(arb_small_string(), 1..3), - arb_small_string(), - ) - .prop_map( - |(path, value)| golem_common::model::worker::TypedAgentConfigEntry { - path, - value: golem_common::schema::TypedSchemaValue::new( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::string(), - ), - golem_common::schema::SchemaValue::String(value), - ), - }, - ) - .boxed() - } - - fn arb_installed_plugin() -> BoxedStrategy { - ( - arb_uuid(), - 0i32..1000, - proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), - arb_uuid(), - arb_small_string(), - arb_small_string(), - proptest::option::of(arb_uuid()), - proptest::option::of(arb_small_u64()), - ) - .prop_map( - |( - environment_plugin_grant_id, - priority, - parameters, - plugin_registration_id, - plugin_name, - plugin_version, - oplog_processor_component_id, - oplog_processor_component_revision, - )| { - golem_common::model::component::InstalledPlugin { - environment_plugin_grant_id: - golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId( - environment_plugin_grant_id, - ), - priority: golem_common::model::component::PluginPriority(priority), - parameters, - plugin_registration_id: - golem_common::model::plugin_registration::PluginRegistrationId( - plugin_registration_id, - ), - plugin_name, - plugin_version, - oplog_processor_component_id: oplog_processor_component_id - .map(golem_common::model::component::ComponentId), - oplog_processor_component_revision: oplog_processor_component_revision.map( - |revision| { - golem_common::model::component::ComponentRevision::new(revision) - .expect("generated revision should be valid") - }, - ), - } - }, - ) - .boxed() - } - - fn arb_initial_agent_file() -> BoxedStrategy { - ( - arb_hash(), - arb_small_string(), - arb_agent_file_permissions(), - arb_small_u64(), - ) - .prop_map(|(content_hash, path, permissions, size)| { - golem_common::model::component::InitialAgentFile { - content_hash: golem_common::model::agent::AgentFileContentHash(content_hash), - path: golem_common::model::component::AgentFilePath::from_abs_str(&format!( - "/{path}" - )) - .expect("generated path should be valid"), - permissions, - size, - } - }) - .boxed() - } - - fn arb_agent_file_permissions() - -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::component::AgentFilePermissions::ReadOnly), - Just(golem_common::model::component::AgentFilePermissions::ReadWrite), - ] - .boxed() - } - - fn arb_polymorphic_card() -> BoxedStrategy { - ( - arb_uuid(), - proptest::collection::vec(arb_uuid(), 1..3), - proptest::bool::ANY, - arb_datetime(), - proptest::option::of(arb_datetime()), - ) - .prop_map( - |(uuid, parent_uuids, system_card, created_at, expires_at)| { - golem_common::model::card::PolymorphicCard { - card_id: CardId(uuid), - parent_ids: parent_uuids.into_iter().map(CardId).collect(), - lower_negative: Vec::new(), - lower_positive: Vec::new(), - upper_negative: Vec::new(), - upper_positive: Vec::new(), - system_card, - created_at, - expires_at, - } - }, - ) - .boxed() - } - - fn arb_deploy_plan_result() -> OutputDocumentStrategy { - any::() - .prop_map(|include_environment_setup| { - let deployment_diff = empty_deployment_diff(); - let environment_setup = include_environment_setup - .then(crate::model::deploy::EnvironmentSetupPlan::default); - to_structured_output_value_masked( - DeployPlanView { - deployment_diff: &deployment_diff, - environment_setup: environment_setup.as_ref(), - }, - MaskingConfig::hide_secrets(), - ) - .expect("generated deploy plan should serialize") - }) - .boxed() - } - - fn arb_deployment_diff_result() -> OutputDocumentStrategy { - arb_deployment_diff() - .prop_map(|diff| { - to_structured_output_value_masked(diff, MaskingConfig::hide_secrets()) - .expect("generated deployment diff should serialize") - }) - .boxed() - } - - fn arb_deployment_diff() -> BoxedStrategy { - ( - arb_small_string(), - arb_small_string(), - arb_small_string(), - arb_hash(), - arb_hash(), - arb_hash(), - arb_hash(), - arb_hash(), - arb_hash(), - ) - .prop_map(|(component_key, http_key, mcp_key, current_component_hash, new_component_hash, current_file_hash, new_file_hash, current_mcp_hash, new_mcp_hash)| { - use golem_common::model::diff::Diffable; - - let mut current = golem_common::model::diff::Deployment::default(); - let mut new = golem_common::model::diff::Deployment::default(); - - let current_component = golem_common::model::diff::Component { - wasm_hash: current_component_hash, - agent_type_provision_configs: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::AgentTypeProvisionConfig { - env: BTreeMap::from_iter([("A".to_string(), "old".to_string())]), - config: BTreeMap::from_iter([( - "path".to_string(), - golem_common::base_model::json::NormalizedJsonValue(json!("old")), - )]), - files_by_path: BTreeMap::from_iter([( - "/config.json".to_string(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::AgentFile { - hash: current_file_hash, - permissions: golem_common::model::component::AgentFilePermissions::ReadOnly, - }, - ), - )]), - plugins_by_grant_id: BTreeMap::new(), - initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { - lower_negative: Vec::new(), - lower_positive: Vec::new(), - upper_negative: Vec::new(), - upper_positive: Vec::new() - } - }, - ), - )]), - }; - - let new_component = golem_common::model::diff::Component { - wasm_hash: new_component_hash, - agent_type_provision_configs: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::AgentTypeProvisionConfig { - env: BTreeMap::from_iter([("A".to_string(), "new".to_string())]), - config: BTreeMap::from_iter([( - "path".to_string(), - golem_common::base_model::json::NormalizedJsonValue(json!("new")), - )]), - files_by_path: BTreeMap::from_iter([( - "/config.json".to_string(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::AgentFile { - hash: new_file_hash, - permissions: golem_common::model::component::AgentFilePermissions::ReadWrite, - }, - ), - )]), - plugins_by_grant_id: BTreeMap::new(), - initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { - lower_negative: Vec::new(), - lower_positive: Vec::new(), - upper_negative: Vec::new(), - upper_positive: Vec::new() - } - }, - ), - )]), - }; - - current.components.insert( - component_key.clone(), - golem_common::model::diff::HashOf::form_value(current_component), - ); - new.components.insert( - component_key, - golem_common::model::diff::HashOf::form_value(new_component), - ); - - new.http_api_deployments.insert( - http_key.clone(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::HttpApiDeployment { - webhooks_prefix: "new-webhooks".to_string(), - openapi_endpoint_prefix: "new-openapi".to_string(), - agents: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::HttpApiDeploymentAgentOptions { - security_scheme: Some("new-scheme".to_string()), - test_session_header: Some("x-test".to_string()), - }, - )]), - }, - ), - ); - current.http_api_deployments.insert( - http_key, - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::HttpApiDeployment { - webhooks_prefix: "old-webhooks".to_string(), - openapi_endpoint_prefix: "old-openapi".to_string(), - agents: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::HttpApiDeploymentAgentOptions { - security_scheme: Some("old-scheme".to_string()), - test_session_header: None, - }, - )]), - }, - ), - ); - - current.mcp_deployments.insert( - mcp_key.clone(), - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::McpDeployment { - agents: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::McpDeploymentAgentOptions { - security_scheme: Some(current_mcp_hash.to_string()), - }, - )]), - }, - ), - ); - new.mcp_deployments.insert( - mcp_key, - golem_common::model::diff::HashOf::form_value( - golem_common::model::diff::McpDeployment { - agents: BTreeMap::from_iter([( - "agent".to_string(), - golem_common::model::diff::McpDeploymentAgentOptions { - security_scheme: Some(new_mcp_hash.to_string()), - }, - )]), - }, - ), - ); - - golem_common::model::diff::Deployment::diff(&new, ¤t) - .expect("generated deployments should diff") - .expect("generated deployments should differ") - }) - .boxed() - } - - fn arb_environment_setup_plan_result() -> OutputDocumentStrategy { - arb_environment_setup_plan() - .prop_map(|output| { - to_structured_output_value(crate::model::text::diff::EnvironmentSetupPlanView( - &output, - )) - .expect("generated environment setup plan should serialize") - }) - .boxed() - } - - fn arb_environment_setup_plan() -> BoxedStrategy { - ( - arb_small_string(), - arb_api_predicate(), - arb_api_retry_policy(), - arb_resource_limit(), - arb_enforcement_action(), - ) - .prop_map(|(name, predicate, policy, limit, enforcement_action)| { - let secret_path = golem_common::model::agent_secret::AgentSecretPath(vec![ - name.clone(), - "token".to_string(), - ]); - let retry_policy_default = - golem_common::model::deployment::DeploymentRetryPolicyDefault { - name: name.clone(), - priority: 1, - predicate: predicate.clone(), - policy: policy.clone(), - }; - let resource_default = golem_common::model::quota::ResourceDefinitionCreation { - name: golem_common::model::quota::ResourceName(name.clone()), - limit: limit.clone(), - enforcement_action, - unit: "request".to_string(), - units: "requests".to_string(), - }; - - crate::model::deploy::EnvironmentSetupPlan { - display: crate::model::deploy::EnvironmentSetupDisplay { - to_be_applied: crate::model::deploy::EnvironmentSetupDetailedSection { - secret_values: BTreeMap::from_iter([( - name.clone(), - crate::model::deploy::EnvironmentSetupSecretValueDisplay { - secret_type: "Str".to_string(), - value: json!("generated-secret"), - }, - )]), - retry_policies: BTreeMap::from_iter([( - name.clone(), - crate::model::deploy::EnvironmentSetupRetryPolicyDisplay { - priority: retry_policy_default.priority, - predicate: serde_json::to_value(&predicate) - .expect("generated predicate should serialize"), - policy: serde_json::to_value(&policy) - .expect("generated policy should serialize"), - }, - )]), - resources: BTreeMap::from_iter([( - name.clone(), - crate::model::deploy::EnvironmentSetupResourceDisplay { - limit: serde_json::to_value(&limit) - .expect("generated limit should serialize"), - enforcement_action: format!("{enforcement_action:?}"), - unit: "request".to_string(), - units: "requests".to_string(), - }, - )]), - }, - skipped_already_exists: - crate::model::deploy::EnvironmentSetupKeysOnlySection { - secret_values: BTreeSet::from_iter([format!("{name}-existing")]), - retry_policies: BTreeSet::from_iter([format!("{name}-retry")]), - resources: BTreeSet::from_iter([format!("{name}-resource")]), - }, - }, - agent_secret_defaults: vec![ - golem_common::model::deployment::DeploymentAgentSecretDefault { - path: secret_path.clone(), - secret_value: json!("generated-secret"), - }, - ], - skipped_existing_agent_secret_defaults: vec![ - golem_common::model::deployment::DeploymentAgentSecretDefault { - path: secret_path, - secret_value: json!("existing-secret"), - }, - ], - retry_policy_defaults: vec![retry_policy_default], - resource_defaults: vec![resource_default], - } - }) - .boxed() - } - - fn arb_deployment_create_result() -> OutputDocumentStrategy { - serialized_output( - ( - arb_small_string(), - arb_small_string(), - arb_current_deployment(), - ) - .prop_map(|(application_name, environment_name, deployment)| { - crate::model::text::deployment::DeploymentNewView { - application_name: golem_common::model::application::ApplicationName( - application_name, - ), - environment_name: golem_common::model::environment::EnvironmentName( - environment_name, - ), - deployment, - } - }), - ) - } - - fn arb_deployment_list_result() -> OutputDocumentStrategy { - serialized_output(proptest::collection::vec(arb_deployment(), 0..5).prop_map( - |deployments| crate::model::text::deployment::DeploymentListView { deployments }, - )) - } - - fn arb_environment_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_environment_with_details(), 0..5).prop_map( - |environments| crate::model::text::environment::EnvironmentListView { - environments, - }, - ), - ) - } - - fn arb_environment_sync_deployment_options_result() -> OutputDocumentStrategy { - serialized_output(any::().prop_map(|updated| { - crate::model::text::environment::EnvironmentSyncDeploymentOptionsResult { updated } - })) - } - - fn arb_environment_with_details() - -> BoxedStrategy { - ( - arb_environment_summary(), - arb_application_summary(), - arb_account_summary(), - ) - .prop_map(|(environment, application, account)| { - golem_common::model::environment::EnvironmentWithDetails { - environment, - application, - account, - } - }) - .boxed() - } - - fn arb_environment_summary() - -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - Just("generated-env".to_string()), - any::(), - any::(), - any::(), - any::(), - proptest::option::of(arb_environment_current_deployment()), - ) - .prop_map( - |( - id, - revision, - name, - diff_model_version, - compatibility_check, - version_check, - security_overrides, - current_deployment, - )| { - golem_common::model::environment::EnvironmentSummary { - id: golem_common::model::environment::EnvironmentId(id), - revision: golem_common::model::environment::EnvironmentRevision::new( - revision, - ) - .expect("generated revision should be valid"), - name: golem_common::model::environment::EnvironmentName(name), - diff_model_version, - compatibility_check, - version_check, - security_overrides, - current_deployment, - } - }, - ) - .boxed() - } - - fn arb_environment_current_deployment() - -> BoxedStrategy { - ( - arb_small_u64(), - arb_small_u64(), - arb_small_string(), - arb_hash(), - ) - .prop_map( - |(revision, deployment_revision, deployment_version, deployment_hash)| { - golem_common::model::environment::EnvironmentCurrentDeploymentView { - revision: golem_common::model::deployment::CurrentDeploymentRevision::new( - revision, - ) - .expect("generated revision should be valid"), - deployment_revision: - golem_common::model::deployment::DeploymentRevision::new( - deployment_revision, - ) - .expect("generated revision should be valid"), - deployment_version: golem_common::model::deployment::DeploymentVersion( - deployment_version, - ), - deployment_hash, - } - }, - ) - .boxed() - } - - fn arb_application_summary() - -> BoxedStrategy { - (arb_uuid(), arb_small_string()) - .prop_map( - |(id, name)| golem_common::model::application::ApplicationSummary { - id: golem_common::model::application::ApplicationId(id), - name: golem_common::model::application::ApplicationName(name), - }, - ) - .boxed() - } - - fn arb_account_summary() -> BoxedStrategy { - (arb_uuid(), arb_small_string(), arb_small_string()) - .prop_map( - |(id, name, email)| golem_common::model::account::AccountSummary { - id: golem_common::model::account::AccountId(id), - name, - email: golem_common::model::account::AccountEmail::new(email), - }, - ) - .boxed() - } - - fn arb_deployment() -> BoxedStrategy { - (arb_uuid(), arb_small_u64(), arb_small_string(), arb_hash()) - .prop_map(|(environment_id, revision, version, deployment_hash)| { - golem_common::model::deployment::Deployment { - environment_id: golem_common::model::environment::EnvironmentId(environment_id), - revision: golem_common::model::deployment::DeploymentRevision::new(revision) - .expect("generated revision should be valid"), - version: golem_common::model::deployment::DeploymentVersion(version), - deployment_hash, - } - }) - .boxed() - } - - fn arb_current_deployment() -> BoxedStrategy - { - ( - arb_uuid(), - arb_small_u64(), - arb_small_string(), - arb_hash(), - arb_small_u64(), - ) - .prop_map( - |(environment_id, revision, version, deployment_hash, current_revision)| { - golem_common::model::deployment::CurrentDeployment { - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - revision: golem_common::model::deployment::DeploymentRevision::new( - revision, - ) - .expect("generated revision should be valid"), - version: golem_common::model::deployment::DeploymentVersion(version), - deployment_hash, - current_revision: - golem_common::model::deployment::CurrentDeploymentRevision::new( - current_revision, - ) - .expect("generated revision should be valid"), - validation_warnings: Vec::new(), - } - }, - ) - .boxed() - } - - fn arb_plugin_unregister_result() -> OutputDocumentStrategy { - serialized_output( - ( - any::(), - arb_uuid(), - arb_small_string(), - arb_small_string(), - ) - .prop_map(|(unregistered, plugin_id, name, version)| { - crate::model::text::plugin::PluginUnregisterResult { - unregistered, - plugin_id, - name, - version, - } - }), - ) - } - - fn arb_plugin_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_plugin_registration() - .prop_map(crate::model::text::plugin::PluginRegistrationGetView), - ) - } - - fn arb_plugin_register_result() -> OutputDocumentStrategy { - serialized_output( - arb_plugin_registration() - .prop_map(crate::model::text::plugin::PluginRegistrationRegisterView), - ) - } - - fn arb_plugin_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_plugin_list_entry(), 0..5) - .prop_map(|plugins| crate::model::text::plugin::PluginListView { plugins }), - ) - } - - fn arb_plugin_list_entry() -> BoxedStrategy { - (arb_plugin_registration(), arb_plugin_source()) - .prop_map( - |(plugin, source)| crate::model::text::plugin::PluginListEntry { plugin, source }, - ) - .boxed() - } - - fn arb_plugin_source() -> BoxedStrategy { - prop_oneof![ - Just(crate::model::text::plugin::PluginSource::Own), - Just(crate::model::text::plugin::PluginSource::Builtin), - Just(crate::model::text::plugin::PluginSource::Shared), - ] - .boxed() - } - - fn arb_plugin_registration() - -> BoxedStrategy { - ( - arb_uuid(), - arb_uuid(), - arb_small_string(), - arb_small_string(), - arb_small_string(), - Just(golem_common::model::base64::Base64(vec![0])), - arb_small_string(), - arb_uuid(), - arb_small_u64(), - ) - .prop_map(|(id, account_id, name, version, description, icon, homepage, component_id, component_revision)| { - golem_common::model::plugin_registration::PluginRegistrationDto { - id: golem_common::model::plugin_registration::PluginRegistrationId(id), - account_id: golem_common::model::account::AccountId(account_id), - name, - version, - description, - icon, - homepage, - spec: golem_common::model::plugin_registration::PluginSpecDto::OplogProcessor( - golem_common::model::plugin_registration::OplogProcessorPluginSpec { - component_id: golem_common::model::component::ComponentId(component_id), - component_revision: golem_common::model::component::ComponentRevision::new(component_revision) - .expect("generated revision should be valid"), - }, - ), - } - }) - .boxed() - } - - fn arb_profile_create_result() -> OutputDocumentStrategy { - serialized_output((any::(), arb_small_string(), any::()).prop_map( - |(created, profile, set_active)| crate::model::text::profile::ProfileCreateResult { - created, - profile: crate::config::ProfileName(profile), - set_active, - }, - )) - } - - fn arb_profile_get_result() -> OutputDocumentStrategy { - serialized_output(arb_profile_view()) - } - - fn arb_profile_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_profile_view(), 0..5) - .prop_map(|profiles| crate::model::text::profile::ProfileListView { profiles }), - ) - } - - fn arb_profile_view() -> BoxedStrategy { - ( - any::(), - arb_small_string(), - proptest::option::of(arb_url_string()), - proptest::option::of(arb_url_string()), - any::(), - proptest::option::of(any::()), - arb_format_string(), - ) - .prop_map( - |( - is_active, - name, - url, - worker_url, - allow_insecure, - authenticated, - default_format, - )| { - crate::model::ProfileView { - is_active, - name: crate::config::ProfileName(name), - url: url.map(|url| url.parse().expect("generated URL should parse")), - worker_url: worker_url - .map(|url| url.parse().expect("generated URL should parse")), - allow_insecure, - authenticated, - config: crate::config::ProfileConfig { - default_format: default_format - .parse() - .expect("generated format should parse"), - }, - } - }, - ) - .boxed() - } - - fn arb_profile_switch_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(switched, profile)| { - crate::model::text::profile::ProfileSwitchResult { - switched, - profile: crate::config::ProfileName(profile), - } - }), - ) - } - - fn arb_profile_delete_result() -> OutputDocumentStrategy { - serialized_output( - (any::(), arb_small_string()).prop_map(|(deleted, profile)| { - crate::model::text::profile::ProfileDeleteResult { - deleted, - profile: crate::config::ProfileName(profile), - } - }), - ) - } - - fn arb_profile_config_set_format_result() -> OutputDocumentStrategy { - serialized_output((any::(), arb_small_string(), arb_format()).prop_map( - |(updated, profile, format)| { - crate::model::text::profile::ProfileConfigSetFormatResult { - updated, - profile: crate::config::ProfileName(profile), - format, - } - }, - )) - } - - fn arb_resource_create_result() -> OutputDocumentStrategy { - serialized_output( - arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionCreateView), - ) - } - - fn arb_resource_delete_result() -> OutputDocumentStrategy { - serialized_output( - arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionDeleteView), - ) - } - - fn arb_resource_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionGetView), - ) - } - - fn arb_resource_update_result() -> OutputDocumentStrategy { - serialized_output( - arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionUpdateView), - ) - } - - fn arb_resource_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_resource_definition(), 0..5).prop_map(|resources| { - crate::model::text::resource_definition::ResourceDefinitionListView { resources } - }), - ) - } - - fn arb_resource_definition() -> BoxedStrategy { - ( - arb_uuid(), - arb_small_u64(), - arb_uuid(), - arb_small_string(), - arb_resource_limit(), - arb_enforcement_action(), - arb_small_string(), - arb_small_string(), - ) - .prop_map( - |(id, revision, environment_id, name, limit, enforcement_action, unit, units)| { - golem_common::model::quota::ResourceDefinition { - id: golem_common::model::quota::ResourceDefinitionId(id), - revision: golem_common::model::quota::ResourceDefinitionRevision::new( - revision, - ) - .expect("generated revision should be valid"), - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - name: golem_common::model::quota::ResourceName(name), - limit, - enforcement_action, - unit, - units, - } - }, - ) - .boxed() - } - - fn arb_resource_limit() -> BoxedStrategy { - prop_oneof![ - (arb_small_u64(), arb_time_period(), arb_small_u64()).prop_map( - |(value, period, max)| { - golem_common::model::quota::ResourceLimit::Rate( - golem_common::model::quota::ResourceRateLimit { value, period, max }, - ) - } - ), - arb_small_u64().prop_map(|value| { - golem_common::model::quota::ResourceLimit::Capacity( - golem_common::model::quota::ResourceCapacityLimit { value }, - ) - }), - arb_small_u64().prop_map(|value| { - golem_common::model::quota::ResourceLimit::Concurrency( - golem_common::model::quota::ResourceConcurrencyLimit { value }, - ) - }), - ] - .boxed() - } - - fn arb_enforcement_action() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::quota::EnforcementAction::Reject), - Just(golem_common::model::quota::EnforcementAction::Throttle), - Just(golem_common::model::quota::EnforcementAction::Terminate), - ] - .boxed() - } - - fn arb_time_period() -> BoxedStrategy { - prop_oneof![ - Just(golem_common::model::quota::TimePeriod::Second), - Just(golem_common::model::quota::TimePeriod::Minute), - Just(golem_common::model::quota::TimePeriod::Hour), - Just(golem_common::model::quota::TimePeriod::Day), - Just(golem_common::model::quota::TimePeriod::Month), - Just(golem_common::model::quota::TimePeriod::Year) - ] - .boxed() - } - - fn arb_api_predicate_value() - -> BoxedStrategy { - prop_oneof![ - arb_small_string().prop_map(|value| { - golem_common::base_model::retry_policy::ApiPredicateValue::Text( - golem_common::base_model::retry_policy::ApiTextValue { value }, - ) - }), - any::().prop_map(|value| { - golem_common::base_model::retry_policy::ApiPredicateValue::Integer( - golem_common::base_model::retry_policy::ApiIntegerValue { value }, - ) - }), - any::().prop_map(|value| { - golem_common::base_model::retry_policy::ApiPredicateValue::Boolean( - golem_common::base_model::retry_policy::ApiBooleanValue { value }, - ) - }), - ] - .boxed() - } - - fn arb_api_predicate() -> BoxedStrategy { - arb_api_predicate_with_depth(2) - } - - fn arb_api_predicate_with_depth( - depth: u32, - ) -> BoxedStrategy { - let leaf = prop_oneof![ - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropEq( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropNeq( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropGt( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropGte( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropLt( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { - golem_common::base_model::retry_policy::ApiPredicate::PropLte( - golem_common::base_model::retry_policy::ApiPropertyComparison { - property, - value, - }, - ) - }), - arb_small_string().prop_map(|property| { - golem_common::base_model::retry_policy::ApiPredicate::PropExists( - golem_common::base_model::retry_policy::ApiPropertyExistence { property }, - ) - }), - ( - arb_small_string(), - proptest::collection::vec(arb_api_predicate_value(), 0..4), - ) - .prop_map(|(property, values)| { - golem_common::base_model::retry_policy::ApiPredicate::PropIn( - golem_common::base_model::retry_policy::ApiPropertySetCheck { - property, - values, - }, - ) - }), - (arb_small_string(), arb_small_string()).prop_map(|(property, pattern)| { - golem_common::base_model::retry_policy::ApiPredicate::PropMatches( - golem_common::base_model::retry_policy::ApiPropertyPattern { - property, - pattern, - }, - ) - }), - (arb_small_string(), arb_small_string()).prop_map(|(property, prefix)| { - golem_common::base_model::retry_policy::ApiPredicate::PropStartsWith( - golem_common::base_model::retry_policy::ApiPropertyPrefix { property, prefix }, - ) - }), - (arb_small_string(), arb_small_string()).prop_map(|(property, substring)| { - golem_common::base_model::retry_policy::ApiPredicate::PropContains( - golem_common::base_model::retry_policy::ApiPropertySubstring { - property, - substring, - }, - ) - }), - Just(golem_common::base_model::retry_policy::ApiPredicate::True( - golem_common::base_model::retry_policy::ApiPredicateTrue {}, - )), - Just(golem_common::base_model::retry_policy::ApiPredicate::False( - golem_common::base_model::retry_policy::ApiPredicateFalse {}, - )), - ] - .boxed(); - - if depth == 0 { - return leaf; - } - - let inner = arb_api_predicate_with_depth(depth - 1); - prop_oneof![ - leaf, - (inner.clone(), inner.clone()).prop_map(|(left, right)| { - golem_common::base_model::retry_policy::ApiPredicate::And( - golem_common::base_model::retry_policy::ApiPredicatePair { - left: Box::new(left), - right: Box::new(right), - }, - ) - }), - (inner.clone(), inner.clone()).prop_map(|(left, right)| { - golem_common::base_model::retry_policy::ApiPredicate::Or( - golem_common::base_model::retry_policy::ApiPredicatePair { - left: Box::new(left), - right: Box::new(right), - }, - ) - }), - inner.prop_map(|predicate| { - golem_common::base_model::retry_policy::ApiPredicate::Not( - golem_common::base_model::retry_policy::ApiPredicateNot { - predicate: Box::new(predicate), - }, - ) - }), - ] - .boxed() - } - - fn arb_api_retry_policy() - -> BoxedStrategy { - arb_api_retry_policy_with_depth(2) - } - - fn arb_api_retry_policy_with_depth( - depth: u32, - ) -> BoxedStrategy { - let leaf = prop_oneof![ - arb_small_u64().prop_map(|delay_ms| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Periodic( - golem_common::base_model::retry_policy::ApiPeriodicPolicy { delay_ms }, - ) - }), - (arb_small_u64(), 0.0f64..10.0).prop_map(|(base_delay_ms, factor)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Exponential( - golem_common::base_model::retry_policy::ApiExponentialPolicy { - base_delay_ms, - factor, - }, - ) - }), - (arb_small_u64(), arb_small_u64()).prop_map(|(first_ms, second_ms)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Fibonacci( - golem_common::base_model::retry_policy::ApiFibonacciPolicy { - first_ms, - second_ms, - }, - ) - }), - Just( - golem_common::base_model::retry_policy::ApiRetryPolicy::Immediate( - golem_common::base_model::retry_policy::ApiImmediatePolicy {}, - ) - ), - Just( - golem_common::base_model::retry_policy::ApiRetryPolicy::Never( - golem_common::base_model::retry_policy::ApiNeverPolicy {}, - ) - ), - ] - .boxed(); - - if depth == 0 { - return leaf; - } - - let inner = arb_api_retry_policy_with_depth(depth - 1); - prop_oneof![ - leaf, - (any::(), inner.clone()).prop_map(|(max_retries, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::CountBox( - golem_common::base_model::retry_policy::ApiCountBoxPolicy { - max_retries, - inner: Box::new(inner), - }, - ) - }), - (arb_small_u64(), inner.clone()).prop_map(|(limit_ms, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::TimeBox( - golem_common::base_model::retry_policy::ApiTimeBoxPolicy { - limit_ms, - inner: Box::new(inner), - }, - ) - }), - (arb_small_u64(), arb_small_u64(), inner.clone()).prop_map( - |(min_delay_ms, max_delay_ms, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Clamp( - golem_common::base_model::retry_policy::ApiClampPolicy { - min_delay_ms, - max_delay_ms, - inner: Box::new(inner), - }, - ) - }, - ), - (arb_small_u64(), inner.clone()).prop_map(|(delay_ms, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::AddDelay( - golem_common::base_model::retry_policy::ApiAddDelayPolicy { - delay_ms, - inner: Box::new(inner), - }, - ) - }), - (0.0f64..1.0, inner.clone()).prop_map(|(factor, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Jitter( - golem_common::base_model::retry_policy::ApiJitterPolicy { - factor, - inner: Box::new(inner), - }, - ) - }), - (arb_api_predicate(), inner.clone()).prop_map(|(predicate, inner)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::FilteredOn( - golem_common::base_model::retry_policy::ApiFilteredOnPolicy { - predicate, - inner: Box::new(inner), - }, - ) - }), - (inner.clone(), inner.clone()).prop_map(|(first, second)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::AndThen( - golem_common::base_model::retry_policy::ApiRetryPolicyPair { - first: Box::new(first), - second: Box::new(second), - }, - ) - },), - (inner.clone(), inner.clone()).prop_map(|(first, second)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Union( - golem_common::base_model::retry_policy::ApiRetryPolicyPair { - first: Box::new(first), - second: Box::new(second), - }, - ) - }), - (inner.clone(), inner.clone()).prop_map(|(first, second)| { - golem_common::base_model::retry_policy::ApiRetryPolicy::Intersect( - golem_common::base_model::retry_policy::ApiRetryPolicyPair { - first: Box::new(first), - second: Box::new(second), - }, - ) - }), - ] - .boxed() - } - - fn arb_retry_policy_create_result() -> OutputDocumentStrategy { - serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyCreateView), - ) - } - - fn arb_retry_policy_delete_result() -> OutputDocumentStrategy { - serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyDeleteView), - ) - } - - fn arb_retry_policy_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyGetView), - ) - } - - fn arb_retry_policy_update_result() -> OutputDocumentStrategy { - serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyUpdateView), - ) - } - - fn arb_retry_policy_list_result() -> OutputDocumentStrategy { - serialized_output( - proptest::collection::vec(arb_retry_policy(), 0..5).prop_map(|retry_policies| { - crate::model::text::retry_policy::RetryPolicyListView { retry_policies } - }), - ) - } - - fn arb_retry_policy() -> BoxedStrategy { - ( - arb_uuid(), - arb_uuid(), - arb_small_string(), - arb_small_u64(), - any::(), - arb_api_predicate(), - arb_api_retry_policy(), - ) - .prop_map( - |(id, environment_id, name, revision, priority, predicate, policy)| { - golem_common::model::retry_policy::RetryPolicyDto { - id: golem_common::model::retry_policy::RetryPolicyId(id), - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - name, - revision: golem_common::model::retry_policy::RetryPolicyRevision::new( - revision, - ) - .expect("generated revision should be valid"), - priority, - predicate: golem_common::model::UntypedJsonBody( - serde_json::to_value(predicate) - .expect("generated predicate should serialize"), - ), - policy: golem_common::model::UntypedJsonBody( - serde_json::to_value(policy) - .expect("generated retry policy should serialize"), - ), - } - }, - ) - .boxed() - } - - fn arb_secret_create_result() -> OutputDocumentStrategy { - arb_secret() - .prop_map(|secret| { - to_structured_output_value_masked( - crate::model::text::secret::SecretCreateView(secret.into()), - MaskingConfig::hide_secrets(), - ) - .expect("generated secret create should serialize") - }) - .boxed() - } - - fn arb_secret_delete_result() -> OutputDocumentStrategy { - arb_secret() - .prop_map(|secret| { - to_structured_output_value_masked( - crate::model::text::secret::SecretDeleteView(secret.into()), - MaskingConfig::hide_secrets(), - ) - .expect("generated secret delete should serialize") - }) - .boxed() - } - - fn arb_secret_get_result() -> OutputDocumentStrategy { - arb_secret() - .prop_map(|secret| { - to_structured_output_value_masked( - crate::model::text::secret::SecretGetView(secret.into()), - MaskingConfig::hide_secrets(), - ) - .expect("generated secret get should serialize") - }) - .boxed() - } - - fn arb_secret_update_value_result() -> OutputDocumentStrategy { - arb_secret() - .prop_map(|secret| { - to_structured_output_value_masked( - crate::model::text::secret::SecretUpdateView(secret.into()), - MaskingConfig::hide_secrets(), - ) - .expect("generated secret update should serialize") - }) - .boxed() - } - - fn arb_secret_list_result() -> OutputDocumentStrategy { - proptest::collection::vec(arb_secret(), 0..5) - .prop_map(|secrets| { - to_structured_output_value_masked( - crate::model::text::secret::SecretListView { - secrets: secrets.into_iter().map(Into::into).collect(), - environment_name: "generated-environment".to_string(), - show_ids: false, - }, - MaskingConfig::hide_secrets(), - ) - .expect("generated secret list should serialize") - }) - .boxed() - } - - fn arb_secret() -> BoxedStrategy { - ( - arb_uuid(), - arb_uuid(), - proptest::collection::vec(arb_small_string(), 1..4), - arb_small_u64(), - arb_secret_type_and_value(), - ) - .prop_map( - |(id, environment_id, path, revision, (secret_type, secret_value))| { - golem_client::model::AgentSecretDto { - id: golem_common::model::agent_secret::AgentSecretId(id), - environment_id: golem_common::model::environment::EnvironmentId( - environment_id, - ), - path: golem_common::model::agent_secret::CanonicalAgentSecretPath(path), - revision: golem_common::model::agent_secret::AgentSecretRevision::new( - revision, - ) - .expect("generated revision should be valid"), - secret_type, - secret_value, - } - }, - ) - .boxed() - } - - fn arb_secret_type_and_value() -> BoxedStrategy<( - golem_common::schema::SchemaGraph, - Option, - )> { - prop_oneof![ - proptest::option::of( - arb_small_string().prop_map(golem_common::schema::SchemaValue::String) - ) - .prop_map(|value| { - ( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::string(), - ), - value, - ) - }), - proptest::option::of(any::().prop_map(golem_common::schema::SchemaValue::Bool)) - .prop_map(|value| { - ( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::bool(), - ), - value, - ) - }), - proptest::option::of(arb_small_u64().prop_map(golem_common::schema::SchemaValue::U64)) - .prop_map(|value| { - ( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::u64(), - ), - value, - ) - }), - proptest::option::of(proptest::collection::vec(arb_small_u64(), 0..3).prop_map( - |values| { - golem_common::schema::SchemaValue::List { - elements: values - .into_iter() - .map(golem_common::schema::SchemaValue::U64) - .collect(), - } - } - )) - .prop_map(|value| { - ( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::list( - golem_common::schema::SchemaType::u64(), - ), - ), - value, - ) - }), - proptest::option::of(proptest::option::of(arb_small_string()).prop_map(|value| { - golem_common::schema::SchemaValue::Option { - inner: value - .map(|value| Box::new(golem_common::schema::SchemaValue::String(value))), - } - })) - .prop_map(|value| { - ( - golem_common::schema::SchemaGraph::anonymous( - golem_common::schema::SchemaType::option( - golem_common::schema::SchemaType::string(), - ), - ), - value, - ) - }), - ] - .boxed() - } - - fn arb_json_value(depth: u32) -> OutputDocumentStrategy { - let leaf = prop_oneof![ - Just(Value::Null), - any::().prop_map(Value::Bool), - any::().prop_map(|value| json!(value)), - arb_small_string().prop_map(Value::String), - ]; - - if depth == 0 { - return leaf.boxed(); - } - - let inner = arb_json_value(depth - 1); - prop_oneof![ - leaf, - proptest::collection::vec(inner.clone(), 0..4).prop_map(Value::Array), - proptest::collection::btree_map(arb_small_string(), inner, 0..4) - .prop_map(|map| Value::Object(map.into_iter().collect())), - ] - .boxed() - } - - fn arb_format_string() -> BoxedStrategy<&'static str> { - prop_oneof![ - Just("json"), - Just("pretty-json"), - Just("yaml"), - Just("pretty-yaml"), - Just("text"), - Just("toon") - ] - .boxed() - } - - fn arb_format() -> BoxedStrategy { - prop_oneof![ - Just(crate::model::format::Format::Json), - Just(crate::model::format::Format::PrettyJson), - Just(crate::model::format::Format::Yaml), - Just(crate::model::format::Format::PrettyYaml), - Just(crate::model::format::Format::Text), - Just(crate::model::format::Format::Toon), - ] - .boxed() - } -} diff --git a/cli/golem-cli/src/model/cli_output/mod.rs b/cli/golem-cli/src/model/cli_output/mod.rs new file mode 100644 index 0000000000..39a2d6effa --- /dev/null +++ b/cli/golem-cli/src/model/cli_output/mod.rs @@ -0,0 +1,251 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::model::masking::MaskingConfig; +use anyhow::{anyhow, bail}; +use serde::Serialize; +use serde::Serializer; +use serde_json::{Map, Value}; +use std::collections::{BTreeSet, VecDeque}; + +pub const CLI_OUTPUT_TYPE_FIELD: &str = "$type"; +const CLI_OUTPUT_TYPES_FIELD: &str = "x-golem-cli-output-types"; +pub const COMMAND_OUTPUT_SCHEMA_JSON: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/command-output-schema/command-output.schema.json" +)); + +pub trait StructuredOutput: Serialize { + const KIND: &'static str; + + fn type_name() -> String { + Self::KIND.to_string() + } + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + Self: Sized, + { + let _ = config; + self.serialize(serializer) + } +} + +pub fn command_output_schema_value() -> anyhow::Result { + serde_json::from_str(COMMAND_OUTPUT_SCHEMA_JSON) + .map_err(|err| anyhow!("Embedded command output schema must parse: {err}")) +} + +pub fn command_output_type_names() -> anyhow::Result { + let schema = command_output_schema_value()?; + let entries = schema_output_type_entries(&schema)?; + let names = entries + .iter() + .filter_map(|entry| entry.get("type")) + .filter_map(Value::as_str) + .map(|name| Value::String(name.to_string())) + .collect::>(); + Ok(Value::Array(names)) +} + +pub fn focused_command_output_schema(output_types: &[String]) -> anyhow::Result { + if output_types.is_empty() { + bail!("At least one output type must be specified"); + } + + let schema = command_output_schema_value()?; + let definitions = schema + .get("definitions") + .and_then(Value::as_object) + .ok_or_else(|| anyhow!("Command output schema is missing definitions"))?; + let output_type_entries = schema_output_type_entries(&schema)?; + let known_output_types = output_type_entries + .iter() + .filter_map(|entry| entry.get("type")) + .filter_map(Value::as_str) + .collect::>(); + + let mut selected = BTreeSet::::new(); + let mut reachable = BTreeSet::::new(); + let mut queue = VecDeque::::new(); + for output_type in output_types { + if !known_output_types.contains(output_type.as_str()) { + bail!( + "Unknown output type: {output_type}; run `golem output-schema --types` to list known output types" + ); + } + if !definitions.contains_key(output_type) { + bail!("Command output schema is missing definition {output_type}"); + } + if selected.insert(output_type.clone()) && reachable.insert(output_type.clone()) { + queue.push_back(output_type.clone()); + } + } + + while let Some(name) = queue.pop_front() { + let definition = definitions + .get(&name) + .ok_or_else(|| anyhow!("Command output schema is missing definition {name}"))?; + let mut refs = BTreeSet::new(); + collect_definition_refs(definition, &mut refs); + for reference in refs { + if !definitions.contains_key(&reference) { + bail!("Command output schema references missing definition {reference}"); + } + if reachable.insert(reference.clone()) { + queue.push_back(reference); + } + } + } + + let mut focused = Map::new(); + if let Some(value) = schema.get("$schema") { + focused.insert("$schema".to_string(), value.clone()); + } + if let Some(value) = schema.get("title") { + focused.insert("title".to_string(), value.clone()); + } + focused.insert( + "description".to_string(), + Value::String( + "Focused structured output schema for selected Golem CLI output types.".to_string(), + ), + ); + focused.insert( + "oneOf".to_string(), + Value::Array( + selected + .iter() + .map(|output_type| json_ref(output_type)) + .collect(), + ), + ); + + let mut pruned_definitions = Map::new(); + for name in &reachable { + pruned_definitions.insert( + name.clone(), + definitions + .get(name) + .ok_or_else(|| anyhow!("Command output schema is missing definition {name}"))? + .clone(), + ); + } + focused.insert("definitions".to_string(), Value::Object(pruned_definitions)); + + focused.insert( + CLI_OUTPUT_TYPES_FIELD.to_string(), + Value::Array( + output_type_entries + .iter() + .filter(|entry| { + entry + .get("type") + .and_then(Value::as_str) + .is_some_and(|output_type| selected.contains(output_type)) + }) + .cloned() + .collect(), + ), + ); + + Ok(Value::Object(focused)) +} + +fn schema_output_type_entries(schema: &Value) -> anyhow::Result<&Vec> { + schema + .get(CLI_OUTPUT_TYPES_FIELD) + .and_then(Value::as_array) + .ok_or_else(|| anyhow!("Command output schema is missing {CLI_OUTPUT_TYPES_FIELD}")) +} + +fn collect_definition_refs(value: &Value, refs: &mut BTreeSet) { + match value { + Value::Object(object) => { + if let Some(reference) = object.get("$ref").and_then(Value::as_str) + && let Some(name) = reference.strip_prefix("#/definitions/") + { + refs.insert(name.to_string()); + } + for value in object.values() { + collect_definition_refs(value, refs); + } + } + Value::Array(values) => { + for value in values { + collect_definition_refs(value, refs); + } + } + _ => {} + } +} + +fn json_ref(definition_name: &str) -> Value { + let mut reference = Map::new(); + reference.insert( + "$ref".to_string(), + Value::String(format!("#/definitions/{definition_name}")), + ); + Value::Object(reference) +} + +pub fn to_structured_output_value( + output: Output, +) -> anyhow::Result { + to_structured_output_value_masked(output, MaskingConfig::hide_secrets()) +} + +pub fn to_structured_output_value_masked( + output: Output, + config: MaskingConfig, +) -> anyhow::Result { + let value = output.serialize_masked(serde_json::value::Serializer, config)?; + let type_value = Value::String(Output::type_name()); + + match value { + Value::Object(fields) => Ok(Value::Object(with_structured_output_type::( + fields, type_value, + )?)), + value => { + let mut fields = Map::new(); + fields.insert(CLI_OUTPUT_TYPE_FIELD.to_string(), type_value); + fields.insert("value".to_string(), value); + Ok(Value::Object(fields)) + } + } +} + +fn with_structured_output_type( + fields: Map, + type_value: Value, +) -> anyhow::Result> { + let mut result = Map::new(); + result.insert(CLI_OUTPUT_TYPE_FIELD.to_string(), type_value); + + for (key, value) in fields { + if key == CLI_OUTPUT_TYPE_FIELD { + bail!( + "CLI output model {} must not define reserved field {CLI_OUTPUT_TYPE_FIELD}", + Output::KIND, + ); + } + result.insert(key, value); + } + + Ok(result) +} + +#[cfg(test)] +mod tests; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs new file mode 100644 index 0000000000..0c8c63bf57 --- /dev/null +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -0,0 +1,5619 @@ +use crate::model::cli_output::{ + CLI_OUTPUT_TYPE_FIELD, StructuredOutput, command_output_type_names, + focused_command_output_schema, to_structured_output_value, to_structured_output_value_masked, +}; +use crate::model::masking::MaskingConfig; +use crate::model::text::diff::DeployPlanView; +use golem_common::model::card::{CardId, PolymorphicCard}; +use proptest::prelude::*; +use quote::ToTokens; +use serde_json::{Value, json}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use syn::{Expr, ImplItem, Item, ItemImpl, Lit, Type}; +use test_r::test; +use uuid::uuid; +use walkdir::WalkDir; + +type OutputDocumentStrategy = BoxedStrategy; + +struct StructuredOutputTestEntry { + rust_type: &'static str, + output_type: &'static str, + examples: fn() -> Vec, + arbitrary: fn() -> OutputDocumentStrategy, +} + +macro_rules! registry_entry { + ($rust_type:literal, $output_type:literal, $arbitrary:expr) => { + StructuredOutputTestEntry { + rust_type: $rust_type, + output_type: $output_type, + examples: || { + let mut runner = proptest::test_runner::TestRunner::deterministic(); + vec![ + ($arbitrary)() + .new_tree(&mut runner) + .expect("example strategy should produce a value") + .current(), + ] + }, + arbitrary: $arbitrary, + } + }; +} + +static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ + registry_entry!( + "AccountDeleteResult", + "account.delete", + arb_account_delete_result + ), + registry_entry!("AccountGetView", "account.get", arb_account_get_result), + registry_entry!("AccountNewView", "account.new", arb_account_new_result), + registry_entry!( + "PermissionShareDeleteResult", + "account.permission-share.delete", + arb_permission_share_delete_result + ), + registry_entry!( + "PermissionShareGetView", + "account.permission-share.get", + arb_permission_share_get_result + ), + registry_entry!( + "PermissionShareListView", + "account.permission-share.list", + arb_permission_share_list_result + ), + registry_entry!( + "PermissionShareNewView", + "account.permission-share.new", + arb_permission_share_new_result + ), + registry_entry!( + "PermissionShareUpdateView", + "account.permission-share.update", + arb_permission_share_update_result + ), + registry_entry!( + "AccountUpdateView", + "account.update", + arb_account_update_result + ), + registry_entry!("CardGetView", "card.get", arb_card_get_result), + registry_entry!("CardListView", "card.list", arb_card_list_result), + registry_entry!("CardRevokeResult", "card.revoke", arb_card_revoke_result), + registry_entry!("AgentTypeView", "agent-type.get", arb_agent_type_get_result), + registry_entry!( + "AgentTypeListView", + "agent-type.list", + arb_agent_type_list_result + ), + registry_entry!( + "AgentCancelInvocationResult", + "agent.cancel-invocation", + arb_agent_cancel_invocation_result + ), + registry_entry!("AgentDeleteResult", "agent.delete", arb_agent_delete_result), + registry_entry!( + "AgentDeleteAllResult", + "agent.delete-all", + arb_agent_delete_all_result + ), + registry_entry!( + "AgentFileContentsResult", + "agent.file-contents", + arb_agent_file_contents_result + ), + registry_entry!("AgentFilesView", "agent.files", arb_agent_files_result), + registry_entry!("AgentGetView", "agent.get", arb_agent_get_result), + registry_entry!( + "AgentInterruptResult", + "agent.interrupt", + arb_agent_interrupt_result + ), + registry_entry!("InvokeResultView", "agent.invoke", arb_agent_invoke_result), + registry_entry!( + "AgentsMetadataResponseView", + "agent.list", + arb_agent_list_result + ), + registry_entry!("AgentCreateView", "agent.new", arb_agent_new_result), + registry_entry!("AgentOplogEntryView", "agent.oplog", arb_agent_oplog_result), + registry_entry!( + "AgentPluginToggleResult", + "agent.plugin-toggle", + arb_agent_plugin_toggle_result + ), + registry_entry!( + "AgentRedeployResult", + "agent.redeploy", + arb_agent_redeploy_result + ), + registry_entry!("AgentResumeResult", "agent.resume", arb_agent_resume_result), + registry_entry!("AgentRevertResult", "agent.revert", arb_agent_revert_result), + registry_entry!( + "AgentSimulateCrashResult", + "agent.simulate-crash", + arb_agent_simulate_crash_result + ), + registry_entry!("AgentStreamEvent", "agent.stream", arb_agent_stream_event), + registry_entry!( + "TryUpdateAllWorkersResult", + "agent.update", + arb_agent_update_result + ), + registry_entry!( + "TokenDeleteResult", + "api-token.delete", + arb_token_delete_result + ), + registry_entry!("TokenListView", "api-token.list", arb_token_list_result), + registry_entry!("TokenNewView", "api-token.new", arb_token_new_result), + registry_entry!( + "HttpApiDeploymentGetView", + "api.deployment.get", + arb_api_deployment_get_result + ), + registry_entry!( + "HttpApiDeploymentListView", + "api.deployment.list", + arb_api_deployment_list_result + ), + registry_entry!( + "DomainRegistrationDeleteResult", + "api.domain.delete", + arb_api_domain_delete_result + ), + registry_entry!( + "HttpApiDomainListView", + "api.domain.list", + arb_api_domain_list_result + ), + registry_entry!( + "DomainRegistrationNewView", + "api.domain.register", + arb_api_domain_register_result + ), + registry_entry!( + "HttpSecuritySchemeCreateView", + "api.security-scheme.create", + arb_api_security_scheme_create_result + ), + registry_entry!( + "HttpSecuritySchemeDeleteView", + "api.security-scheme.delete", + arb_api_security_scheme_delete_result + ), + registry_entry!( + "HttpSecuritySchemeGetView", + "api.security-scheme.get", + arb_api_security_scheme_get_result + ), + registry_entry!( + "HttpSecuritySchemeListView", + "api.security-scheme.list", + arb_api_security_scheme_list_result + ), + registry_entry!( + "HttpSecuritySchemeUpdateView", + "api.security-scheme.update", + arb_api_security_scheme_update_result + ), + registry_entry!("BuildResult", "build", arb_build_result), + registry_entry!("CleanResult", "clean", arb_clean_result), + registry_entry!("DeployPlanView", "deploy.plan", arb_deploy_plan_result), + registry_entry!("DeployResultView", "deploy", arb_deploy_result), + registry_entry!( + "GenerateBridgeResult", + "generate-bridge", + arb_generate_bridge_result + ), + registry_entry!("NewAppResult", "new", arb_new_app_result), + registry_entry!("TemplateListView", "templates", arb_template_list_result), + registry_entry!( + "ComponentGetView", + "component.get", + arb_component_get_result + ), + registry_entry!( + "ComponentListView", + "component.list", + arb_component_list_result + ), + registry_entry!( + "ComponentManifestTraceView", + "component.manifest-trace", + arb_component_manifest_trace_result + ), + registry_entry!( + "DeploymentNewView", + "deploy.deployment", + arb_deployment_create_result + ), + registry_entry!("DeploymentDiff", "deploy.diff", arb_deployment_diff_result), + registry_entry!( + "DeploymentListView", + "deploy.deployments", + arb_deployment_list_result + ), + registry_entry!( + "EnvironmentListView", + "environment.list", + arb_environment_list_result + ), + registry_entry!( + "EnvironmentSyncDeploymentOptionsResult", + "environment.sync-deployment-options", + arb_environment_sync_deployment_options_result + ), + registry_entry!( + "EnvironmentSetupPlanView", + "deploy.environment-setup-plan", + arb_environment_setup_plan_result + ), + registry_entry!( + "PluginRegistrationGetView", + "plugin.get", + arb_plugin_get_result + ), + registry_entry!("PluginListView", "plugin.list", arb_plugin_list_result), + registry_entry!( + "PluginRegistrationRegisterView", + "plugin.register", + arb_plugin_register_result + ), + registry_entry!( + "PluginUnregisterResult", + "plugin.unregister", + arb_plugin_unregister_result + ), + registry_entry!( + "ProfileConfigSetFormatResult", + "profile.config.set-format", + arb_profile_config_set_format_result + ), + registry_entry!( + "ProfileDeleteResult", + "profile.delete", + arb_profile_delete_result + ), + registry_entry!("ProfileView", "profile.get", arb_profile_get_result), + registry_entry!("ProfileListView", "profile.list", arb_profile_list_result), + registry_entry!( + "ProfileCreateResult", + "profile.new", + arb_profile_create_result + ), + registry_entry!( + "ProfileSwitchResult", + "profile.switch", + arb_profile_switch_result + ), + registry_entry!( + "ResourceDefinitionCreateView", + "resource.create", + arb_resource_create_result + ), + registry_entry!( + "ResourceDefinitionDeleteView", + "resource.delete", + arb_resource_delete_result + ), + registry_entry!( + "ResourceDefinitionGetView", + "resource.get", + arb_resource_get_result + ), + registry_entry!( + "ResourceDefinitionListView", + "resource.list", + arb_resource_list_result + ), + registry_entry!( + "ResourceDefinitionUpdateView", + "resource.update", + arb_resource_update_result + ), + registry_entry!( + "RetryPolicyCreateView", + "retry-policy.create", + arb_retry_policy_create_result + ), + registry_entry!( + "RetryPolicyDeleteView", + "retry-policy.delete", + arb_retry_policy_delete_result + ), + registry_entry!( + "RetryPolicyGetView", + "retry-policy.get", + arb_retry_policy_get_result + ), + registry_entry!( + "RetryPolicyListView", + "retry-policy.list", + arb_retry_policy_list_result + ), + registry_entry!( + "RetryPolicyUpdateView", + "retry-policy.update", + arb_retry_policy_update_result + ), + registry_entry!( + "SecretCreateView", + "secret.create", + arb_secret_create_result + ), + registry_entry!( + "SecretDeleteView", + "secret.delete", + arb_secret_delete_result + ), + registry_entry!("SecretGetView", "secret.get", arb_secret_get_result), + registry_entry!("SecretListView", "secret.list", arb_secret_list_result), + registry_entry!( + "SecretUpdateView", + "secret.update-value", + arb_secret_update_value_result + ), +]; + +#[derive(Debug, Clone)] +struct OutputImpl { + rust_type: String, + kind: String, + file: PathBuf, + tuple_field_type: Option, +} + +impl OutputImpl { + fn type_name(&self) -> String { + self.kind.clone() + } +} + +#[derive(Default)] +struct SourceSummary { + outputs: Vec, + tuple_field_types_by_struct: BTreeMap, +} + +#[test] +fn cli_output_schema_source_kinds_are_consistent() { + let summary = collect_source_summary(); + let mut errors = Vec::new(); + + if let Ok(path) = std::env::var("GOLEM_CLI_OUTPUT_SUMMARY_MD") { + if let Some(parent) = Path::new(&path).parent() { + std::fs::create_dir_all(parent) + .unwrap_or_else(|err| panic!("failed to create {}: {err}", parent.display())); + } + std::fs::write(&path, render_markdown_summary(&summary.outputs)) + .unwrap_or_else(|err| panic!("failed to write {path}: {err}")); + } + + let mut by_type_name = BTreeMap::>::new(); + for output in &summary.outputs { + by_type_name + .entry(output.type_name()) + .or_default() + .push(output); + } + + for (type_name, outputs) in by_type_name { + if outputs.len() > 1 { + errors.push(format!( + "duplicate CLI output $type {type_name}: {}", + outputs + .iter() + .map(|output| output.rust_type.as_str()) + .collect::>() + .join(", ") + )); + } + } + + for output in &summary.outputs { + if !is_valid_kind(&output.kind) { + errors.push(format!( + "{} has invalid KIND {:?}", + output.rust_type, output.kind + )); + } + + if let Some(tuple_field_type) = &output.tuple_field_type + && is_known_non_object_type(tuple_field_type) + { + errors.push(format!( + "{} is a StructuredOutput tuple wrapper around non-object type `{}`; use a named output struct instead", + output.rust_type, tuple_field_type, + )); + } + } + + assert!(errors.is_empty(), "\n{}", errors.join("\n")); +} + +#[test] +fn cli_output_schema_matches_source_registry() { + let source_entries = source_output_entries(); + let schema = load_command_output_schema(); + let schema_entries = schema_output_entries(&schema); + let registry_entries = registry_output_entries(); + + assert_eq!(registry_entries, source_entries); + assert_eq!(schema_entries, source_entries); + + let definitions = schema + .get("definitions") + .and_then(Value::as_object) + .expect("schema must have object definitions"); + let one_of_refs = schema + .get("oneOf") + .and_then(Value::as_array) + .expect("schema must have array oneOf") + .iter() + .map(|entry| { + entry + .get("$ref") + .and_then(Value::as_str) + .expect("oneOf entry must have string $ref") + .strip_prefix("#/definitions/") + .expect("oneOf $ref must point to #/definitions") + .to_string() + }) + .collect::>(); + + let schema_types = schema_entries.keys().cloned().collect::>(); + let definition_types = definitions.keys().cloned().collect::>(); + + let missing_definitions = schema_types + .difference(&definition_types) + .cloned() + .collect::>(); + assert!( + missing_definitions.is_empty(), + "each output type must have a schema definition, missing: {missing_definitions:?}" + ); + assert_eq!( + one_of_refs, schema_types, + "oneOf refs must match output types" + ); + + jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); +} + +#[test] +fn cli_output_schema_types_lists_only_type_names() { + let types = command_output_type_names().expect("type names should render"); + let types = types.as_array().expect("types output must be an array"); + + assert!( + types.iter().all(Value::is_string), + "types output must contain only strings" + ); + assert!(types.iter().any(|value| value == "agent.oplog")); + assert!(types.iter().any(|value| value == "agent.stream")); +} + +#[test] +fn cli_output_schema_output_definitions_have_agent_metadata() { + let schema = load_command_output_schema(); + let definitions = schema_definitions(&schema); + + for output_type in schema_output_entries(&schema).keys() { + let definition = definitions + .get(output_type) + .unwrap_or_else(|| panic!("missing definition for {output_type}")); + for field in ["description", "x-golem-output-mode", "x-golem-command"] { + assert!( + definition.get(field).is_some(), + "{output_type} must define top-level {field} metadata" + ); + } + + let output_mode = definition + .get("x-golem-output-mode") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{output_type} must define string x-golem-output-mode")); + assert!( + matches!(output_mode, "single" | "stream" | "multi-document"), + "{output_type} has invalid x-golem-output-mode {output_mode:?}" + ); + + let primary_command = definition + .get("x-golem-command") + .and_then(Value::as_str) + .unwrap_or_else(|| panic!("{output_type} must define string x-golem-command")); + assert!( + !primary_command.trim().is_empty(), + "{output_type} must define non-empty x-golem-command" + ); + + if let Some(commands) = definition.get("x-golem-commands") { + let commands = commands + .as_array() + .unwrap_or_else(|| panic!("{output_type} x-golem-commands must be an array")); + assert!( + !commands.is_empty(), + "{output_type} x-golem-commands must not be empty" + ); + assert!( + commands.iter().all(|command| command + .as_str() + .is_some_and(|command| !command.trim().is_empty())), + "{output_type} x-golem-commands must contain only non-empty strings" + ); + assert!( + commands.iter().any(|command| command == primary_command), + "{output_type} x-golem-commands must include x-golem-command" + ); + } + } +} + +#[test] +fn cli_output_schema_focus_prunes_unrelated_definitions() { + let schema = focused_command_output_schema(&["agent.oplog".to_string()]) + .expect("focused schema should render"); + let definitions = schema_definitions(&schema); + + assert!(definitions.contains_key("agent.oplog")); + assert!(definitions.contains_key("PublicOplogEntry")); + assert!(!definitions.contains_key("agent.list")); + assert!(!definitions.contains_key("component.list")); + + let entries = schema_output_entries(&schema); + assert_eq!( + entries.keys().collect::>(), + vec![&"agent.oplog".to_string()] + ); + + let validator = jsonschema::options() + .build(&schema) + .expect("focused command output schema must be valid JSON schema"); + let example = (arb_agent_oplog_result()) + .new_tree(&mut proptest::test_runner::TestRunner::deterministic()) + .expect("oplog strategy should produce value") + .current(); + assert!( + validator.is_valid(&example), + "focused schema should accept agent.oplog example: {:?}", + validator + .iter_errors(&example) + .map(|error| error.to_string()) + .collect::>() + ); +} + +#[test] +fn cli_output_schema_focus_supports_multiple_types() { + let schema = + focused_command_output_schema(&["agent.oplog".to_string(), "agent.stream".to_string()]) + .expect("focused schema should render"); + let definitions = schema_definitions(&schema); + + assert!(definitions.contains_key("agent.oplog")); + assert!(definitions.contains_key("agent.stream")); + assert!(!definitions.contains_key("agent.list")); + + let entries = schema_output_entries(&schema); + assert_eq!( + entries.keys().cloned().collect::>(), + BTreeSet::from_iter(["agent.oplog".to_string(), "agent.stream".to_string()]) + ); +} + +#[test] +fn cli_output_schema_focus_deduplicates_requested_types() { + let schema = + focused_command_output_schema(&["agent.oplog".to_string(), "agent.oplog".to_string()]) + .expect("focused schema should render"); + + let one_of = schema + .get("oneOf") + .and_then(Value::as_array) + .expect("focused schema must have oneOf"); + assert_eq!(one_of.len(), 1); + + let validator = jsonschema::options() + .build(&schema) + .expect("focused command output schema must be valid JSON schema"); + let example = (arb_agent_oplog_result()) + .new_tree(&mut proptest::test_runner::TestRunner::deterministic()) + .expect("oplog strategy should produce value") + .current(); + assert!(validator.is_valid(&example)); +} + +#[test] +fn cli_output_schema_focus_rejects_helper_definition_names() { + let error = focused_command_output_schema(&["JsonValue".to_string()]) + .expect_err("helper definition should not be accepted as an output type"); + + assert!(error.to_string().contains("Unknown output type: JsonValue")); +} + +#[test] +fn cli_output_schema_focus_rejects_unknown_type() { + let error = focused_command_output_schema(&["unknown".to_string()]) + .expect_err("unknown output type should fail"); + + assert!(error.to_string().contains("Unknown output type: unknown")); +} + +#[test] +fn agent_list_structured_output_masks_secret_config_paths() { + let output = crate::model::agent::AgentsMetadataResponseView { + agents: vec![sample_agent_metadata_view()], + cursors: BTreeMap::new(), + }; + + let value = hidden_structured_output(output); + + assert_eq!(value["agents"][0]["config"][0]["value"], json!("***")); + assert_eq!( + value["agents"][0]["defaultConfig"][0]["value"], + json!("***") + ); + assert!(!value.to_string().contains("runtime-secret")); + assert!(!value.to_string().contains("default-secret")); +} + +#[test] +fn agent_get_structured_output_masks_secret_config_paths() { + let value = hidden_structured_output(crate::model::text::agent::AgentGetView { + metadata: sample_agent_metadata_view(), + precise: true, + }); + + assert_eq!(value["metadata"]["config"][0]["value"], json!("***")); + assert_eq!(value["metadata"]["defaultConfig"][0]["value"], json!("***")); + assert_no_plaintext(&value, &["runtime-secret", "default-secret"]); +} + +#[test] +fn component_get_and_list_structured_outputs_mask_secret_payloads() { + let component = sample_component_view(); + + let get = hidden_structured_output(crate::model::text::component::ComponentGetView( + component.clone(), + )); + let list = hidden_structured_output(crate::model::text::component::ComponentListView { + components: vec![component], + }); + + for value in [get, list] { + assert_no_plaintext( + &value, + &[ + "component-env-secret", + "component-config-secret", + "component-plugin-secret", + ], + ); + assert!(value.to_string().contains("***")); + } +} + +#[test] +fn component_manifest_trace_structured_output_masks_secret_payloads() { + let value = + hidden_structured_output(crate::model::text::component::ComponentManifestTraceView { + component_name: golem_common::model::component::ComponentName("component".to_string()), + properties: sample_component_layer_properties(), + }); + + assert_no_plaintext( + &value, + &[ + "manifest-config-secret", + "manifest-env-secret", + "manifest-plugin-secret", + ], + ); + assert!(value.to_string().contains("***")); +} + +#[test] +fn deploy_diff_and_plan_structured_outputs_mask_secret_payloads() { + let diff = sample_deployment_diff_with_secret_updates(); + let diff_value = hidden_structured_output(diff.clone()); + let plan_value = hidden_structured_output(DeployPlanView { + deployment_diff: &diff, + environment_setup: None, + }); + + for value in [diff_value, plan_value] { + assert_no_plaintext( + &value, + &[ + "deploy-env-secret-old", + "deploy-env-secret-new", + "deploy-config-secret-old", + "deploy-config-secret-new", + ], + ); + assert!(value.to_string().contains("(output: Output) -> Value { + to_structured_output_value_masked(output, MaskingConfig::hide_secrets()) + .expect("output should serialize with hidden secrets") +} + +fn assert_no_plaintext(value: &Value, plaintexts: &[&str]) { + let serialized = value.to_string(); + for plaintext in plaintexts { + assert!( + !serialized.contains(plaintext), + "structured output leaked plaintext {plaintext}: {serialized}" + ); + } +} + +fn sample_agent_metadata_view() -> crate::model::agent::AgentMetadataView { + crate::model::agent::AgentMetadataView { + component_name: golem_common::model::component::ComponentName("component".to_string()), + agent_id: crate::model::agent::RawAgentId("agent()".to_string()), + created_by: golem_common::model::account::AccountId(uuid::Uuid::nil()), + environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), + env: BTreeMap::new().into_iter().collect(), + default_env: BTreeMap::new().into_iter().collect(), + config: vec![golem_common::model::worker::AgentConfigEntryDto { + path: vec!["db".to_string(), "password".to_string()], + value: golem_common::base_model::json::NormalizedJsonValue(json!("runtime-secret")), + }], + default_config: vec![golem_common::model::worker::AgentConfigEntryDto { + path: vec!["db".to_string(), "password".to_string()], + value: golem_common::base_model::json::NormalizedJsonValue(json!("default-secret")), + }], + status: golem_common::model::AgentStatus::Idle, + component_revision: golem_common::model::component::ComponentRevision::new(1).unwrap(), + retry_count: 0, + pending_invocation_count: 0, + updates: vec![], + created_at: "2024-01-01T00:00:00Z".parse().unwrap(), + last_error: None, + component_size: 0, + total_linear_memory_size: 0, + exported_resource_instances: BTreeMap::new().into_iter().collect(), + source_language: crate::agent_id_display::SourceLanguage::default(), + secret_config_paths: BTreeSet::from_iter(["db.password".to_string()]), + } +} + +fn sample_component_view() -> crate::model::component::ComponentView { + let agent_type_name = golem_common::model::agent::AgentTypeName("agent".to_string()); + crate::model::component::ComponentView { + component_name: golem_common::model::component::ComponentName("component".to_string()), + component_id: golem_common::model::component::ComponentId(uuid::Uuid::nil()), + component_version: Some("1.0.0".to_string()), + component_revision: 1, + component_size: 0, + created_at: fixed_datetime(), + environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), + exports: vec![], + agent_types: vec![sample_agent_type_schema(agent_type_name.clone())], + agent_type_provision_configs: BTreeMap::from_iter([( + agent_type_name, + golem_common::model::component_metadata::AgentTypeProvisionConfig { + env: BTreeMap::from_iter([( + "API_TOKEN".to_string(), + "component-env-secret".to_string(), + )]), + config: vec![golem_common::model::worker::TypedAgentConfigEntry { + path: vec!["db".to_string(), "password".to_string()], + value: typed_schema_string("component-config-secret"), + }], + plugins: vec![golem_common::model::component::InstalledPlugin { + environment_plugin_grant_id: + golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId( + uuid::Uuid::nil(), + ), + priority: golem_common::model::component::PluginPriority(1), + parameters: BTreeMap::from_iter([( + "apiKey".to_string(), + "component-plugin-secret".to_string(), + )]), + plugin_registration_id: + golem_common::model::plugin_registration::PluginRegistrationId( + uuid::Uuid::nil(), + ), + plugin_name: "plugin".to_string(), + plugin_version: "1.0.0".to_string(), + oplog_processor_component_id: None, + oplog_processor_component_revision: None, + }], + files: vec![], + initial_permissions: PolymorphicCard { + card_id: CardId(uuid!("a741846a-2562-4065-8a06-fd9dbee52198")), + parent_ids: vec![CardId(uuid!("cd6d7717-4df1-4e9a-ac26-5bf524c1a732"))], + lower_negative: Vec::new(), + lower_positive: Vec::new(), + upper_negative: Vec::new(), + upper_positive: Vec::new(), + system_card: false, + created_at: fixed_datetime(), + expires_at: None, + }, + }, + )]), + } +} + +fn sample_agent_type_schema( + agent_type_name: golem_common::model::agent::AgentTypeName, +) -> golem_common::schema::agent::AgentTypeSchema { + golem_common::schema::agent::AgentTypeSchema { + type_name: agent_type_name, + description: String::new(), + source_language: String::new(), + schema: golem_common::schema::SchemaGraph::empty(), + constructor: golem_common::schema::agent::AgentConstructorSchema { + name: None, + description: String::new(), + prompt_hint: None, + input_schema: golem_common::schema::agent::InputSchema::parameters([ + golem_common::schema::agent::NamedField::user_supplied( + "value", + golem_common::schema::SchemaType::string(), + ), + ]), + }, + methods: vec![golem_common::schema::agent::AgentMethodSchema { + name: "method".to_string(), + description: String::new(), + prompt_hint: None, + input_schema: golem_common::schema::agent::InputSchema::parameters([ + golem_common::schema::agent::NamedField::auto_injected( + "principal", + golem_common::schema::agent::AutoInjectedKind::Principal, + golem_common::schema::SchemaType::string(), + ), + ]), + output_schema: golem_common::schema::agent::OutputSchema::Single(Box::new( + golem_common::schema::SchemaType::u64(), + )), + http_endpoint: vec![], + read_only: None, + }], + dependencies: vec![], + mode: golem_common::model::agent::AgentMode::Durable, + http_mount: None, + snapshotting: golem_common::model::agent::Snapshotting::Disabled( + golem_common::model::Empty {}, + ), + config: vec![golem_common::schema::agent::AgentConfigDeclarationSchema { + source: golem_common::model::agent::AgentConfigSource::Secret, + path: vec!["db".to_string(), "password".to_string()], + value_type: golem_common::schema::SchemaType::string(), + }], + } +} + +fn typed_schema_string(value: &str) -> golem_common::schema::TypedSchemaValue { + golem_common::schema::TypedSchemaValue::new( + golem_common::schema::SchemaGraph::anonymous(golem_common::schema::SchemaType::string()), + golem_common::schema::SchemaValue::String(value.to_string()), + ) +} + +fn sample_component_layer_properties() -> crate::model::app::ComponentLayerProperties { + use crate::model::cascade::property::Property; + + let layer = crate::model::app::ComponentLayerId::ComponentCommon( + golem_common::model::component::ComponentName("component".to_string()), + ); + let mut properties = crate::model::app::ComponentLayerProperties::default(); + properties.config.apply_layer( + &layer, + None, + Some(json!({ "db": { "password": "manifest-config-secret" } })), + ); + properties.env.apply_layer( + &layer, + None, + ( + crate::model::cascade::property::map::MapMergeMode::Upsert, + indexmap::IndexMap::from_iter([( + "API_TOKEN".to_string(), + "manifest-env-secret".to_string(), + )]), + ), + ); + properties.plugins.apply_layer( + &layer, + None, + ( + crate::model::cascade::property::vec::VecMergeMode::Append, + vec![crate::model::app_raw::PluginInstallation { + account: None, + name: "plugin".to_string(), + version: "1.0.0".to_string(), + parameters: std::collections::HashMap::from_iter([( + "apiKey".to_string(), + "manifest-plugin-secret".to_string(), + )]), + }], + ), + ); + properties +} + +fn sample_deployment_diff_with_secret_updates() -> golem_common::model::diff::DeploymentDiff { + use golem_common::model::diff::{Diffable, HashOf}; + + let mut current = golem_common::model::diff::Deployment::default(); + let mut new = golem_common::model::diff::Deployment::default(); + let wasm_hash = fixed_hash("wasm"); + + current.components.insert( + "component".to_string(), + HashOf::form_value(golem_common::model::diff::Component { + wasm_hash, + agent_type_provision_configs: BTreeMap::from_iter([( + "agent".to_string(), + HashOf::form_value(sample_diff_provision_config( + "deploy-env-secret-old", + "deploy-config-secret-old", + )), + )]), + }), + ); + new.components.insert( + "component".to_string(), + HashOf::form_value(golem_common::model::diff::Component { + wasm_hash, + agent_type_provision_configs: BTreeMap::from_iter([( + "agent".to_string(), + HashOf::form_value(sample_diff_provision_config( + "deploy-env-secret-new", + "deploy-config-secret-new", + )), + )]), + }), + ); + + golem_common::model::diff::Deployment::diff(&new, ¤t) + .expect("sample deployments should diff") + .expect("sample deployments should differ") +} + +fn sample_diff_provision_config( + env_secret: &str, + config_secret: &str, +) -> golem_common::model::diff::AgentTypeProvisionConfig { + golem_common::model::diff::AgentTypeProvisionConfig { + env: BTreeMap::from_iter([("API_TOKEN".to_string(), env_secret.to_string())]), + config: BTreeMap::from_iter([( + "db.password".to_string(), + golem_common::base_model::json::NormalizedJsonValue(json!(config_secret)), + )]), + files_by_path: BTreeMap::new(), + plugins_by_grant_id: BTreeMap::new(), + initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { + lower_positive: Vec::new(), + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + }, + } +} + +fn fixed_hash(input: &str) -> golem_common::model::diff::Hash { + golem_common::model::diff::Hash::new(blake3::hash(input.as_bytes())) +} + +#[test] +fn cli_output_schema_validates_schema_native_secret_outputs() { + let schema = load_command_output_schema(); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + + let secret = golem_client::model::AgentSecretDto { + id: golem_common::model::agent_secret::AgentSecretId(uuid::Uuid::nil()), + environment_id: golem_common::model::environment::EnvironmentId(uuid::Uuid::nil()), + path: golem_common::model::agent_secret::CanonicalAgentSecretPath(vec![ + "token".to_string(), + ]), + revision: golem_common::model::agent_secret::AgentSecretRevision::new(1) + .expect("static secret revision should be valid"), + secret_type: golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::string(), + ), + secret_value: Some(golem_common::schema::SchemaValue::String( + "super-secret".to_string(), + )), + }; + + let outputs = vec![ + to_structured_output_value_masked( + crate::model::text::secret::SecretCreateView(secret.clone().into()), + MaskingConfig::hide_secrets(), + ) + .expect("secret.create should serialize"), + to_structured_output_value_masked( + crate::model::text::secret::SecretDeleteView(secret.clone().into()), + MaskingConfig::hide_secrets(), + ) + .expect("secret.delete should serialize"), + to_structured_output_value_masked( + crate::model::text::secret::SecretGetView(secret.clone().into()), + MaskingConfig::hide_secrets(), + ) + .expect("secret.get should serialize"), + to_structured_output_value_masked( + crate::model::text::secret::SecretUpdateView(secret.clone().into()), + MaskingConfig::hide_secrets(), + ) + .expect("secret.update-value should serialize"), + to_structured_output_value_masked( + crate::model::text::secret::SecretListView { + secrets: vec![secret.into()], + environment_name: "generated-environment".to_string(), + show_ids: false, + }, + MaskingConfig::hide_secrets(), + ) + .expect("secret.list should serialize"), + ]; + + for output in outputs { + assert!( + validator.is_valid(&output), + "schema should accept schema-native secret output: {:?}", + validator + .iter_errors(&output) + .map(|error| error.to_string()) + .collect::>() + ); + + let secret_view = output + .get("secrets") + .and_then(Value::as_array) + .and_then(|secrets| secrets.first()) + .unwrap_or(&output); + + assert_eq!(secret_view["secretType"]["root"]["kind"], json!("string")); + assert_eq!(secret_view["secretValue"]["kind"], json!("string")); + assert_eq!(secret_view["secretValue"]["value"], json!("***")); + } +} + +#[test] +fn cli_output_schema_validates_schema_native_component_and_agent_outputs() { + let schema = load_command_output_schema(); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + let mut runner = proptest::test_runner::TestRunner::deterministic(); + + let component = arb_component_view() + .new_tree(&mut runner) + .expect("component strategy should produce a value") + .current(); + let agent_type = arb_deployed_registered_agent_type() + .new_tree(&mut runner) + .expect("agent type strategy should produce a value") + .current(); + + let outputs = vec![ + to_structured_output_value(crate::model::text::component::ComponentGetView( + component.clone(), + )) + .expect("component.get should serialize"), + to_structured_output_value(crate::model::text::component::ComponentListView { + components: vec![component], + }) + .expect("component.list should serialize"), + to_structured_output_value(crate::model::text::agent::AgentTypeListView { + agent_types: vec![agent_type], + }) + .expect("agent-type.list should serialize"), + to_structured_output_value(crate::model::text::agent::AgentOplogEntryView { + index: 0, + entry: sample_public_oplog_entries() + .into_iter() + .next() + .expect("sample oplog entries should not be empty"), + }) + .expect("agent.oplog should serialize"), + ]; + + for output in outputs { + assert!( + validator.is_valid(&output), + "schema should accept schema-native output: {:?}", + validator + .iter_errors(&output) + .map(|error| error.to_string()) + .collect::>() + ); + } +} + +#[test] +fn cli_output_schema_validates_discriminated_documents() { + let schema = load_command_output_schema(); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + + let definitions = schema_definitions(&schema); + for output_type in schema_output_entries(&schema).keys() { + if !is_discriminator_only_definition( + definitions + .get(output_type) + .unwrap_or_else(|| panic!("missing definition for {output_type}")), + ) { + continue; + } + + let value = json!({ CLI_OUTPUT_TYPE_FIELD: output_type }); + assert!( + validator.is_valid(&value), + "schema should accept minimal document for {output_type}: {:?}", + validator + .iter_errors(&value) + .map(|error| error.to_string()) + .collect::>() + ); + } + + let missing_type = json!({ "ok": true }); + assert!( + !validator.is_valid(&missing_type), + "schema must reject output documents without {CLI_OUTPUT_TYPE_FIELD}" + ); + + let unknown_type = json!({ CLI_OUTPUT_TYPE_FIELD: "unknown" }); + assert!( + !validator.is_valid(&unknown_type), + "schema must reject unknown output document types" + ); +} + +#[test] +fn cli_output_schema_exact_registered_schemas_reject_extra_fields() { + let schema = load_command_output_schema(); + let definitions = schema_definitions(&schema); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + + for entry in STRUCTURED_OUTPUT_TEST_REGISTRY.iter().filter(|entry| { + !definition_allows_additional_properties( + definitions + .get(entry.output_type) + .unwrap_or_else(|| panic!("missing definition for {}", entry.output_type)), + ) + }) { + for mut example in (entry.examples)() { + let Some(object) = example.as_object_mut() else { + panic!("example for {} must be an object", entry.output_type); + }; + object.insert("unexpectedExtraField".to_string(), json!(true)); + + assert!( + !validator.is_valid(&example), + "exact schema should reject extra fields for {}", + entry.output_type, + ); + } + } +} + +#[test] +fn cli_output_schema_validates_registered_examples() { + let schema = load_command_output_schema(); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + + for entry in STRUCTURED_OUTPUT_TEST_REGISTRY { + for example in (entry.examples)() { + assert!( + validator.is_valid(&example), + "schema should accept example for {}: {:?}", + entry.output_type, + validator + .iter_errors(&example) + .map(|error| error.to_string()) + .collect::>() + ); + } + } +} + +proptest! { + #[test] + fn cli_output_schema_accepts_registered_generated_examples(value in arb_registered_output_document()) { + let schema = load_command_output_schema(); + let validator = jsonschema::options() + .build(&schema) + .expect("command output schema must be a valid JSON schema"); + + prop_assert!( + validator.is_valid(&value), + "schema should accept generated example: {:?}", + validator + .iter_errors(&value) + .map(|error| error.to_string()) + .collect::>() + ); + } +} + +fn load_command_output_schema() -> Value { + serde_json::from_str(crate::model::cli_output::COMMAND_OUTPUT_SCHEMA_JSON) + .expect("embedded command output schema must parse") +} + +fn schema_output_entries(schema: &Value) -> BTreeMap { + schema + .get("x-golem-cli-output-types") + .and_then(Value::as_array) + .expect("schema must have array x-golem-cli-output-types") + .iter() + .map(|entry| { + let output_type = entry + .get("type") + .and_then(Value::as_str) + .expect("schema output entry must have string type") + .to_string(); + let rust_type = entry + .get("rustType") + .and_then(Value::as_str) + .expect("schema output entry must have string rustType") + .to_string(); + (output_type, rust_type) + }) + .collect() +} + +fn schema_definitions(schema: &Value) -> &serde_json::Map { + schema + .get("definitions") + .and_then(Value::as_object) + .expect("schema must have object definitions") +} + +fn is_discriminator_only_definition(definition: &Value) -> bool { + definition + .get("required") + .and_then(Value::as_array) + .is_some_and(|required| { + required.len() == 1 + && required + .first() + .and_then(Value::as_str) + .is_some_and(|field| field == CLI_OUTPUT_TYPE_FIELD) + }) +} + +fn definition_allows_additional_properties(definition: &Value) -> bool { + definition + .get("additionalProperties") + .and_then(Value::as_bool) + .unwrap_or(true) +} + +fn registry_output_entries() -> BTreeMap { + STRUCTURED_OUTPUT_TEST_REGISTRY + .iter() + .map(|entry| (entry.output_type.to_string(), entry.rust_type.to_string())) + .collect() +} + +fn source_output_entries() -> BTreeMap { + collect_source_summary() + .outputs + .into_iter() + .map(|output| (output.type_name(), output.rust_type)) + .collect() +} + +fn collect_source_summary() -> SourceSummary { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut summary = SourceSummary::default(); + + for entry in WalkDir::new(&source_root) + .into_iter() + .filter_entry(|entry| !is_ignored_path(entry.path())) + .filter_map(Result::ok) + .filter(|entry| entry.file_type().is_file()) + .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "rs")) + { + let file_path = entry.path(); + let source = std::fs::read_to_string(file_path) + .unwrap_or_else(|err| panic!("failed to read {}: {err}", file_path.display())); + let parsed = syn::parse_file(&source) + .unwrap_or_else(|err| panic!("failed to parse {}: {err}", file_path.display())); + let relative_path = file_path + .strip_prefix(Path::new(env!("CARGO_MANIFEST_DIR"))) + .unwrap_or(file_path) + .to_path_buf(); + + collect_items(&parsed.items, &relative_path, &mut summary); + } + + summary.outputs.sort_by(|left, right| { + left.kind + .cmp(&right.kind) + .then(left.rust_type.cmp(&right.rust_type)) + }); + + for output in &mut summary.outputs { + output.tuple_field_type = summary + .tuple_field_types_by_struct + .get(&output.rust_type) + .cloned(); + } + + summary +} + +fn is_ignored_path(path: &Path) -> bool { + path.components().any(|component| { + let component = component.as_os_str(); + component == "target" || component == ".git" + }) +} + +fn collect_items(items: &[Item], file: &Path, summary: &mut SourceSummary) { + for item in items { + match item { + Item::Struct(item) => collect_struct(item, file, summary), + Item::Impl(item) => collect_impl(item, file, summary), + Item::Mod(item) => { + if let Some((_, items)) = &item.content { + collect_items(items, file, summary); + } + } + _ => {} + } + } +} + +fn collect_struct(item: &syn::ItemStruct, file: &Path, summary: &mut SourceSummary) { + if let syn::Fields::Unnamed(fields) = &item.fields + && fields.unnamed.len() == 1 + && let Some(ty) = fields + .unnamed + .first() + .map(|field| field.ty.to_token_stream().to_string()) + { + summary + .tuple_field_types_by_struct + .insert(item.ident.to_string(), ty); + } + + let _ = file; +} + +fn collect_impl(item: &ItemImpl, file: &Path, summary: &mut SourceSummary) { + let Some(trait_name) = item + .trait_ + .as_ref() + .and_then(|(_, path, _)| path.segments.last()) + .map(|segment| segment.ident.to_string()) + else { + return; + }; + + let Some(rust_type) = type_name(&item.self_ty) else { + return; + }; + + if trait_name.as_str() == "StructuredOutput" { + let mut kind = None; + + for impl_item in &item.items { + if let ImplItem::Const(constant) = impl_item + && constant.ident == "KIND" + { + kind = string_literal(&constant.expr); + } + } + + summary.outputs.push(OutputImpl { + rust_type, + kind: kind.unwrap_or_else(|| "".to_string()), + file: file.to_path_buf(), + tuple_field_type: None, + }); + } +} + +fn type_name(ty: &Type) -> Option { + match ty { + Type::Path(path) => path + .path + .segments + .last() + .map(|segment| segment.ident.to_string()), + _ => None, + } +} + +fn string_literal(expr: &Expr) -> Option { + match expr { + Expr::Lit(lit) => match &lit.lit { + Lit::Str(value) => Some(value.value()), + _ => None, + }, + _ => None, + } +} + +fn is_valid_kind(kind: &str) -> bool { + let parts = kind.split('.').collect::>(); + + !parts.is_empty() + && parts.iter().all(|part| { + !part.is_empty() + && part + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') + && part + .bytes() + .next() + .is_some_and(|byte| byte.is_ascii_lowercase()) + }) +} + +fn is_known_non_object_type(ty: &str) -> bool { + let compact = ty.replace(' ', ""); + + if compact.starts_with("Vec<") + || compact.starts_with("Option<") + || compact.starts_with("HashSet<") + || compact.starts_with("BTreeSet<") + || compact.starts_with('[') + { + return true; + } + + matches!( + compact.as_str(), + "String" + | "str" + | "&str" + | "bool" + | "u8" + | "u16" + | "u32" + | "u64" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "isize" + | "f32" + | "f64" + ) +} + +fn render_markdown_summary(summary: &[OutputImpl]) -> String { + let mut output = String::new(); + + output.push_str("# CLI Output Source Summary\n\n"); + output.push_str("Generated from Rust source. Review `$type` names and Rust type mappings.\n\n"); + + output.push_str("## Outputs\n\n"); + output.push_str("| `$type` | Rust Type | Source |\n"); + output.push_str("|---|---|---|\n"); + + for item in summary { + output.push_str(&format!( + "| `{}` | `{}` | `{}` |\n", + escape_table_cell(&item.type_name()), + escape_table_cell(&item.rust_type), + item.file.display(), + )); + } + + output +} + +fn escape_table_cell(value: &str) -> String { + value.replace('|', "\\|").replace('\n', " ") +} + +fn arb_registered_output_document() -> BoxedStrategy { + let strategies = STRUCTURED_OUTPUT_TEST_REGISTRY + .iter() + .map(|entry| (entry.arbitrary)()) + .collect::>(); + proptest::strategy::Union::new(strategies).boxed() +} + +fn serialized_output(strategy: impl Strategy + 'static) -> OutputDocumentStrategy +where + T: StructuredOutput + 'static, +{ + strategy + .prop_map(|output| { + to_structured_output_value(output).expect("generated DTO should serialize") + }) + .boxed() +} + +fn empty_deployment_diff() -> golem_common::model::diff::DeploymentDiff { + golem_common::model::diff::DeploymentDiff { + components: BTreeMap::new(), + http_api_deployments: BTreeMap::new(), + mcp_deployments: BTreeMap::new(), + } +} + +fn arb_small_string() -> BoxedStrategy { + any::() + .prop_map(|value| uuid::Uuid::from_u128(value).to_string()) + .boxed() +} + +fn arb_uuid() -> BoxedStrategy { + any::().prop_map(uuid::Uuid::from_u128).boxed() +} + +fn arb_small_u64() -> BoxedStrategy { + (0u64..1000).boxed() +} + +fn arb_timestamp_string() -> BoxedStrategy { + arb_datetime().prop_map(|value| value.to_rfc3339()).boxed() +} + +fn arb_timestamp() -> BoxedStrategy { + arb_timestamp_string() + .prop_map(|value| value.parse().expect("generated timestamp should parse")) + .boxed() +} + +fn arb_url_string() -> BoxedStrategy { + arb_small_string() + .prop_map(|path| format!("https://example.com/{path}")) + .boxed() +} + +fn arb_datetime() -> BoxedStrategy> { + (0i64..4_102_444_800i64) + .prop_map(|seconds| { + chrono::DateTime::from_timestamp(seconds, 0) + .expect("generated timestamp should be in range") + }) + .boxed() +} + +fn fixed_datetime() -> chrono::DateTime { + chrono::DateTime::parse_from_rfc3339("1970-01-01T00:00:00Z") + .expect("fixed timestamp should parse") + .with_timezone(&chrono::Utc) +} + +fn arb_hash() -> BoxedStrategy { + any::() + .prop_map(|value| golem_common::model::diff::Hash::new(blake3::hash(&value.to_le_bytes()))) + .boxed() +} + +fn arb_agent_status() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::AgentStatus::Running), + Just(golem_common::model::AgentStatus::Idle), + Just(golem_common::model::AgentStatus::Suspended), + Just(golem_common::model::AgentStatus::Interrupted), + Just(golem_common::model::AgentStatus::Retrying), + Just(golem_common::model::AgentStatus::Failed), + Just(golem_common::model::AgentStatus::Exited), + ] + .boxed() +} + +fn sample_public_oplog_entries() -> Vec { + use golem_common::base_model::retry_policy::{ + ApiImmediatePolicy, ApiPredicate, ApiPredicateTrue, ApiRetryPolicy, + }; + use golem_common::model::component::{ComponentId, ComponentRevision, PluginPriority}; + use golem_common::model::environment::EnvironmentId; + use golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId; + use golem_common::model::invocation_context::{SpanId, TraceId}; + use golem_common::model::oplog::public_oplog_entry::*; + use golem_common::model::oplog::*; + use golem_common::model::regions::OplogRegion; + use golem_common::model::{AgentId, Empty, IdempotencyKey, Timestamp}; + use golem_common::schema::{SchemaGraph, SchemaType, SchemaValue, TypedSchemaValue}; + use std::iter::FromIterator; + use uuid::Uuid; + + fn timestamp() -> Timestamp { + Timestamp::from(0) + } + + fn component_id() -> ComponentId { + ComponentId(Uuid::parse_str("13a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap()) + } + + fn agent_id(name: &str) -> AgentId { + AgentId { + component_id: component_id(), + agent_id: name.to_string(), + } + } + + fn plugin(priority: i32) -> PluginInstallationDescription { + PluginInstallationDescription { + environment_plugin_grant_id: EnvironmentPluginGrantId::new(), + plugin_priority: PluginPriority(priority), + plugin_name: "generated-plugin".to_string(), + plugin_version: "1.0.0".to_string(), + parameters: BTreeMap::from_iter([("key".to_string(), "value".to_string())]), + } + } + + fn typed_string_value(value: &str) -> TypedSchemaValue { + TypedSchemaValue::new( + SchemaGraph::anonymous(SchemaType::string()), + SchemaValue::String(value.to_string()), + ) + } + + fn typed_u64_list_value(values: Vec) -> TypedSchemaValue { + TypedSchemaValue::new( + SchemaGraph::anonymous(SchemaType::list(SchemaType::u64())), + SchemaValue::List { + elements: values.into_iter().map(SchemaValue::U64).collect(), + }, + ) + } + + fn span_context() -> Vec> { + vec![vec![PublicSpanData::LocalSpan(PublicLocalSpanData { + span_id: SpanId::generate(), + start: timestamp(), + parent_id: None, + linked_context: Some(1), + attributes: vec![PublicAttribute { + key: "component".to_string(), + value: PublicAttributeValue::String(StringAttributeValue { + value: "generated".to_string(), + }), + }], + inherited: true, + })]] + } + + fn method_invocation() -> PublicAgentInvocation { + PublicAgentInvocation::AgentMethodInvocation(AgentMethodInvocationParameters { + idempotency_key: IdempotencyKey::new("method-key".to_string()), + method_name: "generated-method".to_string(), + function_input: typed_string_value("input"), + trace_id: TraceId::generate(), + trace_states: vec!["trace-state".to_string()], + invocation_context: span_context(), + }) + } + + fn raw_snapshot() -> PublicSnapshotData { + PublicSnapshotData::Raw(RawSnapshotData { + data: vec![1, 2, 3], + mime_type: "application/octet-stream".to_string(), + }) + } + + fn json_snapshot() -> PublicSnapshotData { + PublicSnapshotData::Json(JsonSnapshotData { + data: json!({ "counter": 42 }), + }) + } + + fn multipart_snapshot() -> PublicSnapshotData { + PublicSnapshotData::Multipart(MultipartSnapshotData { + mime_type: "multipart/mixed; boundary=generated".to_string(), + parts: vec![ + MultipartSnapshotPart { + name: "state".to_string(), + content_type: "application/json".to_string(), + data: MultipartPartData::Json(JsonSnapshotData { + data: json!({ "state": "ok" }), + }), + }, + MultipartSnapshotPart { + name: "bytes".to_string(), + content_type: "application/octet-stream".to_string(), + data: MultipartPartData::Raw(RawSnapshotData { + data: vec![4, 5, 6], + mime_type: "application/octet-stream".to_string(), + }), + }, + ], + }) + } + + let retry_policy_state = PublicRetryPolicyState::AndThen(PublicRetryPolicyStateAndThen { + left: Box::new(PublicRetryPolicyState::Counter( + PublicRetryPolicyStateCounter { count: 2 }, + )), + right: Box::new(PublicRetryPolicyState::Terminal(Empty {})), + on_right: true, + }); + + let retry_policy = PublicNamedRetryPolicy { + name: "generated-retry".to_string(), + priority: 10, + predicate: ApiPredicate::True(ApiPredicateTrue {}), + policy: ApiRetryPolicy::Immediate(ApiImmediatePolicy {}), + }; + + vec![ + PublicOplogEntry::Create(CreateParams { + timestamp: timestamp(), + agent_id: agent_id("generated-agent"), + agent_mode: golem_common::model::agent::AgentMode::Durable, + component_revision: ComponentRevision::new(1).unwrap(), + env: BTreeMap::from_iter([("ENV".to_string(), "value".to_string())]), + created_by: golem_common::model::account::AccountId::new(), + local_agent_config: vec![PublicTypedAgentConfigEntry { + path: vec!["config".to_string()], + value: typed_string_value("configured"), + }], + environment_id: EnvironmentId::new(), + parent: Some(agent_id("parent-agent")), + component_size: 10, + initial_total_linear_memory_size: 20, + initial_active_plugins: BTreeSet::from_iter([plugin(0)]), + original_phantom_id: Some( + Uuid::parse_str("23a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap(), + ), + instance_id: Uuid::parse_str("33a5c8d4-f05e-4e23-b982-f4d413e181cb").unwrap(), + }), + PublicOplogEntry::Start(StartParams { + timestamp: timestamp(), + parent_start_index: Some(OplogIndex::from_u64(1)), + function_name: "wasi:keyvalue/store.{get}".to_string(), + request: Some(typed_string_value("request")), + durable_function_type: PublicDurableFunctionType::WriteRemoteBatched( + WriteRemoteBatchedParameters { + index: Some(OplogIndex::from_u64(1)), + }, + ), + }), + PublicOplogEntry::End(EndParams { + timestamp: timestamp(), + start_index: OplogIndex::from_u64(1), + response: Some(typed_u64_list_value(vec![1])), + forced_commit: false, + }), + PublicOplogEntry::Cancelled(CancelledParams { + timestamp: timestamp(), + start_index: OplogIndex::from_u64(2), + partial: Some(typed_string_value("partial")), + }), + PublicOplogEntry::AgentInvocationStarted(AgentInvocationStartedParams { + timestamp: timestamp(), + invocation: method_invocation(), + }), + PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { + timestamp: timestamp(), + result: PublicAgentInvocationResult::AgentMethod(AgentInvocationOutputParameters { + output: typed_string_value("output"), + }), + method_name: Some("generated-method".to_string()), + consumed_fuel: 100, + component_revision: ComponentRevision::new(1).unwrap(), + }), + PublicOplogEntry::Suspend(SuspendParams { + timestamp: timestamp(), + }), + PublicOplogEntry::Error(ErrorParams { + timestamp: timestamp(), + error: "generated error".to_string(), + retry_from: OplogIndex::INITIAL, + inside_atomic_region: false, + retry_policy_state: Some(retry_policy_state), + }), + PublicOplogEntry::NoOp(NoOpParams { + timestamp: timestamp(), + }), + PublicOplogEntry::Jump(JumpParams { + timestamp: timestamp(), + jump: OplogRegion { + start: OplogIndex::from_u64(1), + end: OplogIndex::from_u64(2), + }, + }), + PublicOplogEntry::Interrupted(InterruptedParams { + timestamp: timestamp(), + }), + PublicOplogEntry::Exited(ExitedParams { + timestamp: timestamp(), + }), + PublicOplogEntry::BeginAtomicRegion(BeginAtomicRegionParams { + timestamp: timestamp(), + }), + PublicOplogEntry::EndAtomicRegion(EndAtomicRegionParams { + timestamp: timestamp(), + begin_index: OplogIndex::from_u64(1), + }), + PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { + timestamp: timestamp(), + invocation: PublicAgentInvocation::AgentInitialization(AgentInitializationParameters { + idempotency_key: IdempotencyKey::new("init-key".to_string()), + constructor_parameters: typed_string_value("constructor"), + trace_id: TraceId::generate(), + trace_states: vec![], + invocation_context: span_context(), + }), + }), + PublicOplogEntry::PendingUpdate(PendingUpdateParams { + timestamp: timestamp(), + target_revision: ComponentRevision::new(2).unwrap(), + description: PublicUpdateDescription::SnapshotBased(SnapshotBasedUpdateParameters { + payload: vec![7, 8, 9], + mime_type: "application/octet-stream".to_string(), + }), + }), + PublicOplogEntry::SuccessfulUpdate(SuccessfulUpdateParams { + timestamp: timestamp(), + target_revision: ComponentRevision::new(2).unwrap(), + new_component_size: 30, + new_active_plugins: BTreeSet::from_iter([plugin(1)]), + }), + PublicOplogEntry::FailedUpdate(FailedUpdateParams { + timestamp: timestamp(), + target_revision: ComponentRevision::new(3).unwrap(), + details: None, + }), + PublicOplogEntry::GrowMemory(GrowMemoryParams { + timestamp: timestamp(), + delta: 64, + }), + PublicOplogEntry::FilesystemStorageUsageUpdate(FilesystemStorageUsageUpdateParams { + timestamp: timestamp(), + delta: -5, + }), + PublicOplogEntry::CreateResource(CreateResourceParams { + timestamp: timestamp(), + id: AgentResourceId(1), + name: "resource".to_string(), + owner: "owner".to_string(), + }), + PublicOplogEntry::DropResource(DropResourceParams { + timestamp: timestamp(), + id: AgentResourceId(1), + name: "resource".to_string(), + owner: "owner".to_string(), + }), + PublicOplogEntry::Log(LogParams { + timestamp: timestamp(), + level: LogLevel::Info, + context: "generated".to_string(), + message: "message".to_string(), + }), + PublicOplogEntry::Restart(RestartParams { + timestamp: timestamp(), + }), + PublicOplogEntry::ActivatePlugin(ActivatePluginParams { + timestamp: timestamp(), + plugin: plugin(2), + }), + PublicOplogEntry::DeactivatePlugin(DeactivatePluginParams { + timestamp: timestamp(), + plugin: plugin(3), + }), + PublicOplogEntry::Revert(RevertParams { + timestamp: timestamp(), + dropped_region: OplogRegion { + start: OplogIndex::from_u64(5), + end: OplogIndex::from_u64(10), + }, + }), + PublicOplogEntry::CancelPendingInvocation(CancelPendingInvocationParams { + timestamp: timestamp(), + idempotency_key: IdempotencyKey::new("cancel-key".to_string()), + }), + PublicOplogEntry::StartSpan(StartSpanParams { + timestamp: timestamp(), + span_id: SpanId::generate(), + parent_id: Some(SpanId::generate()), + linked_context: Some(SpanId::generate()), + attributes: vec![PublicAttribute { + key: "http.method".to_string(), + value: PublicAttributeValue::String(StringAttributeValue { + value: "GET".to_string(), + }), + }], + }), + PublicOplogEntry::FinishSpan(FinishSpanParams { + timestamp: timestamp(), + span_id: SpanId::generate(), + }), + PublicOplogEntry::SetSpanAttribute(SetSpanAttributeParams { + timestamp: timestamp(), + span_id: SpanId::generate(), + key: "http.status_code".to_string(), + value: PublicAttributeValue::String(StringAttributeValue { + value: "200".to_string(), + }), + }), + PublicOplogEntry::ChangePersistenceLevel(ChangePersistenceLevelParams { + timestamp: timestamp(), + persistence_level: PersistenceLevel::Smart, + }), + PublicOplogEntry::BeginRemoteTransaction(BeginRemoteTransactionParams { + timestamp: timestamp(), + transaction_id: golem_common::model::TransactionId::new("txn-1".to_string()), + }), + PublicOplogEntry::PreCommitRemoteTransaction(PreCommitRemoteTransactionParams { + timestamp: timestamp(), + begin_index: OplogIndex::from_u64(11), + }), + PublicOplogEntry::PreRollbackRemoteTransaction(PreRollbackRemoteTransactionParams { + timestamp: timestamp(), + begin_index: OplogIndex::from_u64(11), + }), + PublicOplogEntry::CommittedRemoteTransaction(CommittedRemoteTransactionParams { + timestamp: timestamp(), + begin_index: OplogIndex::from_u64(11), + }), + PublicOplogEntry::RolledBackRemoteTransaction(RolledBackRemoteTransactionParams { + timestamp: timestamp(), + begin_index: OplogIndex::from_u64(11), + }), + PublicOplogEntry::Snapshot(SnapshotParams { + timestamp: timestamp(), + data: raw_snapshot(), + }), + PublicOplogEntry::Snapshot(SnapshotParams { + timestamp: timestamp(), + data: json_snapshot(), + }), + PublicOplogEntry::Snapshot(SnapshotParams { + timestamp: timestamp(), + data: multipart_snapshot(), + }), + PublicOplogEntry::OplogProcessorCheckpoint(OplogProcessorCheckpointParams { + timestamp: timestamp(), + plugin: plugin(4), + target_agent_id: agent_id("target-agent"), + confirmed_up_to: OplogIndex::from_u64(20), + sending_up_to: OplogIndex::from_u64(21), + last_batch_start: OplogIndex::from_u64(19), + }), + PublicOplogEntry::SetRetryPolicy(SetRetryPolicyParams { + timestamp: timestamp(), + policy: retry_policy, + }), + PublicOplogEntry::RemoveRetryPolicy(RemoveRetryPolicyParams { + timestamp: timestamp(), + name: "generated-retry".to_string(), + }), + PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { + timestamp: timestamp(), + result: PublicAgentInvocationResult::SaveSnapshot(SaveSnapshotResultParameters { + snapshot: json_snapshot(), + }), + method_name: Some("generated-method".to_string()), + consumed_fuel: 101, + component_revision: ComponentRevision::new(4).unwrap(), + }), + PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { + timestamp: timestamp(), + invocation: PublicAgentInvocation::LoadSnapshot(LoadSnapshotParameters { + snapshot: multipart_snapshot(), + }), + }), + PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { + timestamp: timestamp(), + invocation: PublicAgentInvocation::SaveSnapshot(Empty {}), + }), + PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { + timestamp: timestamp(), + invocation: PublicAgentInvocation::ProcessOplogEntries(ProcessOplogEntriesParameters { + idempotency_key: IdempotencyKey::new("process-key".to_string()), + }), + }), + PublicOplogEntry::PendingAgentInvocation(PendingAgentInvocationParams { + timestamp: timestamp(), + invocation: PublicAgentInvocation::ManualUpdate(ManualUpdateParameters { + target_revision: ComponentRevision::new(5).unwrap(), + }), + }), + PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { + timestamp: timestamp(), + result: PublicAgentInvocationResult::LoadSnapshot(FallibleResultParameters { + error: Some("load failed".to_string()), + }), + method_name: Some("generated-method".to_string()), + consumed_fuel: 102, + component_revision: ComponentRevision::new(5).unwrap(), + }), + PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { + timestamp: timestamp(), + result: PublicAgentInvocationResult::ProcessOplogEntries( + ProcessOplogEntriesResultParameters { error: None }, + ), + method_name: Some("generated-method".to_string()), + consumed_fuel: 103, + component_revision: ComponentRevision::new(6).unwrap(), + }), + PublicOplogEntry::AgentInvocationFinished(AgentInvocationFinishedParams { + timestamp: timestamp(), + result: PublicAgentInvocationResult::ManualUpdate(Empty {}), + method_name: Some("generated-method".to_string()), + consumed_fuel: 104, + component_revision: ComponentRevision::new(7).unwrap(), + }), + PublicOplogEntry::PendingUpdate(PendingUpdateParams { + timestamp: timestamp(), + target_revision: ComponentRevision::new(8).unwrap(), + description: PublicUpdateDescription::Automatic(Empty {}), + }), + ] +} + +fn arb_build_result() -> OutputDocumentStrategy { + serialized_output( + any::().prop_map(|built| crate::model::text::action_result::BuildResult { built }), + ) +} + +fn arb_clean_result() -> OutputDocumentStrategy { + serialized_output( + any::() + .prop_map(|cleaned| crate::model::text::action_result::CleanResult { cleaned }), + ) +} + +fn arb_deploy_result() -> OutputDocumentStrategy { + serialized_output( + any::() + .prop_map(|deployed| crate::model::text::action_result::DeployResultView { deployed }), + ) +} + +fn arb_generate_bridge_result() -> OutputDocumentStrategy { + serialized_output(any::().prop_map(|generated| { + crate::model::text::action_result::GenerateBridgeResult { generated } + })) +} + +fn arb_agent_type_get_result() -> OutputDocumentStrategy { + serialized_output( + (arb_small_string(), arb_small_string(), arb_small_string()).prop_map( + |(agent_type, constructor, description)| crate::model::agent::view::AgentTypeView { + agent_type, + constructor, + description, + }, + ), + ) +} + +fn arb_agent_type_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_deployed_registered_agent_type(), 0..3) + .prop_map(|agent_types| crate::model::text::agent::AgentTypeListView { agent_types }), + ) +} + +fn arb_deployed_registered_agent_type() +-> BoxedStrategy { + ( + arb_agent_type(), + arb_uuid(), + arb_small_u64(), + arb_small_string(), + arb_uuid(), + arb_small_string(), + proptest::option::of(arb_small_string()), + ) + .prop_map( + |( + agent_type, + component_id, + component_revision, + component_name, + account_id, + account_email, + webhook_prefix_authority_and_path, + )| golem_common::model::agent::DeployedRegisteredAgentType { + agent_type, + implemented_by: golem_common::model::agent::RegisteredAgentTypeImplementer { + component_id: golem_common::model::component::ComponentId(component_id), + component_revision: golem_common::model::component::ComponentRevision::new( + component_revision, + ) + .expect("generated revision should be valid"), + component_name, + account_id: golem_common::model::account::AccountId(account_id), + account_email: golem_common::model::account::AccountEmail::new(account_email), + }, + webhook_prefix_authority_and_path, + }, + ) + .boxed() +} + +fn arb_agent_type() -> BoxedStrategy { + ( + arb_agent_type_name(), + arb_small_string(), + arb_small_string(), + arb_agent_constructor(), + proptest::collection::vec(arb_agent_method(), 1..3), + proptest::collection::vec(arb_agent_dependency(), 1..2), + arb_agent_mode(), + proptest::option::of(arb_http_mount_details()), + arb_snapshotting(), + proptest::collection::vec(arb_agent_config_declaration(), 1..3), + ) + .prop_map( + |( + type_name, + description, + source_language, + constructor, + methods, + dependencies, + mode, + http_mount, + snapshotting, + config, + )| { + let methods = if mode == golem_common::model::agent::AgentMode::Ephemeral { + methods + .into_iter() + .map(|mut method| { + method.read_only = None; + method + }) + .collect() + } else { + methods + }; + + golem_common::schema::agent::AgentTypeSchema { + type_name, + description, + source_language, + schema: golem_common::schema::SchemaGraph::empty(), + constructor, + methods, + dependencies, + mode, + http_mount, + snapshotting, + config, + } + }, + ) + .boxed() +} + +fn arb_agent_constructor() -> BoxedStrategy { + ( + proptest::option::of(arb_small_string()), + arb_small_string(), + proptest::option::of(arb_small_string()), + arb_input_schema(), + ) + .prop_map(|(name, description, prompt_hint, input_schema)| { + golem_common::schema::agent::AgentConstructorSchema { + name, + description, + prompt_hint, + input_schema, + } + }) + .boxed() +} + +fn arb_agent_method() -> BoxedStrategy { + ( + arb_small_string(), + arb_small_string(), + proptest::option::of(arb_small_string()), + arb_input_schema(), + arb_output_schema(), + proptest::collection::vec(arb_http_endpoint_details(), 0..3), + proptest::option::of(arb_read_only_config()), + ) + .prop_map( + |( + name, + description, + prompt_hint, + input_schema, + output_schema, + http_endpoint, + read_only, + )| { + golem_common::schema::agent::AgentMethodSchema { + name, + description, + prompt_hint, + input_schema, + output_schema, + http_endpoint, + read_only, + } + }, + ) + .boxed() +} + +fn arb_agent_dependency() -> BoxedStrategy { + ( + arb_small_string(), + proptest::option::of(arb_small_string()), + arb_agent_constructor(), + proptest::collection::vec(arb_agent_method(), 1..2), + ) + .prop_map(|(type_name, description, constructor, methods)| { + golem_common::schema::agent::AgentDependencySchema { + type_name, + description, + schema: golem_common::schema::SchemaGraph::empty(), + constructor, + methods, + } + }) + .boxed() +} + +fn arb_agent_mode() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::agent::AgentMode::Durable), + Just(golem_common::model::agent::AgentMode::Ephemeral), + ] + .boxed() +} + +/// Structural [`SchemaType`] strategy covering every schema-native type +/// case. Derived from the shared `golem-schema` graph strategy (we take the +/// generated graph's root), so new `SchemaType` variants are exercised +/// automatically. +fn arb_schema_type() -> BoxedStrategy { + golem_common::schema::proptest_strategies::schema_graph_strategy() + .prop_map(|graph| graph.root) + .boxed() +} + +fn arb_metadata_envelope() -> BoxedStrategy { + ( + proptest::option::of(arb_small_string()), + proptest::collection::vec(arb_small_string(), 0..2), + proptest::collection::vec(arb_small_string(), 0..2), + proptest::option::of(arb_small_string()), + proptest::option::of(prop_oneof![ + Just(golem_common::schema::Role::Multimodal), + Just(golem_common::schema::Role::UnstructuredText), + Just(golem_common::schema::Role::UnstructuredBinary), + arb_small_string().prop_map(golem_common::schema::Role::Other), + ]), + ) + .prop_map(|(doc, aliases, examples, deprecated, role)| { + golem_common::schema::MetadataEnvelope { + doc, + aliases, + examples, + deprecated, + role, + } + }) + .boxed() +} + +fn arb_field_source() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::schema::FieldSource::UserSupplied), + Just(golem_common::schema::FieldSource::AutoInjected( + golem_common::schema::AutoInjectedKind::Principal, + )), + ] + .boxed() +} + +fn arb_named_field() -> BoxedStrategy { + ( + arb_small_string(), + arb_field_source(), + arb_schema_type(), + arb_metadata_envelope(), + ) + .prop_map( + |(name, source, schema, metadata)| golem_common::schema::NamedField { + name, + source, + schema, + metadata, + }, + ) + .boxed() +} + +fn arb_input_schema() -> BoxedStrategy { + proptest::collection::vec(arb_named_field(), 0..3) + .prop_map(golem_common::schema::agent::InputSchema::Parameters) + .boxed() +} + +fn arb_output_schema() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::schema::agent::OutputSchema::Unit), + arb_schema_type().prop_map(|schema_type| { + golem_common::schema::agent::OutputSchema::Single(Box::new(schema_type)) + }), + ] + .boxed() +} + +fn arb_read_only_config() -> BoxedStrategy { + (arb_cache_policy(), any::()) + .prop_map( + |(cache_policy, uses_principal)| golem_common::model::agent::ReadOnlyConfig { + cache_policy, + uses_principal, + }, + ) + .boxed() +} + +fn arb_cache_policy() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::agent::CachePolicy::NoCache( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::CachePolicy::UntilWrite( + golem_common::model::Empty {} + )), + arb_small_u64().prop_map(|duration_nanos| { + golem_common::model::agent::CachePolicy::Ttl( + golem_common::model::agent::CachePolicyTtl { duration_nanos }, + ) + }), + ] + .boxed() +} + +fn arb_snapshotting() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::agent::Snapshotting::Disabled( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::Snapshotting::Enabled( + golem_common::model::agent::SnapshottingConfig::Default(golem_common::model::Empty {},) + )), + arb_small_u64().prop_map(|duration_nanos| { + golem_common::model::agent::Snapshotting::Enabled( + golem_common::model::agent::SnapshottingConfig::Periodic( + golem_common::model::agent::SnapshottingPeriodic { duration_nanos }, + ), + ) + }), + any::().prop_map(|count| { + golem_common::model::agent::Snapshotting::Enabled( + golem_common::model::agent::SnapshottingConfig::EveryNInvocation( + golem_common::model::agent::SnapshottingEveryNInvocation { count }, + ), + ) + }), + ] + .boxed() +} + +fn arb_agent_config_declaration() +-> BoxedStrategy { + ( + prop_oneof![ + Just(golem_common::model::agent::AgentConfigSource::Local), + Just(golem_common::model::agent::AgentConfigSource::Secret), + ], + proptest::collection::vec(arb_small_string(), 1..3), + arb_schema_type(), + ) + .prop_map(|(source, path, value_type)| { + golem_common::schema::agent::AgentConfigDeclarationSchema { + source, + path, + value_type, + } + }) + .boxed() +} + +fn arb_http_mount_details() -> BoxedStrategy { + ( + proptest::collection::vec(arb_path_segment(), 0..2), + proptest::option::of( + any::() + .prop_map(|required| golem_common::model::agent::AgentHttpAuthDetails { required }), + ), + any::(), + proptest::collection::vec(arb_small_string(), 0..2), + proptest::collection::vec(arb_path_segment(), 0..2), + ) + .prop_map( + |(path_prefix, auth_details, phantom_agent, allowed_patterns, webhook_suffix)| { + golem_common::model::agent::HttpMountDetails { + path_prefix, + auth_details, + phantom_agent, + cors_options: golem_common::model::agent::CorsOptions { allowed_patterns }, + webhook_suffix, + } + }, + ) + .boxed() +} + +fn arb_http_endpoint_details() -> BoxedStrategy { + ( + arb_http_method(), + proptest::collection::vec(arb_path_segment(), 0..2), + proptest::collection::vec( + (arb_small_string(), arb_small_string()).prop_map(|(header_name, variable_name)| { + golem_common::model::agent::HeaderVariable { + header_name, + variable_name, + } + }), + 0..2, + ), + proptest::collection::vec( + (arb_small_string(), arb_small_string()).prop_map( + |(query_param_name, variable_name)| golem_common::model::agent::QueryVariable { + query_param_name, + variable_name, + }, + ), + 0..2, + ), + proptest::option::of( + any::() + .prop_map(|required| golem_common::model::agent::AgentHttpAuthDetails { required }), + ), + proptest::collection::vec(arb_small_string(), 0..2), + ) + .prop_map( + |( + http_method, + path_suffix, + header_vars, + query_vars, + auth_details, + allowed_patterns, + )| { + golem_common::model::agent::HttpEndpointDetails { + http_method, + path_suffix, + header_vars, + query_vars, + auth_details, + cors_options: golem_common::model::agent::CorsOptions { allowed_patterns }, + } + }, + ) + .boxed() +} + +fn arb_http_method() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::agent::HttpMethod::Get( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Head( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Post( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Put( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Delete( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Connect( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Options( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Trace( + golem_common::model::Empty {} + )), + Just(golem_common::model::agent::HttpMethod::Patch( + golem_common::model::Empty {} + )), + arb_small_string().prop_map(|value| { + golem_common::model::agent::HttpMethod::Custom( + golem_common::model::agent::CustomHttpMethod { value }, + ) + }), + ] + .boxed() +} + +fn arb_path_segment() -> BoxedStrategy { + prop_oneof![ + arb_small_string().prop_map(|value| { + golem_common::model::agent::PathSegment::Literal( + golem_common::model::agent::LiteralSegment { value }, + ) + }), + arb_small_string().prop_map(|variable_name| { + golem_common::model::agent::PathSegment::PathVariable( + golem_common::model::agent::PathVariable { variable_name }, + ) + }), + arb_small_string().prop_map(|variable_name| { + golem_common::model::agent::PathSegment::RemainingPathVariable( + golem_common::model::agent::PathVariable { variable_name }, + ) + }), + prop_oneof![ + Just(golem_common::model::agent::SystemVariable::AgentType), + Just(golem_common::model::agent::SystemVariable::AgentVersion), + ] + .prop_map(|value| { + golem_common::model::agent::PathSegment::SystemVariable( + golem_common::model::agent::SystemVariableSegment { value }, + ) + }), + ] + .boxed() +} + +fn arb_agent_files_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_file_node(), 0..6) + .prop_map(|nodes| crate::model::text::agent::AgentFilesView { nodes }), + ) +} + +fn arb_file_node() -> BoxedStrategy { + ( + arb_small_string(), + arb_small_string(), + arb_timestamp_string(), + arb_timestamp_string(), + arb_small_u64(), + ) + .prop_map(|(name, last_modified, kind, permissions, size)| { + crate::model::text::agent::FileNodeView { + name, + last_modified, + kind, + permissions, + size, + } + }) + .boxed() +} + +fn arb_agent_get_result() -> OutputDocumentStrategy { + serialized_output( + (arb_agent_metadata_view(), any::()).prop_map(|(metadata, precise)| { + crate::model::text::agent::AgentGetView { metadata, precise } + }), + ) +} + +fn arb_agent_invoke_result() -> OutputDocumentStrategy { + ( + arb_small_string(), + prop_oneof![Just(0u8), Just(1u8)], + arb_value_and_type(), + arb_format_string(), + ) + .prop_map(|(idempotency_key, shape, result_json, result_format)| { + to_structured_output_value(crate::model::invoke_result_view::InvokeResultView { + idempotency_key, + result_json: (shape == 1).then_some(result_json), + result: None, + result_format: (shape != 0).then_some(result_format.to_string()), + is_void_result: shape == 0, + }) + .expect("generated invoke result should serialize") + }) + .boxed() +} + +/// Structural [`TypedSchemaValue`] strategy covering every schema-native +/// value and type case. Reuses the shared `golem-schema` strategy so new +/// `SchemaValue` / `SchemaType` / `SchemaGraph` shapes are exercised +/// automatically. +fn arb_value_and_type() -> BoxedStrategy { + golem_common::schema::proptest_strategies::typed_schema_value_strategy().boxed() +} + +fn arb_agent_list_result() -> OutputDocumentStrategy { + ( + proptest::collection::vec(arb_agent_metadata_view(), 0..5), + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), + ) + .prop_map( + |(agents, cursors)| crate::model::agent::AgentsMetadataResponseView { agents, cursors }, + ) + .prop_map(|output| { + to_structured_output_value(output).expect("generated DTO should serialize") + }) + .boxed() +} + +fn arb_agent_new_result() -> OutputDocumentStrategy { + serialized_output((arb_small_string(), arb_small_string()).prop_map( + |(component_name, agent_id)| crate::model::text::agent::AgentCreateView { + component_name: golem_common::model::component::ComponentName(component_name), + agent_id: crate::model::agent::RawAgentId(agent_id), + }, + )) +} + +fn arb_agent_oplog_result() -> OutputDocumentStrategy { + serialized_output( + ( + arb_small_u64(), + prop_oneof![ + proptest::sample::select(sample_public_oplog_entries()), + arb_typed_value_oplog_entry(), + ], + ) + .prop_map( + |(index, entry)| crate::model::text::agent::AgentOplogEntryView { index, entry }, + ), + ) +} + +/// Oplog entry carrying a structurally-comprehensive [`TypedSchemaValue`] +/// (full `SchemaGraph` + `SchemaValue`). This is the non-masked path that +/// exercises every schema-native value/graph case against the schema's +/// `TypedSchemaValue` definition, so new variants are caught automatically. +fn arb_typed_value_oplog_entry() -> BoxedStrategy { + golem_common::schema::proptest_strategies::typed_schema_value_strategy() + .prop_map(|response| { + golem_common::model::oplog::PublicOplogEntry::End( + golem_common::model::oplog::public_oplog_entry::EndParams { + timestamp: golem_common::model::Timestamp::from(0), + start_index: golem_common::model::oplog::OplogIndex::from_u64(1), + response: Some(response), + forced_commit: false, + }, + ) + }) + .boxed() +} + +fn arb_agent_stream_event() -> OutputDocumentStrategy { + serialized_output( + ( + arb_timestamp(), + arb_agent_stream_event_kind(), + arb_small_string(), + arb_small_string(), + arb_small_string(), + proptest::option::of(arb_small_string()), + proptest::option::of(arb_small_string()), + proptest::option::of(arb_small_u64()), + proptest::option::of(arb_small_string()), + ) + .prop_map( + |( + timestamp, + kind, + level, + context, + message, + function_name, + idempotency_key, + number_of_missed_messages, + error, + )| crate::model::agent::stream::AgentStreamEvent { + timestamp, + kind, + level, + context, + message, + function_name, + idempotency_key, + number_of_missed_messages, + error, + }, + ), + ) +} + +fn arb_agent_stream_event_kind() -> BoxedStrategy +{ + prop_oneof![ + Just(crate::model::agent::stream::AgentStreamEventKind::Log), + Just(crate::model::agent::stream::AgentStreamEventKind::Stdout), + Just(crate::model::agent::stream::AgentStreamEventKind::Stderr), + Just(crate::model::agent::stream::AgentStreamEventKind::StreamClosed), + Just(crate::model::agent::stream::AgentStreamEventKind::StreamError), + Just(crate::model::agent::stream::AgentStreamEventKind::InvocationStarted), + Just(crate::model::agent::stream::AgentStreamEventKind::InvocationFinished), + Just(crate::model::agent::stream::AgentStreamEventKind::MissedMessages), + ] + .boxed() +} + +fn arb_agent_update_result() -> OutputDocumentStrategy { + serialized_output( + ( + proptest::collection::vec(arb_agent_update_meta(), 0..5), + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), + ) + .prop_map( + |(agents, errors)| crate::model::deploy::TryUpdateAllWorkersResult { + agents, + errors, + }, + ), + ) +} + +/// Shared field generator for the two identical revision-transition metas +/// (`AgentUpdateMeta` / `AgentRedeploymentMeta`). +fn arb_agent_transition_fields() -> BoxedStrategy<( + golem_common::model::component::ComponentName, + crate::model::agent::RawAgentId, + golem_common::model::component::ComponentRevision, + golem_common::model::component::ComponentRevision, + Option, + Option, +)> { + ( + arb_small_string(), + arb_small_string(), + arb_small_u64(), + arb_small_u64(), + proptest::option::of(arb_small_string()), + proptest::option::of(arb_small_string()), + ) + .prop_map( + |(component_name, agent_id, from_revision, revision, from_version, version)| { + ( + golem_common::model::component::ComponentName(component_name), + crate::model::agent::RawAgentId(agent_id), + golem_common::model::component::ComponentRevision::new(from_revision) + .expect("generated revision should be valid"), + golem_common::model::component::ComponentRevision::new(revision) + .expect("generated revision should be valid"), + from_version, + version, + ) + }, + ) + .boxed() +} + +fn arb_agent_update_meta() -> BoxedStrategy { + arb_agent_transition_fields() + .prop_map( + |(component_name, agent_id, from_revision, revision, from_version, version)| { + crate::model::deploy::AgentUpdateMeta { + component_name, + agent_id, + from_revision, + revision, + from_version, + version, + } + }, + ) + .boxed() +} + +fn arb_agent_redeployment_meta() +-> BoxedStrategy { + arb_agent_transition_fields() + .prop_map( + |(component_name, agent_id, from_revision, revision, from_version, version)| { + crate::model::text::action_result::AgentRedeploymentMeta { + component_name, + agent_id, + from_revision, + revision, + from_version, + version, + } + }, + ) + .boxed() +} + +fn arb_agent_deletion_meta() -> BoxedStrategy +{ + (arb_small_string(), arb_small_string()) + .prop_map(|(component_name, agent_id)| { + crate::model::text::action_result::AgentDeletionMeta { + component_name: golem_common::model::component::ComponentName(component_name), + agent_id: crate::model::agent::RawAgentId(agent_id), + } + }) + .boxed() +} + +fn arb_agent_metadata_view() -> BoxedStrategy { + ( + ( + arb_small_string(), + arb_small_string(), + arb_small_string(), + arb_small_string(), + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..4), + proptest::collection::vec(arb_agent_config_entry_dto(), 0..4), + proptest::collection::vec(arb_agent_config_entry_dto(), 0..4), + arb_agent_status(), + ), + ( + arb_small_u64(), + any::(), + arb_small_u64(), + proptest::collection::vec(arb_update_record(), 0..4), + arb_timestamp_string(), + proptest::option::of(arb_small_string()), + arb_small_u64(), + arb_small_u64(), + proptest::collection::btree_map( + arb_small_string(), + arb_agent_resource_description(), + 0..4, + ), + ), + ) + .prop_map(|(left, right)| { + let ( + component_name, + agent_id, + created_by, + environment_id, + env, + default_env, + config, + default_config, + status, + ) = left; + let ( + component_revision, + retry_count, + pending_invocation_count, + updates, + created_at, + last_error, + component_size, + total_linear_memory_size, + exported_resource_instances, + ) = right; + + crate::model::agent::AgentMetadataView { + component_name: golem_common::model::component::ComponentName(component_name), + agent_id: crate::model::agent::RawAgentId(agent_id), + created_by: golem_common::model::account::AccountId( + uuid::Uuid::parse_str(&created_by).expect("generated UUID should parse"), + ), + environment_id: golem_common::model::environment::EnvironmentId( + uuid::Uuid::parse_str(&environment_id).expect("generated UUID should parse"), + ), + env: env.into_iter().collect(), + default_env: default_env.into_iter().collect(), + config, + default_config, + status, + component_revision: golem_common::model::component::ComponentRevision::new( + component_revision, + ) + .expect("generated revision should be valid"), + retry_count, + pending_invocation_count, + updates, + created_at: created_at + .parse() + .expect("generated timestamp should parse"), + last_error, + component_size, + total_linear_memory_size, + exported_resource_instances: exported_resource_instances.into_iter().collect(), + source_language: crate::agent_id_display::SourceLanguage::default(), + secret_config_paths: BTreeSet::new(), + } + }) + .boxed() +} + +fn arb_agent_config_entry_dto() -> BoxedStrategy { + ( + proptest::collection::vec(arb_small_string(), 1..4), + arb_json_value(2), + ) + .prop_map( + |(path, value)| golem_common::model::worker::AgentConfigEntryDto { + path, + value: golem_common::base_model::json::NormalizedJsonValue(value), + }, + ) + .boxed() +} + +fn arb_update_record() -> BoxedStrategy { + prop_oneof![ + (arb_timestamp(), arb_small_u64()).prop_map(|(timestamp, target_revision)| { + golem_common::model::worker::UpdateRecord::PendingUpdate( + golem_common::model::worker::PendingUpdate { + timestamp, + target_revision: golem_common::model::component::ComponentRevision::new( + target_revision, + ) + .expect("generated revision should be valid"), + }, + ) + }), + (arb_timestamp(), arb_small_u64()).prop_map(|(timestamp, target_revision)| { + golem_common::model::worker::UpdateRecord::SuccessfulUpdate( + golem_common::model::worker::SuccessfulUpdate { + timestamp, + target_revision: golem_common::model::component::ComponentRevision::new( + target_revision, + ) + .expect("generated revision should be valid"), + }, + ) + }), + ( + arb_timestamp(), + arb_small_u64(), + proptest::option::of(arb_small_string()), + ) + .prop_map(|(timestamp, target_revision, details)| { + golem_common::model::worker::UpdateRecord::FailedUpdate( + golem_common::model::worker::FailedUpdate { + timestamp, + target_revision: golem_common::model::component::ComponentRevision::new( + target_revision, + ) + .expect("generated revision should be valid"), + details, + }, + ) + }), + ] + .boxed() +} + +fn arb_agent_resource_description() -> BoxedStrategy +{ + (arb_timestamp(), arb_small_string(), arb_small_string()) + .prop_map(|(created_at, resource_owner, resource_name)| { + golem_common::model::AgentResourceDescription { + created_at, + resource_owner, + resource_name, + } + }) + .boxed() +} + +fn arb_agent_delete_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(deleted, agent)| { + crate::model::text::action_result::AgentDeleteResult { + deleted, + agent_id: agent, + } + }), + ) +} + +fn arb_agent_file_contents_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + arb_small_string(), + arb_small_string(), + arb_small_string(), + arb_small_u64(), + ) + .prop_map(|(saved, agent, path, output_path, bytes)| { + crate::model::text::action_result::AgentFileContentsResult { + saved, + agent_id: agent, + path, + output_path: output_path.into(), + bytes: bytes as usize, + } + }), + ) +} + +fn arb_agent_interrupt_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(interrupted, agent)| { + crate::model::text::action_result::AgentInterruptResult { + interrupted, + agent_id: agent, + } + }), + ) +} + +fn arb_agent_resume_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(resumed, agent)| { + crate::model::text::action_result::AgentResumeResult { + resumed, + agent_id: agent, + } + }), + ) +} + +fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(simulated, agent)| { + crate::model::text::action_result::AgentSimulateCrashResult { + simulated, + agent_id: agent, + } + }), + ) +} + +fn arb_account_delete_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(deleted, account_id)| { + crate::model::text::account::AccountDeleteResult { + deleted, + account_id: golem_common::model::account::AccountId( + uuid::Uuid::parse_str(&account_id).expect("generated UUID should parse"), + ), + } + }), + ) +} + +fn arb_account_get_result() -> OutputDocumentStrategy { + serialized_output(arb_account().prop_map(crate::model::text::account::AccountGetView)) +} + +fn arb_account_new_result() -> OutputDocumentStrategy { + serialized_output(arb_account().prop_map(crate::model::text::account::AccountNewView)) +} + +fn arb_account_update_result() -> OutputDocumentStrategy { + serialized_output(arb_account().prop_map(crate::model::text::account::AccountUpdateView)) +} + +fn arb_account() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_small_string(), + arb_small_string(), + arb_uuid(), + proptest::collection::vec(arb_account_role(), 0..4), + arb_uuid(), + ) + .prop_map( + |(id, revision, name, email, plan_id, roles, account_root_card_id)| { + golem_client::model::Account { + id: golem_common::model::account::AccountId(id), + revision: golem_common::model::account::AccountRevision::new(revision) + .expect("generated revision should be valid"), + name, + email: golem_common::model::account::AccountEmail::new(email), + plan_id: golem_common::model::plan::PlanId(plan_id), + roles, + account_root_card_id: golem_common::model::card::CardId(account_root_card_id), + } + }, + ) + .boxed() +} + +fn arb_account_role() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::auth::AccountRole::Admin), + Just(golem_common::model::auth::AccountRole::MarketingAdmin), + Just(golem_common::model::auth::AccountRole::BuiltinPluginOwner), + ] + .boxed() +} + +fn arb_permission_share_delete_result() -> OutputDocumentStrategy { + serialized_output((any::(), arb_small_string()).prop_map( + |(deleted, permission_share_id)| crate::model::text::account::PermissionShareDeleteResult { + deleted, + permission_share_id: golem_common::model::permission_share::PermissionShareId( + uuid::Uuid::parse_str(&permission_share_id).expect("generated UUID should parse"), + ), + }, + )) +} + +fn arb_permission_share_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_permission_share().prop_map(crate::model::text::account::PermissionShareGetView), + ) +} + +fn arb_permission_share_new_result() -> OutputDocumentStrategy { + serialized_output( + arb_permission_share().prop_map(crate::model::text::account::PermissionShareNewView), + ) +} + +fn arb_permission_share_update_result() -> OutputDocumentStrategy { + serialized_output( + arb_permission_share().prop_map(crate::model::text::account::PermissionShareUpdateView), + ) +} + +fn arb_permission_share_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_permission_share(), 0..5).prop_map(|permission_shares| { + crate::model::text::account::PermissionShareListView { permission_shares } + }), + ) +} + +fn arb_permission_share() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_uuid(), + arb_uuid(), + arb_small_string(), + proptest::option::of(arb_uuid()), + arb_permission_share_data(), + ) + .prop_map( + |(id, revision, owner_account_id, target_account_id, name, current_card_id, data)| { + golem_client::model::PermissionShare { + id: golem_common::model::permission_share::PermissionShareId(id), + revision: golem_common::model::permission_share::PermissionShareRevision::new( + revision, + ) + .expect("generated revision should be valid"), + owner_account_id: golem_common::model::account::AccountId(owner_account_id), + target_account_id: golem_common::model::account::AccountId(target_account_id), + name: golem_common::model::permission_share::PermissionShareName(name), + current_card_id: current_card_id.map(golem_common::model::card::CardId), + data, + } + }, + ) + .boxed() +} + +fn arb_permission_share_data() +-> BoxedStrategy { + ( + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + ) + .prop_map( + |(lower_positive, lower_negative, upper_positive, upper_negative)| { + golem_common::model::permission_share::PermissionShareData { + lower_positive, + lower_negative, + upper_positive, + upper_negative, + } + }, + ) + .boxed() +} + +fn arb_card_get_result() -> OutputDocumentStrategy { + serialized_output(arb_stored_card().prop_map(crate::model::text::card::CardGetView)) +} + +fn arb_card_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_stored_card(), 0..5) + .prop_map(|cards| crate::model::text::card::CardListView { cards }), + ) +} + +fn arb_card_revoke_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_uuid(), 0..5).prop_map(|revoked_card_ids| { + crate::model::text::card::CardRevokeResult { revoked_card_ids } + }), + ) +} + +fn arb_stored_card() -> BoxedStrategy { + prop_oneof![ + arb_card().prop_map(golem_client::model::StoredCard::Concrete), + arb_client_polymorphic_card().prop_map(golem_client::model::StoredCard::Polymorphic), + ] + .boxed() +} + +fn arb_card() -> BoxedStrategy { + ( + arb_uuid(), + proptest::collection::vec(arb_uuid(), 0..3), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + arb_datetime(), + proptest::option::of(arb_datetime()), + proptest::bool::ANY, + proptest::option::of(arb_card_managed_by()), + ) + .prop_map( + |( + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at, + expires_at, + system_card, + managed_by, + )| golem_client::model::Card { + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at, + expires_at, + system_card, + managed_by, + }, + ) + .boxed() +} + +fn arb_client_polymorphic_card() -> BoxedStrategy { + ( + arb_uuid(), + proptest::collection::vec(arb_uuid(), 0..3), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + proptest::collection::vec(arb_small_string(), 0..4), + arb_datetime(), + proptest::option::of(arb_datetime()), + proptest::bool::ANY, + ) + .prop_map( + |( + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at, + expires_at, + system_card, + )| golem_client::model::PolymorphicCard { + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at, + expires_at, + system_card, + }, + ) + .boxed() +} + +fn arb_card_managed_by() -> BoxedStrategy { + prop_oneof![ + arb_uuid().prop_map(|account_id| { + golem_client::model::CardManagedBy::AccountRoot( + golem_client::model::CardManagedByAccountRoot { account_id }, + ) + }), + arb_uuid().prop_map(|environment_id| { + golem_client::model::CardManagedBy::EnvironmentDefault( + golem_client::model::CardManagedByEnvironmentDefault { environment_id }, + ) + }), + arb_uuid().prop_map(|permission_share_id| { + golem_client::model::CardManagedBy::PermissionShare( + golem_client::model::CardManagedByPermissionShare { + permission_share_id, + }, + ) + }), + (arb_uuid(), arb_small_u64(), arb_small_string()).prop_map( + |(component_id, component_revision, agent_type)| { + golem_client::model::CardManagedBy::AgentInitial( + golem_client::model::CardManagedByAgentInitial { + component_id, + component_revision, + agent_type, + }, + ) + } + ), + ] + .boxed() +} + +fn arb_agent_cancel_invocation_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string(), arb_small_string()).prop_map( + |(canceled, agent, idempotency_key)| { + crate::model::text::action_result::AgentCancelInvocationResult { + canceled, + agent_id: agent, + idempotency_key, + } + }, + ), + ) +} + +fn arb_agent_delete_all_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + proptest::collection::vec(arb_agent_deletion_meta(), 0..5), + ) + .prop_map(|(deleted, agents)| { + crate::model::text::action_result::AgentDeleteAllResult { deleted, agents } + }), + ) +} + +fn arb_agent_redeploy_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + proptest::collection::vec(arb_agent_redeployment_meta(), 0..5), + ) + .prop_map(|(redeployed, agents)| { + crate::model::text::action_result::AgentRedeployResult { redeployed, agents } + }), + ) +} + +fn arb_agent_revert_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + arb_small_string(), + proptest::option::of(arb_small_u64()), + proptest::option::of(arb_small_u64()), + ) + .prop_map( + |(reverted, agent, last_oplog_index, number_of_invocations)| { + crate::model::text::action_result::AgentRevertResult { + reverted, + agent_id: agent, + last_oplog_index, + number_of_invocations, + } + }, + ), + ) +} + +fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + arb_small_string(), + arb_small_string(), + 0i32..1000, + ) + .prop_map(|(activated, agent, plugin, priority)| { + crate::model::text::action_result::AgentPluginToggleResult { + activated, + agent_id: agent, + plugin, + priority, + } + }), + ) +} + +fn arb_token_delete_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(deleted, token_id)| { + crate::model::text::token::TokenDeleteResult { + deleted, + token_id: golem_common::model::auth::TokenId( + uuid::Uuid::parse_str(&token_id).expect("generated UUID should parse"), + ), + } + }), + ) +} + +fn arb_token_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_token(), 0..5) + .prop_map(|tokens| crate::model::text::token::TokenListView { tokens }), + ) +} + +fn arb_token_new_result() -> OutputDocumentStrategy { + serialized_output(arb_token_with_secret().prop_map(crate::model::text::token::TokenNewView)) +} + +fn arb_token() -> BoxedStrategy { + (arb_uuid(), arb_uuid(), arb_datetime(), arb_datetime()) + .prop_map( + |(id, account_id, created_at, expires_at)| golem_common::model::auth::Token { + id: golem_common::model::auth::TokenId(id), + account_id: golem_common::model::account::AccountId(account_id), + created_at, + expires_at, + }, + ) + .boxed() +} + +fn arb_token_with_secret() -> BoxedStrategy { + ( + arb_uuid(), + arb_uuid(), + arb_small_string(), + arb_datetime(), + arb_datetime(), + ) + .prop_map(|(id, account_id, secret, created_at, expires_at)| { + golem_common::model::auth::TokenWithSecret { + id: golem_common::model::auth::TokenId(id), + secret: golem_common::model::auth::TokenSecret::trusted(secret), + account_id: golem_common::model::account::AccountId(account_id), + created_at, + expires_at, + } + }) + .boxed() +} + +fn arb_api_domain_delete_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string(), arb_small_string()).prop_map( + |(deleted, domain, id)| { + crate::model::text::http_api_domain::DomainRegistrationDeleteResult { + deleted, + domain: golem_common::model::domain_registration::Domain(domain), + id: golem_common::model::domain_registration::DomainRegistrationId( + uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), + ), + } + }, + ), + ) +} + +fn arb_api_domain_register_result() -> OutputDocumentStrategy { + serialized_output( + arb_domain_registration() + .prop_map(crate::model::text::http_api_domain::DomainRegistrationNewView), + ) +} + +fn arb_api_domain_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_domain_registration(), 0..5).prop_map(|domains| { + crate::model::text::http_api_domain::HttpApiDomainListView { domains } + }), + ) +} + +fn arb_domain_registration() +-> BoxedStrategy { + (arb_uuid(), arb_uuid(), arb_small_string()) + .prop_map(|(id, environment_id, domain)| { + golem_common::model::domain_registration::DomainRegistration { + id: golem_common::model::domain_registration::DomainRegistrationId(id), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + domain: golem_common::model::domain_registration::Domain(domain), + } + }) + .boxed() +} + +fn arb_api_deployment_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_http_api_deployment() + .prop_map(crate::model::text::http_api_deployment::HttpApiDeploymentGetView), + ) +} + +fn arb_api_deployment_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_http_api_deployment(), 0..5).prop_map(|deployments| { + crate::model::text::http_api_deployment::HttpApiDeploymentListView { deployments } + }), + ) +} + +fn arb_http_api_deployment() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_uuid(), + arb_small_string(), + proptest::collection::btree_map( + arb_agent_type_name(), + arb_http_api_deployment_agent_options(), + 0..4, + ), + arb_small_string(), + arb_small_string(), + arb_datetime(), + ) + .prop_map( + |( + id, + revision, + environment_id, + domain, + agents, + webhooks_prefix, + openapi_endpoint_prefix, + created_at, + )| { + golem_client::model::HttpApiDeployment { + id: golem_common::model::http_api_deployment::HttpApiDeploymentId(id), + revision: + golem_common::model::http_api_deployment::HttpApiDeploymentRevision::new( + revision, + ) + .expect("generated revision should be valid"), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + domain: golem_common::model::domain_registration::Domain(domain.clone()), + hash: golem_common::model::diff::Hash::new(blake3::hash(domain.as_bytes())), + agents, + webhooks_prefix, + openapi_endpoint_prefix, + created_at, + } + }, + ) + .boxed() +} + +fn arb_agent_type_name() -> BoxedStrategy { + arb_small_string() + .prop_map(golem_common::model::agent::AgentTypeName) + .boxed() +} + +fn arb_http_api_deployment_agent_options() +-> BoxedStrategy { + proptest::option::of(prop_oneof![ + arb_small_string().prop_map(|header_name| { + golem_common::model::http_api_deployment::HttpApiDeploymentAgentSecurity::TestSessionHeader( + golem_common::model::http_api_deployment::TestSessionHeaderAgentSecurity { header_name }, + ) + }), + arb_small_string().prop_map(|security_scheme| { + golem_common::model::http_api_deployment::HttpApiDeploymentAgentSecurity::SecurityScheme( + golem_common::model::http_api_deployment::SecuritySchemeAgentSecurity { + security_scheme: golem_common::model::security_scheme::SecuritySchemeName(security_scheme), + }, + ) + }), + ]) + .prop_map(|security| { + golem_common::model::http_api_deployment::HttpApiDeploymentAgentOptions { + security, + } + }) + .boxed() +} + +fn arb_api_security_scheme_create_result() -> OutputDocumentStrategy { + serialized_output( + arb_security_scheme() + .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeCreateView), + ) +} + +fn arb_api_security_scheme_delete_result() -> OutputDocumentStrategy { + serialized_output( + arb_security_scheme() + .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeDeleteView), + ) +} + +fn arb_api_security_scheme_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_security_scheme() + .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeGetView), + ) +} + +fn arb_api_security_scheme_update_result() -> OutputDocumentStrategy { + serialized_output( + arb_security_scheme() + .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeUpdateView), + ) +} + +fn arb_api_security_scheme_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_security_scheme(), 0..5).prop_map(|security_schemes| { + crate::model::text::http_api_security::HttpSecuritySchemeListView { security_schemes } + }), + ) +} + +fn arb_security_scheme() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_small_string(), + arb_uuid(), + arb_security_scheme_provider(), + arb_small_string(), + arb_url_string(), + proptest::collection::vec(arb_small_string(), 0..5), + ) + .prop_map( + |( + id, + revision, + name, + environment_id, + provider_type, + client_id, + redirect_url, + scopes, + )| { + golem_client::model::SecuritySchemeDto { + id: golem_common::model::security_scheme::SecuritySchemeId(id), + revision: golem_common::model::security_scheme::SecuritySchemeRevision::new( + revision, + ) + .expect("generated revision should be valid"), + name: golem_common::model::security_scheme::SecuritySchemeName(name), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + provider_type, + client_id, + redirect_url, + scopes, + } + }, + ) + .boxed() +} + +fn arb_security_scheme_provider() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::security_scheme::Provider::Google( + golem_common::model::Empty {} + )), + Just(golem_common::model::security_scheme::Provider::Facebook( + golem_common::model::Empty {} + )), + Just(golem_common::model::security_scheme::Provider::Microsoft( + golem_common::model::Empty {} + )), + Just(golem_common::model::security_scheme::Provider::Gitlab( + golem_common::model::Empty {} + )), + (arb_small_string(), arb_url_string()).prop_map(|(name, issuer_url)| { + golem_common::model::security_scheme::Provider::Custom( + golem_common::model::security_scheme::CustomProvider { name, issuer_url }, + ) + }), + ] + .boxed() +} + +fn arb_new_app_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string(), arb_small_string()).prop_map( + |(created, application_name, application_dir)| { + crate::model::text::action_result::NewAppResult { + created, + application_name, + application_dir: PathBuf::from(application_dir), + } + }, + ), + ) +} + +fn arb_template_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_template_description(), 0..5) + .prop_map(|templates| crate::model::text::template::TemplateListView { templates }), + ) +} + +fn arb_template_description() -> BoxedStrategy { + (arb_small_string(), arb_guest_language(), arb_small_string()) + .prop_map( + |(name, language, description)| crate::model::TemplateDescription { + name, + language, + description, + }, + ) + .boxed() +} + +fn arb_guest_language() -> BoxedStrategy { + prop_oneof![ + Just(crate::model::GuestLanguage::TypeScript), + Just(crate::model::GuestLanguage::Rust), + Just(crate::model::GuestLanguage::Scala), + Just(crate::model::GuestLanguage::MoonBit) + ] + .boxed() +} + +fn arb_component_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_component_view().prop_map(crate::model::text::component::ComponentGetView), + ) +} + +fn arb_component_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_component_view(), 0..5) + .prop_map(|components| crate::model::text::component::ComponentListView { components }), + ) +} + +fn arb_component_manifest_trace_result() -> OutputDocumentStrategy { + serialized_output( + (arb_small_string(), arb_component_layer_properties()).prop_map( + |(component_name, properties)| { + crate::model::text::component::ComponentManifestTraceView { + component_name: golem_common::model::component::ComponentName(component_name), + properties, + } + }, + ), + ) +} + +fn arb_component_layer_properties() -> BoxedStrategy { + ( + ( + arb_component_layer_id(), + arb_component_layer_id(), + proptest::option::of(arb_small_string()), + proptest::option::of(arb_small_string()), + proptest::option::of(arb_small_string()), + arb_vec_merge_mode(), + proptest::collection::vec(arb_manifest_build_command(), 1..3), + arb_map_merge_mode(), + proptest::collection::hash_map( + arb_small_string(), + proptest::collection::vec(arb_manifest_external_command(), 1..3), + 1..3, + ), + ), + ( + arb_vec_merge_mode(), + proptest::collection::vec(arb_small_string(), 1..3), + arb_json_value(1), + arb_json_value(1), + arb_map_merge_mode(), + proptest::collection::hash_map(arb_small_string(), arb_small_string(), 1..3), + arb_vec_merge_mode(), + proptest::collection::vec(arb_manifest_plugin_installation(), 1..3), + arb_vec_merge_mode(), + proptest::collection::vec(arb_manifest_initial_component_file(), 1..3), + ), + ) + .prop_map(|(left, right)| { + use crate::model::cascade::property::Property; + + let ( + layer_id, + second_layer_id, + selection, + component_wasm, + output_wasm, + _build_mode, + build, + _custom_commands_mode, + custom_commands, + ) = left; + let ( + _clean_mode, + clean, + config_first, + config_second, + _env_mode, + env, + _plugins_mode, + plugins, + _files_mode, + files, + ) = right; + + let mut properties = crate::model::app::ComponentLayerProperties::default(); + let selection = selection.as_ref(); + + properties + .component_wasm + .apply_layer(&layer_id, selection, None); + properties + .component_wasm + .apply_layer(&second_layer_id, selection, component_wasm); + properties + .output_wasm + .apply_layer(&second_layer_id, selection, output_wasm); + properties.build.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::vec::VecMergeMode::Append, + build, + ), + ); + properties.clean.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::vec::VecMergeMode::Prepend, + clean, + ), + ); + properties.custom_commands.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::map::MapMergeMode::Upsert, + custom_commands.clone().into_iter().collect(), + ), + ); + properties.custom_commands.apply_layer( + &second_layer_id, + selection, + ( + crate::model::cascade::property::map::MapMergeMode::Replace, + custom_commands.clone().into_iter().collect(), + ), + ); + properties.custom_commands.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::map::MapMergeMode::Remove, + custom_commands.into_iter().collect(), + ), + ); + properties + .config + .apply_layer(&layer_id, selection, Some(config_first)); + properties + .config + .apply_layer(&second_layer_id, selection, Some(config_second)); + properties + .config + .apply_layer(&layer_id, selection, Some(json!("replacement"))); + properties.env.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::map::MapMergeMode::Upsert, + env.clone().into_iter().collect(), + ), + ); + properties.env.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::map::MapMergeMode::Remove, + env.into_iter().collect(), + ), + ); + properties.plugins.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::vec::VecMergeMode::Replace, + plugins, + ), + ); + properties.files.apply_layer( + &layer_id, + selection, + ( + crate::model::cascade::property::vec::VecMergeMode::Append, + files.clone(), + ), + ); + properties.files.apply_layer( + &second_layer_id, + selection, + ( + crate::model::cascade::property::vec::VecMergeMode::Replace, + files, + ), + ); + + properties + }) + .boxed() +} + +fn arb_component_layer_id() -> BoxedStrategy { + prop_oneof![ + arb_small_string().prop_map(crate::model::app::ComponentLayerId::TemplateCommon), + arb_small_string() + .prop_map(crate::model::app::ComponentLayerId::TemplateEnvironmentPresets), + arb_small_string().prop_map(crate::model::app::ComponentLayerId::TemplateCustomPresets), + arb_small_string().prop_map(|name| { + crate::model::app::ComponentLayerId::ComponentCommon( + golem_common::model::component::ComponentName(name), + ) + }), + arb_small_string().prop_map(|name| { + crate::model::app::ComponentLayerId::ComponentEnvironmentPresets( + golem_common::model::component::ComponentName(name), + ) + }), + arb_small_string().prop_map(|name| { + crate::model::app::ComponentLayerId::ComponentCustomPresets( + golem_common::model::component::ComponentName(name), + ) + }), + ] + .boxed() +} + +fn arb_vec_merge_mode() -> BoxedStrategy { + prop_oneof![ + Just(crate::model::cascade::property::vec::VecMergeMode::Append), + Just(crate::model::cascade::property::vec::VecMergeMode::Prepend), + Just(crate::model::cascade::property::vec::VecMergeMode::Replace), + ] + .boxed() +} + +fn arb_map_merge_mode() -> BoxedStrategy { + prop_oneof![ + Just(crate::model::cascade::property::map::MapMergeMode::Upsert), + Just(crate::model::cascade::property::map::MapMergeMode::Replace), + Just(crate::model::cascade::property::map::MapMergeMode::Remove), + ] + .boxed() +} + +fn arb_manifest_build_command() -> BoxedStrategy { + prop_oneof![ + arb_manifest_external_command().prop_map(crate::model::app_raw::BuildCommand::External), + ( + arb_small_string(), + arb_small_string(), + proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), + proptest::option::of(arb_small_string()), + ) + .prop_map(|(generate_quickjs_crate, wit, js_modules, world)| { + crate::model::app_raw::BuildCommand::QuickJSCrate( + crate::model::app_raw::GenerateQuickJSCrate { + generate_quickjs_crate, + wit, + js_modules, + world, + }, + ) + }), + ( + arb_small_string(), + arb_small_string(), + proptest::option::of(arb_small_string()) + ) + .prop_map(|(generate_quickjs_dts, wit, world)| { + crate::model::app_raw::BuildCommand::QuickJSDTS( + crate::model::app_raw::GenerateQuickJSDTS { + generate_quickjs_dts, + wit, + world, + }, + ) + }), + (arb_small_string(), arb_small_string(), arb_small_string()).prop_map( + |(inject_to_prebuilt_quickjs, module, into)| { + crate::model::app_raw::BuildCommand::InjectToPrebuiltQuickJs( + crate::model::app_raw::InjectToPrebuiltQuickJs { + inject_to_prebuilt_quickjs, + module, + into, + }, + ) + } + ), + (arb_small_string(), arb_small_string()).prop_map(|(preinitialize_js, into)| { + crate::model::app_raw::BuildCommand::PreinitializeJs( + crate::model::app_raw::PreinitializeJs { + preinitialize_js, + into, + }, + ) + }), + ] + .boxed() +} + +fn arb_manifest_external_command() -> BoxedStrategy { + ( + arb_small_string(), + proptest::option::of(arb_small_string()), + proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), + proptest::collection::vec(arb_small_string(), 0..3), + proptest::collection::vec(arb_small_string(), 0..3), + proptest::collection::vec(arb_small_string(), 0..3), + proptest::collection::vec(arb_small_string(), 0..3), + ) + .prop_map(|(command, dir, env, rmdirs, mkdirs, sources, targets)| { + crate::model::app_raw::ExternalCommand { + command, + dir, + env: env.into_iter().collect(), + rmdirs, + mkdirs, + sources, + targets, + } + }) + .boxed() +} + +fn arb_manifest_plugin_installation() -> BoxedStrategy { + ( + proptest::option::of(arb_small_string()), + arb_small_string(), + arb_small_string(), + proptest::collection::hash_map(arb_small_string(), arb_small_string(), 0..3), + ) + .prop_map(|(account, name, version, parameters)| { + crate::model::app_raw::PluginInstallation { + account, + name, + version, + parameters, + } + }) + .boxed() +} + +fn arb_manifest_initial_component_file() +-> BoxedStrategy { + ( + arb_small_string(), + arb_small_string(), + proptest::option::of(arb_agent_file_permissions()), + ) + .prop_map(|(source_path, target_path, permissions)| { + crate::model::app_raw::InitialComponentFile { + source_path, + target_path: golem_common::model::component::CanonicalFilePath::from_abs_str( + &format!("/{target_path}"), + ) + .expect("generated path should be valid"), + permissions, + } + }) + .boxed() +} + +fn arb_component_view() -> BoxedStrategy { + ( + arb_small_string(), + arb_uuid(), + proptest::option::of(arb_small_string()), + arb_small_u64(), + arb_small_u64(), + Just(fixed_datetime()), + arb_uuid(), + proptest::collection::vec(arb_small_string(), 0..5), + proptest::collection::vec(arb_agent_type(), 0..3), + proptest::collection::btree_map( + arb_agent_type_name(), + arb_agent_type_provision_config(), + 0..3, + ), + ) + .prop_map( + |( + component_name, + component_id, + component_version, + component_revision, + component_size, + created_at, + environment_id, + exports, + agent_types, + agent_type_provision_configs, + )| { + crate::model::component::ComponentView { + component_name: golem_common::model::component::ComponentName(component_name), + component_id: golem_common::model::component::ComponentId(component_id), + component_version, + component_revision, + component_size, + created_at, + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + exports, + agent_types, + agent_type_provision_configs, + } + }, + ) + .boxed() +} + +fn arb_agent_type_provision_config() +-> BoxedStrategy { + ( + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), + proptest::collection::vec(arb_typed_agent_config_entry(), 0..3), + proptest::collection::vec(arb_installed_plugin(), 0..2), + proptest::collection::vec(arb_initial_agent_file(), 0..2), + arb_polymorphic_card(), + ) + .prop_map(|(env, config, plugins, files, initial_permission)| { + golem_common::model::component_metadata::AgentTypeProvisionConfig { + env, + config, + plugins, + files, + initial_permissions: initial_permission, + } + }) + .boxed() +} + +fn arb_typed_agent_config_entry() +-> BoxedStrategy { + ( + proptest::collection::vec(arb_small_string(), 1..3), + arb_small_string(), + ) + .prop_map( + |(path, value)| golem_common::model::worker::TypedAgentConfigEntry { + path, + value: golem_common::schema::TypedSchemaValue::new( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::string(), + ), + golem_common::schema::SchemaValue::String(value), + ), + }, + ) + .boxed() +} + +fn arb_installed_plugin() -> BoxedStrategy { + ( + arb_uuid(), + 0i32..1000, + proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), + arb_uuid(), + arb_small_string(), + arb_small_string(), + proptest::option::of(arb_uuid()), + proptest::option::of(arb_small_u64()), + ) + .prop_map( + |( + environment_plugin_grant_id, + priority, + parameters, + plugin_registration_id, + plugin_name, + plugin_version, + oplog_processor_component_id, + oplog_processor_component_revision, + )| { + golem_common::model::component::InstalledPlugin { + environment_plugin_grant_id: + golem_common::model::environment_plugin_grant::EnvironmentPluginGrantId( + environment_plugin_grant_id, + ), + priority: golem_common::model::component::PluginPriority(priority), + parameters, + plugin_registration_id: + golem_common::model::plugin_registration::PluginRegistrationId( + plugin_registration_id, + ), + plugin_name, + plugin_version, + oplog_processor_component_id: oplog_processor_component_id + .map(golem_common::model::component::ComponentId), + oplog_processor_component_revision: oplog_processor_component_revision.map( + |revision| { + golem_common::model::component::ComponentRevision::new(revision) + .expect("generated revision should be valid") + }, + ), + } + }, + ) + .boxed() +} + +fn arb_initial_agent_file() -> BoxedStrategy { + ( + arb_hash(), + arb_small_string(), + arb_agent_file_permissions(), + arb_small_u64(), + ) + .prop_map(|(content_hash, path, permissions, size)| { + golem_common::model::component::InitialAgentFile { + content_hash: golem_common::model::agent::AgentFileContentHash(content_hash), + path: golem_common::model::component::AgentFilePath::from_abs_str(&format!( + "/{path}" + )) + .expect("generated path should be valid"), + permissions, + size, + } + }) + .boxed() +} + +fn arb_agent_file_permissions() +-> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::component::AgentFilePermissions::ReadOnly), + Just(golem_common::model::component::AgentFilePermissions::ReadWrite), + ] + .boxed() +} + +fn arb_polymorphic_card() -> BoxedStrategy { + ( + arb_uuid(), + proptest::collection::vec(arb_uuid(), 1..3), + proptest::bool::ANY, + arb_datetime(), + proptest::option::of(arb_datetime()), + ) + .prop_map( + |(uuid, parent_uuids, system_card, created_at, expires_at)| { + golem_common::model::card::PolymorphicCard { + card_id: CardId(uuid), + parent_ids: parent_uuids.into_iter().map(CardId).collect(), + lower_negative: Vec::new(), + lower_positive: Vec::new(), + upper_negative: Vec::new(), + upper_positive: Vec::new(), + system_card, + created_at, + expires_at, + } + }, + ) + .boxed() +} + +fn arb_deploy_plan_result() -> OutputDocumentStrategy { + any::() + .prop_map(|include_environment_setup| { + let deployment_diff = empty_deployment_diff(); + let environment_setup = + include_environment_setup.then(crate::model::deploy::EnvironmentSetupPlan::default); + to_structured_output_value_masked( + DeployPlanView { + deployment_diff: &deployment_diff, + environment_setup: environment_setup.as_ref(), + }, + MaskingConfig::hide_secrets(), + ) + .expect("generated deploy plan should serialize") + }) + .boxed() +} + +fn arb_deployment_diff_result() -> OutputDocumentStrategy { + arb_deployment_diff() + .prop_map(|diff| { + to_structured_output_value_masked(diff, MaskingConfig::hide_secrets()) + .expect("generated deployment diff should serialize") + }) + .boxed() +} + +fn arb_deployment_diff() -> BoxedStrategy { + ( + arb_small_string(), + arb_small_string(), + arb_small_string(), + arb_hash(), + arb_hash(), + arb_hash(), + arb_hash(), + arb_hash(), + arb_hash(), + ) + .prop_map(|(component_key, http_key, mcp_key, current_component_hash, new_component_hash, current_file_hash, new_file_hash, current_mcp_hash, new_mcp_hash)| { + use golem_common::model::diff::Diffable; + + let mut current = golem_common::model::diff::Deployment::default(); + let mut new = golem_common::model::diff::Deployment::default(); + + let current_component = golem_common::model::diff::Component { + wasm_hash: current_component_hash, + agent_type_provision_configs: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::AgentTypeProvisionConfig { + env: BTreeMap::from_iter([("A".to_string(), "old".to_string())]), + config: BTreeMap::from_iter([( + "path".to_string(), + golem_common::base_model::json::NormalizedJsonValue(json!("old")), + )]), + files_by_path: BTreeMap::from_iter([( + "/config.json".to_string(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::AgentFile { + hash: current_file_hash, + permissions: golem_common::model::component::AgentFilePermissions::ReadOnly, + }, + ), + )]), + plugins_by_grant_id: BTreeMap::new(), + initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { + lower_negative: Vec::new(), + lower_positive: Vec::new(), + upper_negative: Vec::new(), + upper_positive: Vec::new() + } + }, + ), + )]), + }; + + let new_component = golem_common::model::diff::Component { + wasm_hash: new_component_hash, + agent_type_provision_configs: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::AgentTypeProvisionConfig { + env: BTreeMap::from_iter([("A".to_string(), "new".to_string())]), + config: BTreeMap::from_iter([( + "path".to_string(), + golem_common::base_model::json::NormalizedJsonValue(json!("new")), + )]), + files_by_path: BTreeMap::from_iter([( + "/config.json".to_string(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::AgentFile { + hash: new_file_hash, + permissions: golem_common::model::component::AgentFilePermissions::ReadWrite, + }, + ), + )]), + plugins_by_grant_id: BTreeMap::new(), + initial_permissions: golem_common::model::diff::AgentTypeInitialPermission { + lower_negative: Vec::new(), + lower_positive: Vec::new(), + upper_negative: Vec::new(), + upper_positive: Vec::new() + } + }, + ), + )]), + }; + + current.components.insert( + component_key.clone(), + golem_common::model::diff::HashOf::form_value(current_component), + ); + new.components.insert( + component_key, + golem_common::model::diff::HashOf::form_value(new_component), + ); + + new.http_api_deployments.insert( + http_key.clone(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::HttpApiDeployment { + webhooks_prefix: "new-webhooks".to_string(), + openapi_endpoint_prefix: "new-openapi".to_string(), + agents: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::HttpApiDeploymentAgentOptions { + security_scheme: Some("new-scheme".to_string()), + test_session_header: Some("x-test".to_string()), + }, + )]), + }, + ), + ); + current.http_api_deployments.insert( + http_key, + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::HttpApiDeployment { + webhooks_prefix: "old-webhooks".to_string(), + openapi_endpoint_prefix: "old-openapi".to_string(), + agents: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::HttpApiDeploymentAgentOptions { + security_scheme: Some("old-scheme".to_string()), + test_session_header: None, + }, + )]), + }, + ), + ); + + current.mcp_deployments.insert( + mcp_key.clone(), + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::McpDeployment { + agents: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::McpDeploymentAgentOptions { + security_scheme: Some(current_mcp_hash.to_string()), + }, + )]), + }, + ), + ); + new.mcp_deployments.insert( + mcp_key, + golem_common::model::diff::HashOf::form_value( + golem_common::model::diff::McpDeployment { + agents: BTreeMap::from_iter([( + "agent".to_string(), + golem_common::model::diff::McpDeploymentAgentOptions { + security_scheme: Some(new_mcp_hash.to_string()), + }, + )]), + }, + ), + ); + + golem_common::model::diff::Deployment::diff(&new, ¤t) + .expect("generated deployments should diff") + .expect("generated deployments should differ") + }) + .boxed() +} + +fn arb_environment_setup_plan_result() -> OutputDocumentStrategy { + arb_environment_setup_plan() + .prop_map(|output| { + to_structured_output_value(crate::model::text::diff::EnvironmentSetupPlanView(&output)) + .expect("generated environment setup plan should serialize") + }) + .boxed() +} + +fn arb_environment_setup_plan() -> BoxedStrategy { + ( + arb_small_string(), + arb_api_predicate(), + arb_api_retry_policy(), + arb_resource_limit(), + arb_enforcement_action(), + ) + .prop_map(|(name, predicate, policy, limit, enforcement_action)| { + let secret_path = golem_common::model::agent_secret::AgentSecretPath(vec![ + name.clone(), + "token".to_string(), + ]); + let retry_policy_default = + golem_common::model::deployment::DeploymentRetryPolicyDefault { + name: name.clone(), + priority: 1, + predicate: predicate.clone(), + policy: policy.clone(), + }; + let resource_default = golem_common::model::quota::ResourceDefinitionCreation { + name: golem_common::model::quota::ResourceName(name.clone()), + limit: limit.clone(), + enforcement_action, + unit: "request".to_string(), + units: "requests".to_string(), + }; + + crate::model::deploy::EnvironmentSetupPlan { + display: crate::model::deploy::EnvironmentSetupDisplay { + to_be_applied: crate::model::deploy::EnvironmentSetupDetailedSection { + secret_values: BTreeMap::from_iter([( + name.clone(), + crate::model::deploy::EnvironmentSetupSecretValueDisplay { + secret_type: "Str".to_string(), + value: json!("generated-secret"), + }, + )]), + retry_policies: BTreeMap::from_iter([( + name.clone(), + crate::model::deploy::EnvironmentSetupRetryPolicyDisplay { + priority: retry_policy_default.priority, + predicate: serde_json::to_value(&predicate) + .expect("generated predicate should serialize"), + policy: serde_json::to_value(&policy) + .expect("generated policy should serialize"), + }, + )]), + resources: BTreeMap::from_iter([( + name.clone(), + crate::model::deploy::EnvironmentSetupResourceDisplay { + limit: serde_json::to_value(&limit) + .expect("generated limit should serialize"), + enforcement_action: format!("{enforcement_action:?}"), + unit: "request".to_string(), + units: "requests".to_string(), + }, + )]), + }, + skipped_already_exists: crate::model::deploy::EnvironmentSetupKeysOnlySection { + secret_values: BTreeSet::from_iter([format!("{name}-existing")]), + retry_policies: BTreeSet::from_iter([format!("{name}-retry")]), + resources: BTreeSet::from_iter([format!("{name}-resource")]), + }, + }, + agent_secret_defaults: vec![ + golem_common::model::deployment::DeploymentAgentSecretDefault { + path: secret_path.clone(), + secret_value: json!("generated-secret"), + }, + ], + skipped_existing_agent_secret_defaults: vec![ + golem_common::model::deployment::DeploymentAgentSecretDefault { + path: secret_path, + secret_value: json!("existing-secret"), + }, + ], + retry_policy_defaults: vec![retry_policy_default], + resource_defaults: vec![resource_default], + } + }) + .boxed() +} + +fn arb_deployment_create_result() -> OutputDocumentStrategy { + serialized_output( + ( + arb_small_string(), + arb_small_string(), + arb_current_deployment(), + ) + .prop_map(|(application_name, environment_name, deployment)| { + crate::model::text::deployment::DeploymentNewView { + application_name: golem_common::model::application::ApplicationName( + application_name, + ), + environment_name: golem_common::model::environment::EnvironmentName( + environment_name, + ), + deployment, + } + }), + ) +} + +fn arb_deployment_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_deployment(), 0..5).prop_map(|deployments| { + crate::model::text::deployment::DeploymentListView { deployments } + }), + ) +} + +fn arb_environment_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_environment_with_details(), 0..5).prop_map(|environments| { + crate::model::text::environment::EnvironmentListView { environments } + }), + ) +} + +fn arb_environment_sync_deployment_options_result() -> OutputDocumentStrategy { + serialized_output(any::().prop_map(|updated| { + crate::model::text::environment::EnvironmentSyncDeploymentOptionsResult { updated } + })) +} + +fn arb_environment_with_details() +-> BoxedStrategy { + ( + arb_environment_summary(), + arb_application_summary(), + arb_account_summary(), + ) + .prop_map(|(environment, application, account)| { + golem_common::model::environment::EnvironmentWithDetails { + environment, + application, + account, + } + }) + .boxed() +} + +fn arb_environment_summary() -> BoxedStrategy +{ + ( + arb_uuid(), + arb_small_u64(), + Just("generated-env".to_string()), + any::(), + any::(), + any::(), + any::(), + proptest::option::of(arb_environment_current_deployment()), + ) + .prop_map( + |( + id, + revision, + name, + diff_model_version, + compatibility_check, + version_check, + security_overrides, + current_deployment, + )| { + golem_common::model::environment::EnvironmentSummary { + id: golem_common::model::environment::EnvironmentId(id), + revision: golem_common::model::environment::EnvironmentRevision::new(revision) + .expect("generated revision should be valid"), + name: golem_common::model::environment::EnvironmentName(name), + diff_model_version, + compatibility_check, + version_check, + security_overrides, + current_deployment, + } + }, + ) + .boxed() +} + +fn arb_environment_current_deployment() +-> BoxedStrategy { + ( + arb_small_u64(), + arb_small_u64(), + arb_small_string(), + arb_hash(), + ) + .prop_map( + |(revision, deployment_revision, deployment_version, deployment_hash)| { + golem_common::model::environment::EnvironmentCurrentDeploymentView { + revision: golem_common::model::deployment::CurrentDeploymentRevision::new( + revision, + ) + .expect("generated revision should be valid"), + deployment_revision: golem_common::model::deployment::DeploymentRevision::new( + deployment_revision, + ) + .expect("generated revision should be valid"), + deployment_version: golem_common::model::deployment::DeploymentVersion( + deployment_version, + ), + deployment_hash, + } + }, + ) + .boxed() +} + +fn arb_application_summary() -> BoxedStrategy +{ + (arb_uuid(), arb_small_string()) + .prop_map( + |(id, name)| golem_common::model::application::ApplicationSummary { + id: golem_common::model::application::ApplicationId(id), + name: golem_common::model::application::ApplicationName(name), + }, + ) + .boxed() +} + +fn arb_account_summary() -> BoxedStrategy { + (arb_uuid(), arb_small_string(), arb_small_string()) + .prop_map( + |(id, name, email)| golem_common::model::account::AccountSummary { + id: golem_common::model::account::AccountId(id), + name, + email: golem_common::model::account::AccountEmail::new(email), + }, + ) + .boxed() +} + +fn arb_deployment() -> BoxedStrategy { + (arb_uuid(), arb_small_u64(), arb_small_string(), arb_hash()) + .prop_map(|(environment_id, revision, version, deployment_hash)| { + golem_common::model::deployment::Deployment { + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + revision: golem_common::model::deployment::DeploymentRevision::new(revision) + .expect("generated revision should be valid"), + version: golem_common::model::deployment::DeploymentVersion(version), + deployment_hash, + } + }) + .boxed() +} + +fn arb_current_deployment() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_small_string(), + arb_hash(), + arb_small_u64(), + ) + .prop_map( + |(environment_id, revision, version, deployment_hash, current_revision)| { + golem_common::model::deployment::CurrentDeployment { + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + revision: golem_common::model::deployment::DeploymentRevision::new(revision) + .expect("generated revision should be valid"), + version: golem_common::model::deployment::DeploymentVersion(version), + deployment_hash, + current_revision: + golem_common::model::deployment::CurrentDeploymentRevision::new( + current_revision, + ) + .expect("generated revision should be valid"), + validation_warnings: Vec::new(), + } + }, + ) + .boxed() +} + +fn arb_plugin_unregister_result() -> OutputDocumentStrategy { + serialized_output( + ( + any::(), + arb_uuid(), + arb_small_string(), + arb_small_string(), + ) + .prop_map(|(unregistered, plugin_id, name, version)| { + crate::model::text::plugin::PluginUnregisterResult { + unregistered, + plugin_id, + name, + version, + } + }), + ) +} + +fn arb_plugin_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_plugin_registration().prop_map(crate::model::text::plugin::PluginRegistrationGetView), + ) +} + +fn arb_plugin_register_result() -> OutputDocumentStrategy { + serialized_output( + arb_plugin_registration() + .prop_map(crate::model::text::plugin::PluginRegistrationRegisterView), + ) +} + +fn arb_plugin_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_plugin_list_entry(), 0..5) + .prop_map(|plugins| crate::model::text::plugin::PluginListView { plugins }), + ) +} + +fn arb_plugin_list_entry() -> BoxedStrategy { + (arb_plugin_registration(), arb_plugin_source()) + .prop_map(|(plugin, source)| crate::model::text::plugin::PluginListEntry { plugin, source }) + .boxed() +} + +fn arb_plugin_source() -> BoxedStrategy { + prop_oneof![ + Just(crate::model::text::plugin::PluginSource::Own), + Just(crate::model::text::plugin::PluginSource::Builtin), + Just(crate::model::text::plugin::PluginSource::Shared), + ] + .boxed() +} + +fn arb_plugin_registration() +-> BoxedStrategy { + ( + arb_uuid(), + arb_uuid(), + arb_small_string(), + arb_small_string(), + arb_small_string(), + Just(golem_common::model::base64::Base64(vec![0])), + arb_small_string(), + arb_uuid(), + arb_small_u64(), + ) + .prop_map( + |( + id, + account_id, + name, + version, + description, + icon, + homepage, + component_id, + component_revision, + )| { + golem_common::model::plugin_registration::PluginRegistrationDto { + id: golem_common::model::plugin_registration::PluginRegistrationId(id), + account_id: golem_common::model::account::AccountId(account_id), + name, + version, + description, + icon, + homepage, + spec: golem_common::model::plugin_registration::PluginSpecDto::OplogProcessor( + golem_common::model::plugin_registration::OplogProcessorPluginSpec { + component_id: golem_common::model::component::ComponentId(component_id), + component_revision: + golem_common::model::component::ComponentRevision::new( + component_revision, + ) + .expect("generated revision should be valid"), + }, + ), + } + }, + ) + .boxed() +} + +fn arb_profile_create_result() -> OutputDocumentStrategy { + serialized_output((any::(), arb_small_string(), any::()).prop_map( + |(created, profile, set_active)| crate::model::text::profile::ProfileCreateResult { + created, + profile: crate::config::ProfileName(profile), + set_active, + }, + )) +} + +fn arb_profile_get_result() -> OutputDocumentStrategy { + serialized_output(arb_profile_view()) +} + +fn arb_profile_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_profile_view(), 0..5) + .prop_map(|profiles| crate::model::text::profile::ProfileListView { profiles }), + ) +} + +fn arb_profile_view() -> BoxedStrategy { + ( + any::(), + arb_small_string(), + proptest::option::of(arb_url_string()), + proptest::option::of(arb_url_string()), + any::(), + proptest::option::of(any::()), + arb_format_string(), + ) + .prop_map( + |(is_active, name, url, worker_url, allow_insecure, authenticated, default_format)| { + crate::model::ProfileView { + is_active, + name: crate::config::ProfileName(name), + url: url.map(|url| url.parse().expect("generated URL should parse")), + worker_url: worker_url + .map(|url| url.parse().expect("generated URL should parse")), + allow_insecure, + authenticated, + config: crate::config::ProfileConfig { + default_format: default_format + .parse() + .expect("generated format should parse"), + }, + } + }, + ) + .boxed() +} + +fn arb_profile_switch_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(switched, profile)| { + crate::model::text::profile::ProfileSwitchResult { + switched, + profile: crate::config::ProfileName(profile), + } + }), + ) +} + +fn arb_profile_delete_result() -> OutputDocumentStrategy { + serialized_output( + (any::(), arb_small_string()).prop_map(|(deleted, profile)| { + crate::model::text::profile::ProfileDeleteResult { + deleted, + profile: crate::config::ProfileName(profile), + } + }), + ) +} + +fn arb_profile_config_set_format_result() -> OutputDocumentStrategy { + serialized_output((any::(), arb_small_string(), arb_format()).prop_map( + |(updated, profile, format)| crate::model::text::profile::ProfileConfigSetFormatResult { + updated, + profile: crate::config::ProfileName(profile), + format, + }, + )) +} + +fn arb_resource_create_result() -> OutputDocumentStrategy { + serialized_output( + arb_resource_definition() + .prop_map(crate::model::text::resource_definition::ResourceDefinitionCreateView), + ) +} + +fn arb_resource_delete_result() -> OutputDocumentStrategy { + serialized_output( + arb_resource_definition() + .prop_map(crate::model::text::resource_definition::ResourceDefinitionDeleteView), + ) +} + +fn arb_resource_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_resource_definition() + .prop_map(crate::model::text::resource_definition::ResourceDefinitionGetView), + ) +} + +fn arb_resource_update_result() -> OutputDocumentStrategy { + serialized_output( + arb_resource_definition() + .prop_map(crate::model::text::resource_definition::ResourceDefinitionUpdateView), + ) +} + +fn arb_resource_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_resource_definition(), 0..5).prop_map(|resources| { + crate::model::text::resource_definition::ResourceDefinitionListView { resources } + }), + ) +} + +fn arb_resource_definition() -> BoxedStrategy { + ( + arb_uuid(), + arb_small_u64(), + arb_uuid(), + arb_small_string(), + arb_resource_limit(), + arb_enforcement_action(), + arb_small_string(), + arb_small_string(), + ) + .prop_map( + |(id, revision, environment_id, name, limit, enforcement_action, unit, units)| { + golem_common::model::quota::ResourceDefinition { + id: golem_common::model::quota::ResourceDefinitionId(id), + revision: golem_common::model::quota::ResourceDefinitionRevision::new(revision) + .expect("generated revision should be valid"), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + name: golem_common::model::quota::ResourceName(name), + limit, + enforcement_action, + unit, + units, + } + }, + ) + .boxed() +} + +fn arb_resource_limit() -> BoxedStrategy { + prop_oneof![ + (arb_small_u64(), arb_time_period(), arb_small_u64()).prop_map(|(value, period, max)| { + golem_common::model::quota::ResourceLimit::Rate( + golem_common::model::quota::ResourceRateLimit { value, period, max }, + ) + }), + arb_small_u64().prop_map(|value| { + golem_common::model::quota::ResourceLimit::Capacity( + golem_common::model::quota::ResourceCapacityLimit { value }, + ) + }), + arb_small_u64().prop_map(|value| { + golem_common::model::quota::ResourceLimit::Concurrency( + golem_common::model::quota::ResourceConcurrencyLimit { value }, + ) + }), + ] + .boxed() +} + +fn arb_enforcement_action() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::quota::EnforcementAction::Reject), + Just(golem_common::model::quota::EnforcementAction::Throttle), + Just(golem_common::model::quota::EnforcementAction::Terminate), + ] + .boxed() +} + +fn arb_time_period() -> BoxedStrategy { + prop_oneof![ + Just(golem_common::model::quota::TimePeriod::Second), + Just(golem_common::model::quota::TimePeriod::Minute), + Just(golem_common::model::quota::TimePeriod::Hour), + Just(golem_common::model::quota::TimePeriod::Day), + Just(golem_common::model::quota::TimePeriod::Month), + Just(golem_common::model::quota::TimePeriod::Year) + ] + .boxed() +} + +fn arb_api_predicate_value() +-> BoxedStrategy { + prop_oneof![ + arb_small_string().prop_map(|value| { + golem_common::base_model::retry_policy::ApiPredicateValue::Text( + golem_common::base_model::retry_policy::ApiTextValue { value }, + ) + }), + any::().prop_map(|value| { + golem_common::base_model::retry_policy::ApiPredicateValue::Integer( + golem_common::base_model::retry_policy::ApiIntegerValue { value }, + ) + }), + any::().prop_map(|value| { + golem_common::base_model::retry_policy::ApiPredicateValue::Boolean( + golem_common::base_model::retry_policy::ApiBooleanValue { value }, + ) + }), + ] + .boxed() +} + +fn arb_api_predicate() -> BoxedStrategy { + arb_api_predicate_with_depth(2) +} + +fn arb_api_predicate_with_depth( + depth: u32, +) -> BoxedStrategy { + let leaf = prop_oneof![ + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropEq( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropNeq( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropGt( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropGte( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropLt( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + (arb_small_string(), arb_api_predicate_value()).prop_map(|(property, value)| { + golem_common::base_model::retry_policy::ApiPredicate::PropLte( + golem_common::base_model::retry_policy::ApiPropertyComparison { property, value }, + ) + }), + arb_small_string().prop_map(|property| { + golem_common::base_model::retry_policy::ApiPredicate::PropExists( + golem_common::base_model::retry_policy::ApiPropertyExistence { property }, + ) + }), + ( + arb_small_string(), + proptest::collection::vec(arb_api_predicate_value(), 0..4), + ) + .prop_map(|(property, values)| { + golem_common::base_model::retry_policy::ApiPredicate::PropIn( + golem_common::base_model::retry_policy::ApiPropertySetCheck { + property, + values, + }, + ) + }), + (arb_small_string(), arb_small_string()).prop_map(|(property, pattern)| { + golem_common::base_model::retry_policy::ApiPredicate::PropMatches( + golem_common::base_model::retry_policy::ApiPropertyPattern { property, pattern }, + ) + }), + (arb_small_string(), arb_small_string()).prop_map(|(property, prefix)| { + golem_common::base_model::retry_policy::ApiPredicate::PropStartsWith( + golem_common::base_model::retry_policy::ApiPropertyPrefix { property, prefix }, + ) + }), + (arb_small_string(), arb_small_string()).prop_map(|(property, substring)| { + golem_common::base_model::retry_policy::ApiPredicate::PropContains( + golem_common::base_model::retry_policy::ApiPropertySubstring { + property, + substring, + }, + ) + }), + Just(golem_common::base_model::retry_policy::ApiPredicate::True( + golem_common::base_model::retry_policy::ApiPredicateTrue {}, + )), + Just(golem_common::base_model::retry_policy::ApiPredicate::False( + golem_common::base_model::retry_policy::ApiPredicateFalse {}, + )), + ] + .boxed(); + + if depth == 0 { + return leaf; + } + + let inner = arb_api_predicate_with_depth(depth - 1); + prop_oneof![ + leaf, + (inner.clone(), inner.clone()).prop_map(|(left, right)| { + golem_common::base_model::retry_policy::ApiPredicate::And( + golem_common::base_model::retry_policy::ApiPredicatePair { + left: Box::new(left), + right: Box::new(right), + }, + ) + }), + (inner.clone(), inner.clone()).prop_map(|(left, right)| { + golem_common::base_model::retry_policy::ApiPredicate::Or( + golem_common::base_model::retry_policy::ApiPredicatePair { + left: Box::new(left), + right: Box::new(right), + }, + ) + }), + inner.prop_map(|predicate| { + golem_common::base_model::retry_policy::ApiPredicate::Not( + golem_common::base_model::retry_policy::ApiPredicateNot { + predicate: Box::new(predicate), + }, + ) + }), + ] + .boxed() +} + +fn arb_api_retry_policy() -> BoxedStrategy { + arb_api_retry_policy_with_depth(2) +} + +fn arb_api_retry_policy_with_depth( + depth: u32, +) -> BoxedStrategy { + let leaf = prop_oneof![ + arb_small_u64().prop_map(|delay_ms| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Periodic( + golem_common::base_model::retry_policy::ApiPeriodicPolicy { delay_ms }, + ) + }), + (arb_small_u64(), 0.0f64..10.0).prop_map(|(base_delay_ms, factor)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Exponential( + golem_common::base_model::retry_policy::ApiExponentialPolicy { + base_delay_ms, + factor, + }, + ) + }), + (arb_small_u64(), arb_small_u64()).prop_map(|(first_ms, second_ms)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Fibonacci( + golem_common::base_model::retry_policy::ApiFibonacciPolicy { + first_ms, + second_ms, + }, + ) + }), + Just( + golem_common::base_model::retry_policy::ApiRetryPolicy::Immediate( + golem_common::base_model::retry_policy::ApiImmediatePolicy {}, + ) + ), + Just( + golem_common::base_model::retry_policy::ApiRetryPolicy::Never( + golem_common::base_model::retry_policy::ApiNeverPolicy {}, + ) + ), + ] + .boxed(); + + if depth == 0 { + return leaf; + } + + let inner = arb_api_retry_policy_with_depth(depth - 1); + prop_oneof![ + leaf, + (any::(), inner.clone()).prop_map(|(max_retries, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::CountBox( + golem_common::base_model::retry_policy::ApiCountBoxPolicy { + max_retries, + inner: Box::new(inner), + }, + ) + }), + (arb_small_u64(), inner.clone()).prop_map(|(limit_ms, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::TimeBox( + golem_common::base_model::retry_policy::ApiTimeBoxPolicy { + limit_ms, + inner: Box::new(inner), + }, + ) + }), + (arb_small_u64(), arb_small_u64(), inner.clone()).prop_map( + |(min_delay_ms, max_delay_ms, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Clamp( + golem_common::base_model::retry_policy::ApiClampPolicy { + min_delay_ms, + max_delay_ms, + inner: Box::new(inner), + }, + ) + }, + ), + (arb_small_u64(), inner.clone()).prop_map(|(delay_ms, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::AddDelay( + golem_common::base_model::retry_policy::ApiAddDelayPolicy { + delay_ms, + inner: Box::new(inner), + }, + ) + }), + (0.0f64..1.0, inner.clone()).prop_map(|(factor, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Jitter( + golem_common::base_model::retry_policy::ApiJitterPolicy { + factor, + inner: Box::new(inner), + }, + ) + }), + (arb_api_predicate(), inner.clone()).prop_map(|(predicate, inner)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::FilteredOn( + golem_common::base_model::retry_policy::ApiFilteredOnPolicy { + predicate, + inner: Box::new(inner), + }, + ) + }), + (inner.clone(), inner.clone()).prop_map(|(first, second)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::AndThen( + golem_common::base_model::retry_policy::ApiRetryPolicyPair { + first: Box::new(first), + second: Box::new(second), + }, + ) + },), + (inner.clone(), inner.clone()).prop_map(|(first, second)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Union( + golem_common::base_model::retry_policy::ApiRetryPolicyPair { + first: Box::new(first), + second: Box::new(second), + }, + ) + }), + (inner.clone(), inner.clone()).prop_map(|(first, second)| { + golem_common::base_model::retry_policy::ApiRetryPolicy::Intersect( + golem_common::base_model::retry_policy::ApiRetryPolicyPair { + first: Box::new(first), + second: Box::new(second), + }, + ) + }), + ] + .boxed() +} + +fn arb_retry_policy_create_result() -> OutputDocumentStrategy { + serialized_output( + arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyCreateView), + ) +} + +fn arb_retry_policy_delete_result() -> OutputDocumentStrategy { + serialized_output( + arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyDeleteView), + ) +} + +fn arb_retry_policy_get_result() -> OutputDocumentStrategy { + serialized_output( + arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyGetView), + ) +} + +fn arb_retry_policy_update_result() -> OutputDocumentStrategy { + serialized_output( + arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyUpdateView), + ) +} + +fn arb_retry_policy_list_result() -> OutputDocumentStrategy { + serialized_output( + proptest::collection::vec(arb_retry_policy(), 0..5).prop_map(|retry_policies| { + crate::model::text::retry_policy::RetryPolicyListView { retry_policies } + }), + ) +} + +fn arb_retry_policy() -> BoxedStrategy { + ( + arb_uuid(), + arb_uuid(), + arb_small_string(), + arb_small_u64(), + any::(), + arb_api_predicate(), + arb_api_retry_policy(), + ) + .prop_map( + |(id, environment_id, name, revision, priority, predicate, policy)| { + golem_common::model::retry_policy::RetryPolicyDto { + id: golem_common::model::retry_policy::RetryPolicyId(id), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + name, + revision: golem_common::model::retry_policy::RetryPolicyRevision::new(revision) + .expect("generated revision should be valid"), + priority, + predicate: golem_common::model::UntypedJsonBody( + serde_json::to_value(predicate) + .expect("generated predicate should serialize"), + ), + policy: golem_common::model::UntypedJsonBody( + serde_json::to_value(policy) + .expect("generated retry policy should serialize"), + ), + } + }, + ) + .boxed() +} + +fn arb_secret_create_result() -> OutputDocumentStrategy { + arb_secret() + .prop_map(|secret| { + to_structured_output_value_masked( + crate::model::text::secret::SecretCreateView(secret.into()), + MaskingConfig::hide_secrets(), + ) + .expect("generated secret create should serialize") + }) + .boxed() +} + +fn arb_secret_delete_result() -> OutputDocumentStrategy { + arb_secret() + .prop_map(|secret| { + to_structured_output_value_masked( + crate::model::text::secret::SecretDeleteView(secret.into()), + MaskingConfig::hide_secrets(), + ) + .expect("generated secret delete should serialize") + }) + .boxed() +} + +fn arb_secret_get_result() -> OutputDocumentStrategy { + arb_secret() + .prop_map(|secret| { + to_structured_output_value_masked( + crate::model::text::secret::SecretGetView(secret.into()), + MaskingConfig::hide_secrets(), + ) + .expect("generated secret get should serialize") + }) + .boxed() +} + +fn arb_secret_update_value_result() -> OutputDocumentStrategy { + arb_secret() + .prop_map(|secret| { + to_structured_output_value_masked( + crate::model::text::secret::SecretUpdateView(secret.into()), + MaskingConfig::hide_secrets(), + ) + .expect("generated secret update should serialize") + }) + .boxed() +} + +fn arb_secret_list_result() -> OutputDocumentStrategy { + proptest::collection::vec(arb_secret(), 0..5) + .prop_map(|secrets| { + to_structured_output_value_masked( + crate::model::text::secret::SecretListView { + secrets: secrets.into_iter().map(Into::into).collect(), + environment_name: "generated-environment".to_string(), + show_ids: false, + }, + MaskingConfig::hide_secrets(), + ) + .expect("generated secret list should serialize") + }) + .boxed() +} + +fn arb_secret() -> BoxedStrategy { + ( + arb_uuid(), + arb_uuid(), + proptest::collection::vec(arb_small_string(), 1..4), + arb_small_u64(), + arb_secret_type_and_value(), + ) + .prop_map( + |(id, environment_id, path, revision, (secret_type, secret_value))| { + golem_client::model::AgentSecretDto { + id: golem_common::model::agent_secret::AgentSecretId(id), + environment_id: golem_common::model::environment::EnvironmentId(environment_id), + path: golem_common::model::agent_secret::CanonicalAgentSecretPath(path), + revision: golem_common::model::agent_secret::AgentSecretRevision::new(revision) + .expect("generated revision should be valid"), + secret_type, + secret_value, + } + }, + ) + .boxed() +} + +fn arb_secret_type_and_value() -> BoxedStrategy<( + golem_common::schema::SchemaGraph, + Option, +)> { + prop_oneof![ + proptest::option::of( + arb_small_string().prop_map(golem_common::schema::SchemaValue::String) + ) + .prop_map(|value| { + ( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::string(), + ), + value, + ) + }), + proptest::option::of(any::().prop_map(golem_common::schema::SchemaValue::Bool)) + .prop_map(|value| { + ( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::bool(), + ), + value, + ) + }), + proptest::option::of(arb_small_u64().prop_map(golem_common::schema::SchemaValue::U64)) + .prop_map(|value| { + ( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::u64(), + ), + value, + ) + }), + proptest::option::of( + proptest::collection::vec(arb_small_u64(), 0..3).prop_map(|values| { + golem_common::schema::SchemaValue::List { + elements: values + .into_iter() + .map(golem_common::schema::SchemaValue::U64) + .collect(), + } + }) + ) + .prop_map(|value| { + ( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::list(golem_common::schema::SchemaType::u64()), + ), + value, + ) + }), + proptest::option::of(proptest::option::of(arb_small_string()).prop_map(|value| { + golem_common::schema::SchemaValue::Option { + inner: value + .map(|value| Box::new(golem_common::schema::SchemaValue::String(value))), + } + })) + .prop_map(|value| { + ( + golem_common::schema::SchemaGraph::anonymous( + golem_common::schema::SchemaType::option( + golem_common::schema::SchemaType::string(), + ), + ), + value, + ) + }), + ] + .boxed() +} + +fn arb_json_value(depth: u32) -> OutputDocumentStrategy { + let leaf = prop_oneof![ + Just(Value::Null), + any::().prop_map(Value::Bool), + any::().prop_map(|value| json!(value)), + arb_small_string().prop_map(Value::String), + ]; + + if depth == 0 { + return leaf.boxed(); + } + + let inner = arb_json_value(depth - 1); + prop_oneof![ + leaf, + proptest::collection::vec(inner.clone(), 0..4).prop_map(Value::Array), + proptest::collection::btree_map(arb_small_string(), inner, 0..4) + .prop_map(|map| Value::Object(map.into_iter().collect())), + ] + .boxed() +} + +fn arb_format_string() -> BoxedStrategy<&'static str> { + prop_oneof![ + Just("json"), + Just("pretty-json"), + Just("yaml"), + Just("pretty-yaml"), + Just("text"), + Just("toon") + ] + .boxed() +} + +fn arb_format() -> BoxedStrategy { + prop_oneof![ + Just(crate::model::format::Format::Json), + Just(crate::model::format::Format::PrettyJson), + Just(crate::model::format::Format::Yaml), + Just(crate::model::format::Format::PrettyYaml), + Just(crate::model::format::Format::Text), + Just(crate::model::format::Format::Toon), + ] + .boxed() +} From fc2013781230a4ef44a98af66032a1f68544791c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 15:37:08 +0200 Subject: [PATCH 32/70] move model/mod.rs catch-all types to proper homes --- cli/golem-cli/src/app/template/description.rs | 35 +++ cli/golem-cli/src/app/template/mod.rs | 2 + cli/golem-cli/src/app/template/template.rs | 5 + cli/golem-cli/src/command.rs | 79 +++++- cli/golem-cli/src/command_handler/app/mod.rs | 5 +- cli/golem-cli/src/command_handler/plugin.rs | 2 +- .../src/command_handler/profile/mod.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 8 +- cli/golem-cli/src/model/config/mod.rs | 41 ++++ cli/golem-cli/src/model/input.rs | 61 +++++ cli/golem-cli/src/model/mod.rs | 231 +----------------- cli/golem-cli/src/model/text/profile.rs | 2 +- cli/golem-cli/src/model/text/template.rs | 2 +- 13 files changed, 229 insertions(+), 246 deletions(-) create mode 100644 cli/golem-cli/src/app/template/description.rs create mode 100644 cli/golem-cli/src/model/input.rs diff --git a/cli/golem-cli/src/app/template/description.rs b/cli/golem-cli/src/app/template/description.rs new file mode 100644 index 0000000000..1e506162dd --- /dev/null +++ b/cli/golem-cli/src/app/template/description.rs @@ -0,0 +1,35 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::app::template::AppTemplate; +use crate::model::GuestLanguage; +use serde::{Deserialize, Serialize}; + +/// A summary view of an [`AppTemplate`] for CLI output (the `app templates` listing). +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct TemplateDescription { + pub name: String, + pub language: GuestLanguage, + pub description: String, +} + +impl TemplateDescription { + pub fn from_template(template: &AppTemplate) -> Self { + Self { + name: template.name.as_str().to_string(), + language: template.language, + description: template.description().to_string(), + } + } +} diff --git a/cli/golem-cli/src/app/template/mod.rs b/cli/golem-cli/src/app/template/mod.rs index 3db3c0bb98..a51b0c9f37 100644 --- a/cli/golem-cli/src/app/template/mod.rs +++ b/cli/golem-cli/src/app/template/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod description; mod generator; mod metadata; mod plan; @@ -20,6 +21,7 @@ mod snippet; #[allow(clippy::module_inception)] mod template; +pub use description::TemplateDescription; pub use generator::InMemoryFs; pub use metadata::AppTemplateMetadata; pub use plan::{ diff --git a/cli/golem-cli/src/app/template/template.rs b/cli/golem-cli/src/app/template/template.rs index 269a7901b6..eb191657f6 100644 --- a/cli/golem-cli/src/app/template/template.rs +++ b/cli/golem-cli/src/app/template/template.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::app::template::description::TemplateDescription; use crate::app::template::generator::{ InMemoryFs, StdFs, generate_agent_by_template, generate_commons_by_template, generate_component_by_template, generate_on_demand_commons_by_template, @@ -135,6 +136,10 @@ impl AppTemplate { } } + pub fn to_description(&self) -> TemplateDescription { + TemplateDescription::from_template(self) + } + fn generate_commons( &self, application_name: &ApplicationName, diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 5ad03fce63..44162f2f1d 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -1748,9 +1748,30 @@ pub mod api { } pub mod security_scheme { - use crate::model::ProviderKindArg; - use clap::Subcommand; - use golem_common::model::security_scheme::SecuritySchemeName; + use clap::{Subcommand, ValueEnum}; + use golem_common::model::security_scheme::{ProviderKind, SecuritySchemeName}; + + #[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] + #[clap(rename_all = "lower")] + pub enum ProviderKindArg { + Google, + Facebook, + Microsoft, + Gitlab, + Custom, + } + + impl From for ProviderKind { + fn from(value: ProviderKindArg) -> Self { + match value { + ProviderKindArg::Google => ProviderKind::Google, + ProviderKindArg::Facebook => ProviderKind::Facebook, + ProviderKindArg::Microsoft => ProviderKind::Microsoft, + ProviderKindArg::Gitlab => ProviderKind::Gitlab, + ProviderKindArg::Custom => ProviderKind::Custom, + } + } + } #[derive(Debug, Subcommand)] pub enum ApiSecuritySchemeSubcommand { @@ -1878,9 +1899,53 @@ pub mod api { } pub mod resource_definition { - use crate::model::EnforcementActionArg; - use clap::Subcommand; - use golem_common::model::quota::ResourceDefinitionId; + use clap::{Subcommand, ValueEnum}; + use golem_common::model::quota::{EnforcementAction, ResourceDefinitionId}; + use std::fmt::{Display, Formatter}; + use std::str::FromStr; + + #[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] + #[clap(rename_all = "kebab-case")] + pub enum EnforcementActionArg { + Throttle, + Reject, + Terminate, + } + + impl Display for EnforcementActionArg { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EnforcementActionArg::Throttle => write!(f, "throttle"), + EnforcementActionArg::Reject => write!(f, "reject"), + EnforcementActionArg::Terminate => write!(f, "terminate"), + } + } + } + + impl FromStr for EnforcementActionArg { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "throttle" => Ok(Self::Throttle), + "reject" => Ok(Self::Reject), + "terminate" => Ok(Self::Terminate), + _ => Err(format!( + "Unknown enforcement actions: {s}. Expected one of \"throttle\", \"reject\", \"terminate\"" + )), + } + } + } + + impl From for EnforcementAction { + fn from(value: EnforcementActionArg) -> Self { + match value { + EnforcementActionArg::Throttle => Self::Throttle, + EnforcementActionArg::Terminate => Self::Terminate, + EnforcementActionArg::Reject => Self::Reject, + } + } + } #[derive(Debug, Subcommand)] pub enum ResourceDefinitionSubcommand { @@ -2056,7 +2121,7 @@ pub mod retry_policy { } pub mod plugin { - use crate::model::PathBufOrStdin; + use crate::model::input::PathBufOrStdin; use clap::Subcommand; use uuid::Uuid; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 36137a6d29..7c1e6f7da4 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -18,6 +18,7 @@ use crate::app::build::check::{ use crate::app::context::BuildContext; use crate::app::error::CustomCommandError; use crate::app::template::AppTemplateName; +use crate::app::template::TemplateDescription; use crate::command::builtin_exec_subcommands; use crate::command::exec::ExecSubcommand; use crate::command::shared_args::{ @@ -40,6 +41,7 @@ use crate::log::{ log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; +use crate::model::GuestLanguage; use crate::model::agent::AgentUpdateMode; use crate::model::agent::view::AgentTypeView; use crate::model::app::{ @@ -60,7 +62,6 @@ use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; use crate::model::text::help::AvailableComponentNamesHelp; use crate::model::text::server::ToFormattedServerContext; use crate::model::text::template::TemplateListView; -use crate::model::{GuestLanguage, TemplateDescription}; use anyhow::{anyhow, bail}; use colored::Colorize; use futures_util::{StreamExt, TryStreamExt, stream}; @@ -479,7 +480,7 @@ impl AppCommandHandler { let templates: Vec = templates .into_values() .flat_map(|templates| templates.into_values()) - .map(|template| TemplateDescription::from_template(&template.0)) + .map(|template| template.0.to_description()) .collect(); self.ctx diff --git a/cli/golem-cli/src/command_handler/plugin.rs b/cli/golem-cli/src/command_handler/plugin.rs index 912cc14f87..af98528204 100644 --- a/cli/golem-cli/src/command_handler/plugin.rs +++ b/cli/golem-cli/src/command_handler/plugin.rs @@ -17,8 +17,8 @@ use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; -use crate::model::PathBufOrStdin; use crate::model::environment::EnvironmentResolveMode; +use crate::model::input::PathBufOrStdin; use crate::model::plugin_manifest::{PluginManifest, PluginTypeSpecificManifest}; use crate::model::text::plugin::{ PluginListEntry, PluginListView, PluginRegistrationGetView, PluginRegistrationRegisterView, diff --git a/cli/golem-cli/src/command_handler/profile/mod.rs b/cli/golem-cli/src/command_handler/profile/mod.rs index f913933581..9b850a36f4 100644 --- a/cli/golem-cli/src/command_handler/profile/mod.rs +++ b/cli/golem-cli/src/command_handler/profile/mod.rs @@ -23,7 +23,7 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::log::log_error; use crate::log::{LogColorize, log_action, log_warn_action}; -use crate::model::ProfileView; +use crate::model::config::ProfileView; use crate::model::format::Format; use crate::model::text::profile::{ ProfileCreateResult, ProfileDeleteResult, ProfileListView, ProfileSwitchResult, diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 0c8c63bf57..f1b6c06c77 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3749,10 +3749,10 @@ fn arb_template_list_result() -> OutputDocumentStrategy { ) } -fn arb_template_description() -> BoxedStrategy { +fn arb_template_description() -> BoxedStrategy { (arb_small_string(), arb_guest_language(), arb_small_string()) .prop_map( - |(name, language, description)| crate::model::TemplateDescription { + |(name, language, description)| crate::app::template::TemplateDescription { name, language, description, @@ -4911,7 +4911,7 @@ fn arb_profile_list_result() -> OutputDocumentStrategy { ) } -fn arb_profile_view() -> BoxedStrategy { +fn arb_profile_view() -> BoxedStrategy { ( any::(), arb_small_string(), @@ -4923,7 +4923,7 @@ fn arb_profile_view() -> BoxedStrategy { ) .prop_map( |(is_active, name, url, worker_url, allow_insecure, authenticated, default_format)| { - crate::model::ProfileView { + crate::model::config::ProfileView { is_active, name: crate::config::ProfileName(name), url: url.map(|url| url.parse().expect("generated URL should parse")), diff --git a/cli/golem-cli/src/model/config/mod.rs b/cli/golem-cli/src/model/config/mod.rs index 95553df946..e438c07181 100644 --- a/cli/golem-cli/src/model/config/mod.rs +++ b/cli/golem-cli/src/model/config/mod.rs @@ -12,6 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::config::{AuthenticationConfig, NamedProfile, ProfileConfig, ProfileName}; +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProfileView { + pub is_active: bool, + pub name: ProfileName, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub url: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub worker_url: Option, + #[serde(skip_serializing_if = "std::ops::Not::not", default)] + pub allow_insecure: bool, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub authenticated: Option, + pub config: ProfileConfig, +} + +impl ProfileView { + pub fn from_profile(active: &ProfileName, profile: NamedProfile) -> Self { + let NamedProfile { name, profile } = profile; + + let authenticated = match &profile.auth { + AuthenticationConfig::OAuth2(inner) => Some(inner.data.is_some()), + AuthenticationConfig::Static(_) => None, + }; + + ProfileView { + is_active: &name == active, + name, + url: profile.custom_url, + worker_url: profile.custom_worker_url, + allow_insecure: profile.allow_insecure, + authenticated, + config: profile.config, + } + } +} + pub fn value_at_path<'a>( root: &'a serde_json::Value, path: &[String], diff --git a/cli/golem-cli/src/model/input.rs b/cli/golem-cli/src/model/input.rs new file mode 100644 index 0000000000..a6ca92e8e2 --- /dev/null +++ b/cli/golem-cli/src/model/input.rs @@ -0,0 +1,61 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Shared CLI input source: a value that is either a filesystem path or STDIN. + +use anyhow::{Context, anyhow}; +use std::io::Read; +use std::path::PathBuf; +use std::str::FromStr; + +#[derive(Clone, Debug)] +pub enum PathBufOrStdin { + Path(PathBuf), + Stdin, +} + +impl PathBufOrStdin { + pub fn read_to_string(&self) -> anyhow::Result { + match self { + PathBufOrStdin::Path(path) => std::fs::read_to_string(path) + .with_context(|| anyhow!("Failed to read file: {}", path.display())), + PathBufOrStdin::Stdin => { + let mut content = String::new(); + let _ = std::io::stdin() + .read_to_string(&mut content) + .with_context(|| anyhow!("Failed to read from STDIN"))?; + Ok(content) + } + } + } + + pub fn is_stdin(&self) -> bool { + match self { + PathBufOrStdin::Path(_) => false, + PathBufOrStdin::Stdin => true, + } + } +} + +impl FromStr for PathBufOrStdin { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + if s == "-" { + Ok(PathBufOrStdin::Stdin) + } else { + Ok(PathBufOrStdin::Path(PathBuf::from_str(s)?)) + } + } +} diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 9fd87d5267..8718c8bd14 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -24,6 +24,7 @@ pub mod deploy; pub mod environment; pub mod format; pub mod http_api; +pub mod input; pub mod invoke_result_view; pub mod masking; pub mod plugin; @@ -32,29 +33,12 @@ pub mod repl; pub mod template; pub mod text; -use crate::app::template::AppTemplate; -use crate::config::AuthenticationConfig; -use crate::config::{NamedProfile, ProfileConfig, ProfileName}; -use anyhow::{Context, anyhow}; use clap::ValueEnum; -use clap::builder::{StringValueParser, TypedValueParser}; -use clap::error::{ContextKind, ContextValue, ErrorKind}; -use clap::{Arg, Error}; -use golem_common::model::account::AccountId; -use golem_common::model::quota::EnforcementAction; -use golem_common::model::security_scheme::ProviderKind; use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::ffi::OsStr; -use std::fmt; -use std::fmt::Display; -use std::fmt::{Debug, Formatter}; -use std::io::Read; -use std::path::PathBuf; +use std::fmt::{self, Formatter}; use std::str::FromStr; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use url::Url; // NOTE: the order of languages (currently) is NOT alphabetical, rather based on recommendation #[derive( @@ -139,214 +123,3 @@ impl FromStr for GuestLanguage { }) } } - -#[derive(Clone)] -pub struct JsonValueParser; - -impl TypedValueParser for JsonValueParser { - type Value = Value; - - fn parse_ref( - &self, - cmd: &clap::Command, - arg: Option<&Arg>, - value: &OsStr, - ) -> Result { - let inner = StringValueParser::new(); - let val = inner.parse_ref(cmd, arg, value)?; - let parsed = ::from_str(&val); - - match parsed { - Ok(value) => Ok(value), - Err(serde_err) => { - let mut err = clap::Error::new(ErrorKind::ValueValidation); - if let Some(arg) = arg { - err.insert( - ContextKind::InvalidArg, - ContextValue::String(arg.to_string()), - ); - } - err.insert( - ContextKind::InvalidValue, - ContextValue::String(format!("Invalid JSON value: {serde_err}")), - ); - Err(err) - } - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct TemplateDescription { - pub name: String, - pub language: GuestLanguage, - pub description: String, -} - -impl TemplateDescription { - pub fn from_template(template: &AppTemplate) -> Self { - Self { - name: template.name.as_str().to_string(), - language: template.language, - description: template.description().to_string(), - } - } -} - -#[derive(Clone, Debug)] -pub enum PathBufOrStdin { - Path(PathBuf), - Stdin, -} - -impl PathBufOrStdin { - pub fn read_to_string(&self) -> anyhow::Result { - match self { - PathBufOrStdin::Path(path) => std::fs::read_to_string(path) - .with_context(|| anyhow!("Failed to read file: {}", path.display())), - PathBufOrStdin::Stdin => { - let mut content = String::new(); - let _ = std::io::stdin() - .read_to_string(&mut content) - .with_context(|| anyhow!("Failed to read from STDIN"))?; - Ok(content) - } - } - } - - pub fn is_stdin(&self) -> bool { - match self { - PathBufOrStdin::Path(_) => false, - PathBufOrStdin::Stdin => true, - } - } -} - -impl FromStr for PathBufOrStdin { - type Err = core::convert::Infallible; - - fn from_str(s: &str) -> Result { - if s == "-" { - Ok(PathBufOrStdin::Stdin) - } else { - Ok(PathBufOrStdin::Path(PathBuf::from_str(s)?)) - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct ProfileView { - pub is_active: bool, - pub name: ProfileName, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub url: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub worker_url: Option, - #[serde(skip_serializing_if = "std::ops::Not::not", default)] - pub allow_insecure: bool, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub authenticated: Option, - pub config: ProfileConfig, -} - -impl ProfileView { - pub fn from_profile(active: &ProfileName, profile: NamedProfile) -> Self { - let NamedProfile { name, profile } = profile; - - let authenticated = match &profile.auth { - AuthenticationConfig::OAuth2(inner) => Some(inner.data.is_some()), - AuthenticationConfig::Static(_) => None, - }; - - ProfileView { - is_active: &name == active, - name, - url: profile.custom_url, - worker_url: profile.custom_worker_url, - allow_insecure: profile.allow_insecure, - authenticated, - config: profile.config, - } - } -} - -#[derive(Debug, Clone)] -pub struct AccountDetails { - pub account_id: AccountId, - pub email: String, -} - -impl From for AccountDetails { - fn from(value: golem_client::model::Account) -> Self { - Self { - account_id: value.id, - email: value.email.into_inner(), - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] -#[clap(rename_all = "kebab-case")] -pub enum EnforcementActionArg { - Throttle, - Reject, - Terminate, -} - -impl Display for EnforcementActionArg { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - EnforcementActionArg::Throttle => write!(f, "throttle"), - EnforcementActionArg::Reject => write!(f, "reject"), - EnforcementActionArg::Terminate => write!(f, "terminate"), - } - } -} - -impl FromStr for EnforcementActionArg { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "throttle" => Ok(Self::Throttle), - "reject" => Ok(Self::Reject), - "terminate" => Ok(Self::Terminate), - _ => Err(format!( - "Unknown enforcement actions: {s}. Expected one of \"throttle\", \"reject\", \"terminate\"" - )), - } - } -} - -impl From for EnforcementAction { - fn from(value: EnforcementActionArg) -> Self { - match value { - EnforcementActionArg::Throttle => Self::Throttle, - EnforcementActionArg::Terminate => Self::Terminate, - EnforcementActionArg::Reject => Self::Reject, - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] -#[clap(rename_all = "lower")] -pub enum ProviderKindArg { - Google, - Facebook, - Microsoft, - Gitlab, - Custom, -} - -impl From for ProviderKind { - fn from(value: ProviderKindArg) -> Self { - match value { - ProviderKindArg::Google => ProviderKind::Google, - ProviderKindArg::Facebook => ProviderKind::Facebook, - ProviderKindArg::Microsoft => ProviderKind::Microsoft, - ProviderKindArg::Gitlab => ProviderKind::Gitlab, - ProviderKindArg::Custom => ProviderKind::Custom, - } - } -} diff --git a/cli/golem-cli/src/model/text/profile.rs b/cli/golem-cli/src/model/text/profile.rs index 99ace2d5fa..d109b417cf 100644 --- a/cli/golem-cli/src/model/text/profile.rs +++ b/cli/golem-cli/src/model/text/profile.rs @@ -15,8 +15,8 @@ use crate::config::ProfileConfig; use crate::config::ProfileName; use crate::log::{LogColorize, logln}; -use crate::model::ProfileView; use crate::model::cli_output::StructuredOutput; +use crate::model::config::ProfileView; use crate::model::format::Format; use crate::model::masking::Masked; use crate::model::text::fmt::*; diff --git a/cli/golem-cli/src/model/text/template.rs b/cli/golem-cli/src/model/text/template.rs index 151bbc586e..dde1088e5b 100644 --- a/cli/golem-cli/src/model/text/template.rs +++ b/cli/golem-cli/src/model/text/template.rs @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::app::template::TemplateDescription; use crate::log::current_indent_width; -use crate::model::TemplateDescription; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::*; use itertools::Itertools; From c5467fa1ca5dc6b06ad4bb6667f24c435255e477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 17:37:33 +0200 Subject: [PATCH 33/70] dedup permission-grant text helpers into text/grant.rs --- cli/golem-cli/src/model/text/account.rs | 15 ++----------- cli/golem-cli/src/model/text/card.rs | 9 +------- cli/golem-cli/src/model/text/grant.rs | 30 +++++++++++++++++++++++++ cli/golem-cli/src/model/text/mod.rs | 1 + 4 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 cli/golem-cli/src/model/text/grant.rs diff --git a/cli/golem-cli/src/model/text/account.rs b/cli/golem-cli/src/model/text/account.rs index 23e215d4e1..352fff4cfc 100644 --- a/cli/golem-cli/src/model/text/account.rs +++ b/cli/golem-cli/src/model/text/account.rs @@ -15,9 +15,10 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; use crate::model::text::fmt::*; +use crate::model::text::grant::{format_grants, grant_count}; use golem_client::model::{Account, PermissionShare}; use golem_common::model::account::AccountId; -use golem_common::model::permission_share::{PermissionShareData, PermissionShareId}; +use golem_common::model::permission_share::PermissionShareId; use serde::{Deserialize, Serialize}; fn account_fields(account: &Account) -> Vec<(String, String)> { @@ -123,18 +124,6 @@ fn permission_share_fields(share: &PermissionShare) -> Vec<(String, String)> { fields.build() } -fn format_grants(grants: &[String]) -> String { - if grants.is_empty() { - "(none)".to_string() - } else { - grants.join("\n") - } -} - -fn grant_count(data: &PermissionShareData) -> usize { - data.lower_positive.len() + data.lower_negative.len() -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionShareGetView(pub PermissionShare); diff --git a/cli/golem-cli/src/model/text/card.rs b/cli/golem-cli/src/model/text/card.rs index 987098867c..5be642a8f7 100644 --- a/cli/golem-cli/src/model/text/card.rs +++ b/cli/golem-cli/src/model/text/card.rs @@ -15,6 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; use crate::model::text::fmt::*; +use crate::model::text::grant::format_grants; use golem_client::model::{CardManagedBy, StoredCard}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -236,11 +237,3 @@ fn format_ids(ids: &[Uuid]) -> String { .join("\n") } } - -fn format_grants(grants: &[String]) -> String { - if grants.is_empty() { - "(none)".to_string() - } else { - grants.join("\n") - } -} diff --git a/cli/golem-cli/src/model/text/grant.rs b/cli/golem-cli/src/model/text/grant.rs new file mode 100644 index 0000000000..7f127e3e51 --- /dev/null +++ b/cli/golem-cli/src/model/text/grant.rs @@ -0,0 +1,30 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Text rendering helpers for permission grants, shared by the permission-share +//! and card views. + +use golem_common::model::permission_share::PermissionShareData; + +pub(crate) fn format_grants(grants: &[String]) -> String { + if grants.is_empty() { + "(none)".to_string() + } else { + grants.join("\n") + } +} + +pub(crate) fn grant_count(data: &PermissionShareData) -> usize { + data.lower_positive.len() + data.lower_negative.len() +} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 77b91ea13b..4ba089eaf0 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -21,6 +21,7 @@ pub mod deployment; pub mod diff; pub mod environment; pub mod fmt; +pub mod grant; pub mod help; pub mod http_api_deployment; pub mod http_api_domain; From 9f9c10d075888333a022b09b45fd4539f725b53e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 18:24:30 +0200 Subject: [PATCH 34/70] move GuestLanguage to model/language.rs with domain-mirrored paths --- cli/golem-cli/src/app/build/check/agents.rs | 4 +- cli/golem-cli/src/app/build/check/mod.rs | 2 +- .../src/app/build/check/requirements.rs | 2 +- cli/golem-cli/src/app/build/check/rust.rs | 2 +- cli/golem-cli/src/app/build/check/skills.rs | 2 +- cli/golem-cli/src/app/build/check/ts.rs | 2 +- cli/golem-cli/src/app/build/gen_bridge.rs | 2 +- cli/golem-cli/src/app/build/mod.rs | 2 +- .../src/app/build/task_result_marker.rs | 3 +- cli/golem-cli/src/app/context.rs | 3 +- cli/golem-cli/src/app/edit/tests.rs | 2 +- cli/golem-cli/src/app/template/description.rs | 2 +- cli/golem-cli/src/app/template/repo.rs | 4 +- cli/golem-cli/src/app/template/template.rs | 2 +- cli/golem-cli/src/command.rs | 4 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- .../src/command_handler/app/template.rs | 2 +- cli/golem-cli/src/command_handler/bridge.rs | 2 +- .../src/command_handler/component/mod.rs | 2 +- cli/golem-cli/src/command_handler/repl/mod.rs | 2 +- .../src/command_handler/repl/typescript.rs | 2 +- cli/golem-cli/src/command_handler/secret.rs | 2 +- cli/golem-cli/src/model/app.rs | 26 +++-- cli/golem-cli/src/model/app_raw.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 10 +- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/language.rs | 104 ++++++++++++++++++ cli/golem-cli/src/model/mod.rs | 92 +--------------- cli/golem-cli/src/model/repl.rs | 2 +- cli/golem-cli/tests/app/agents.rs | 2 +- cli/golem-cli/tests/app/app.rs | 2 +- .../tests/app/build_and_deploy_all.rs | 2 +- cli/golem-cli/tests/bridge_gen/fixtures.rs | 2 +- cli/golem-cli/tests/bridge_gen/moonbit.rs | 2 +- cli/golem-cli/tests/bridge_gen/rust.rs | 2 +- cli/golem-cli/tests/bridge_gen/type_naming.rs | 2 +- cli/golem-cli/tests/bridge_gen/typescript.rs | 2 +- 37 files changed, 163 insertions(+), 143 deletions(-) create mode 100644 cli/golem-cli/src/model/language.rs diff --git a/cli/golem-cli/src/app/build/check/agents.rs b/cli/golem-cli/src/app/build/check/agents.rs index fb95cb1c3a..d4a029f14f 100644 --- a/cli/golem-cli/src/app/build/check/agents.rs +++ b/cli/golem-cli/src/app/build/check/agents.rs @@ -17,7 +17,7 @@ use crate::app::context::BuildContext; use crate::app::edit; use crate::app::template::AppTemplateRepo; use crate::fs; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use anyhow::anyhow; use std::collections::BTreeSet; use std::path::Path; @@ -97,7 +97,7 @@ mod tests { use super::managed_guide_differs; use crate::app::edit::agents_md; use crate::app::template::AppTemplateRepo; - use crate::model::GuestLanguage; + use crate::model::language::GuestLanguage; use std::path::Path; use test_r::test; diff --git a/cli/golem-cli/src/app/build/check/mod.rs b/cli/golem-cli/src/app/build/check/mod.rs index d48569328d..21385ff995 100644 --- a/cli/golem-cli/src/app/build/check/mod.rs +++ b/cli/golem-cli/src/app/build/check/mod.rs @@ -31,7 +31,7 @@ use crate::app::context::{BuildContext, validated_to_anyhow}; use crate::app::edit::golem_yaml; use crate::fs; use crate::log::LogColorize; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use crate::process::which; use crate::sdk_overrides::sdk_overrides; use crate::validation::ValidationBuilder; diff --git a/cli/golem-cli/src/app/build/check/requirements.rs b/cli/golem-cli/src/app/build/check/requirements.rs index 8d34782b6f..0ad4b469c3 100644 --- a/cli/golem-cli/src/app/build/check/requirements.rs +++ b/cli/golem-cli/src/app/build/check/requirements.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use crate::versions; #[derive(Clone, Copy, Debug)] diff --git a/cli/golem-cli/src/app/build/check/rust.rs b/cli/golem-cli/src/app/build/check/rust.rs index 94eb06060a..baed5de3ee 100644 --- a/cli/golem-cli/src/app/build/check/rust.rs +++ b/cli/golem-cli/src/app/build/check/rust.rs @@ -20,7 +20,7 @@ use crate::app::context::BuildContext; use crate::app::edit; use crate::app::edit::cargo_toml::{DependencySpec, DependencyTable}; use crate::fs; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use crate::sdk_overrides::{RustDependency, SdkOverrides}; use crate::versions; use std::collections::BTreeMap; diff --git a/cli/golem-cli/src/app/build/check/skills.rs b/cli/golem-cli/src/app/build/check/skills.rs index ffefc02aa7..3c463b4e57 100644 --- a/cli/golem-cli/src/app/build/check/skills.rs +++ b/cli/golem-cli/src/app/build/check/skills.rs @@ -17,7 +17,7 @@ use crate::app::context::BuildContext; use crate::app::template::AppTemplateRepo; use crate::fs; use crate::log::log_warn; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use anyhow::Context; use anyhow::bail; use std::collections::btree_map::Entry; diff --git a/cli/golem-cli/src/app/build/check/ts.rs b/cli/golem-cli/src/app/build/check/ts.rs index ca311e51a1..23750a8398 100644 --- a/cli/golem-cli/src/app/build/check/ts.rs +++ b/cli/golem-cli/src/app/build/check/ts.rs @@ -338,7 +338,7 @@ mod test { #[test] fn ts_tsconfig_requires_bundler_resolution_without_decorators() { use crate::app::build::check::requirements::typescript_tsconfig_requirements; - use crate::model::GuestLanguage; + use crate::model::language::GuestLanguage; let setting_keys = |language| -> Vec<&'static str> { typescript_tsconfig_requirements(language) diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index c646b25c34..e24737afa2 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -16,11 +16,11 @@ use crate::error::NonSuccessfulExit; use crate::fs; use crate::log::log_error; use crate::log::{LogColorize, LogIndent, log_action, log_skipping_up_to_date, logln}; -use crate::model::GuestLanguage; use crate::model::app::{ BridgeSdkTarget, BridgeSdkTargetKind, BridgeSdkTargetSubject, ComponentDependency, CustomBridgeSdkTarget, }; +use crate::model::language::GuestLanguage; use crate::model::repl::{ReplAgentMetadata, ReplMetadata}; use anyhow::bail; use camino::Utf8PathBuf; diff --git a/cli/golem-cli/src/app/build/mod.rs b/cli/golem-cli/src/app/build/mod.rs index 6c85d3e440..113251ba3b 100644 --- a/cli/golem-cli/src/app/build/mod.rs +++ b/cli/golem-cli/src/app/build/mod.rs @@ -28,10 +28,10 @@ use crate::app::context::BuildContext; use crate::bridge_gen::BridgeMode; use crate::error::NonSuccessfulExit; use crate::log::{LogColorize, LogIndent, log_action, log_error, logln}; -use crate::model::GuestLanguage; use crate::model::app::{ AppBuildStep, BridgeSdkTarget, ComponentDependency, CustomBridgeSdkTarget, }; +use crate::model::language::GuestLanguage; use golem_common::model::agent::AgentTypeName; use golem_common::model::agent::extraction::ExtractedComponentMetadata; use golem_common::model::component::ComponentName; diff --git a/cli/golem-cli/src/app/build/task_result_marker.rs b/cli/golem-cli/src/app/build/task_result_marker.rs index 013d282b79..006f70e02a 100644 --- a/cli/golem-cli/src/app/build/task_result_marker.rs +++ b/cli/golem-cli/src/app/build/task_result_marker.rs @@ -16,10 +16,11 @@ use crate::app::build::task_result_marker::TaskResultMarkerHashSourceKind::{Hash use crate::bridge_gen::BridgeMode; use crate::fs; use crate::log::log_warn_action; +use crate::model::app_raw; use crate::model::app_raw::{ GenerateQuickJSCrate, GenerateQuickJSDTS, InjectToPrebuiltQuickJs, PreinitializeJs, }; -use crate::model::{GuestLanguage, app_raw}; +use crate::model::language::GuestLanguage; use anyhow::{Context, anyhow, bail}; use golem_common::model::agent::AgentTypeName; use golem_common::model::component::{ComponentName, ComponentRevision}; diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 1f02be0b7d..d352123e42 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -29,12 +29,13 @@ use crate::model::app::{ CustomBridgeSdkTarget, DynamicHelpSections, LoadedRawApps, ResolvedLocalServer, WithSource, includes_from_yaml_file, }; +use crate::model::app_raw; use crate::model::format::Format; +use crate::model::language::GuestLanguage; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::DecoratedIndent; use crate::model::text::fmt::format_component_applied_layers; use crate::model::text::server::ToFormattedServerContext; -use crate::model::{GuestLanguage, app_raw}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/app/edit/tests.rs b/cli/golem-cli/src/app/edit/tests.rs index 59e72e9a54..10c0049d8a 100644 --- a/cli/golem-cli/src/app/edit/tests.rs +++ b/cli/golem-cli/src/app/edit/tests.rs @@ -18,7 +18,7 @@ use crate::app::edit::{ agents_md, cargo_toml, gitignore, golem_yaml, json, main_rs, main_ts, package_json, tsconfig_json, }; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use proptest::prelude::*; use serde::Serialize; use serde_json::{Map as JsonMap, Value as JsonValue}; diff --git a/cli/golem-cli/src/app/template/description.rs b/cli/golem-cli/src/app/template/description.rs index 1e506162dd..215be9420f 100644 --- a/cli/golem-cli/src/app/template/description.rs +++ b/cli/golem-cli/src/app/template/description.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::app::template::AppTemplate; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use serde::{Deserialize, Serialize}; /// A summary view of an [`AppTemplate`] for CLI output (the `app templates` listing). diff --git a/cli/golem-cli/src/app/template/repo.rs b/cli/golem-cli/src/app/template/repo.rs index edf6627270..4debd4ac35 100644 --- a/cli/golem-cli/src/app/template/repo.rs +++ b/cli/golem-cli/src/app/template/repo.rs @@ -19,7 +19,7 @@ use crate::app::template::template::{ AppTemplateComponent, AppTemplatesForLanguage, }; use crate::fs; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use anyhow::{Context, anyhow, bail}; use include_dir::{Dir, include_dir}; use std::collections::{BTreeMap, HashSet}; @@ -330,7 +330,7 @@ impl AppTemplateRepo { #[cfg(test)] mod tests { use super::AppTemplateRepo; - use crate::model::GuestLanguage; + use crate::model::language::GuestLanguage; use std::fs as stdfs; use std::path::{Path, PathBuf}; use test_r::test; diff --git a/cli/golem-cli/src/app/template/template.rs b/cli/golem-cli/src/app/template/template.rs index eb191657f6..9f2b34d3c1 100644 --- a/cli/golem-cli/src/app/template/template.rs +++ b/cli/golem-cli/src/app/template/template.rs @@ -19,7 +19,7 @@ use crate::app::template::generator::{ }; use crate::app::template::metadata::AppTemplateMetadata; use crate::fs; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use golem_common::base_model::application::ApplicationName; use golem_common::base_model::component::ComponentName; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 44162f2f1d..f7f11d796b 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -34,12 +34,12 @@ use crate::command::shared_args::{ use crate::command::worker::AgentSubcommand; use crate::config::ProfileName; use crate::error::ShowClapHelpTarget; -use crate::model::GuestLanguage; use crate::model::agent::{AgentUpdateMode, RawAgentId}; use crate::model::app::ComponentPresetName; use crate::model::cli_command_metadata::{CliCommandMetadata, CliMetadataFilter}; use crate::model::environment::EnvironmentReference; use crate::model::format::Format; +use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; use crate::{command_name, version}; use anyhow::{Context as AnyhowContext, anyhow}; @@ -940,9 +940,9 @@ pub enum GolemCliSubcommand { } pub mod shared_args { - use crate::model::GuestLanguage; use crate::model::agent::{AgentUpdateMode, RawAgentId}; use crate::model::app::AppBuildStep; + use crate::model::language::GuestLanguage; use clap::Args; use golem_common::model::account::AccountId; use golem_common::model::component::ComponentName; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 7c1e6f7da4..28b65dd973 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -41,7 +41,6 @@ use crate::log::{ log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; -use crate::model::GuestLanguage; use crate::model::agent::AgentUpdateMode; use crate::model::agent::view::AgentTypeView; use crate::model::app::{ @@ -55,6 +54,7 @@ use crate::model::deploy::{ preferred_source_language_for_setup, }; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; +use crate::model::language::GuestLanguage; use crate::model::text::agent::AgentTypeListView; use crate::model::text::deployment::{DeploymentListView, DeploymentNewView}; use crate::model::text::diff::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index fd27300c7e..36aef117c3 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -33,7 +33,7 @@ use crate::log::{ LogColorize, LogIndent, log_action, log_anyhow_error, log_error, log_failed_to, log_finished_ok, log_skipping_up_to_date, logln, }; -use crate::model::GuestLanguage; +use crate::model::language::GuestLanguage; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::log_text_view; use crate::model::text::help::{AppNewNextStepsHint, AppNewNextStepsMode}; diff --git a/cli/golem-cli/src/command_handler/bridge.rs b/cli/golem-cli/src/command_handler/bridge.rs index 01cc5d00b1..c64bac8039 100644 --- a/cli/golem-cli/src/command_handler/bridge.rs +++ b/cli/golem-cli/src/command_handler/bridge.rs @@ -14,8 +14,8 @@ use crate::command_handler::Handlers; use crate::context::Context; -use crate::model::GuestLanguage; use crate::model::app::{ApplicationComponentSelectMode, BuildConfig, CustomBridgeSdkTarget}; +use crate::model::language::GuestLanguage; use golem_common::model::agent::AgentTypeName; use golem_common::model::component::ComponentName; use std::path::PathBuf; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index cf32f7107e..5a3e45a27b 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -24,7 +24,6 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_error, log_warn_action, logln}; -use crate::model::GuestLanguage; use crate::model::agent::AgentUpdateMode; use crate::model::app::BuildConfig; use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; @@ -42,6 +41,7 @@ use crate::model::deploy::{ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; +use crate::model::language::GuestLanguage; use crate::model::text::action_result::{ AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; diff --git a/cli/golem-cli/src/command_handler/repl/mod.rs b/cli/golem-cli/src/command_handler/repl/mod.rs index 4fa670f831..bbad9ef6f8 100644 --- a/cli/golem-cli/src/command_handler/repl/mod.rs +++ b/cli/golem-cli/src/command_handler/repl/mod.rs @@ -19,12 +19,12 @@ use crate::command_handler::repl::typescript::TypeScriptRepl; use crate::config::{builtin_local_url, uses_default_builtin_local_url}; use crate::context::Context; use crate::fs; -use crate::model::GuestLanguage; use crate::model::app::{ApplicationComponentSelectMode, BuildConfig}; use crate::model::app_raw::{BuiltinServer, Server}; use crate::model::component::ComponentNameMatchKind; use crate::model::deploy::DeployConfig; use crate::model::environment::EnvironmentResolveMode; +use crate::model::language::GuestLanguage; use crate::model::repl::{BridgeReplArgs, ReplLanguage, ReplMetadata, ReplScriptSource}; use anyhow::bail; use golem_client::LOCAL_WELL_KNOWN_TOKEN; diff --git a/cli/golem-cli/src/command_handler/repl/typescript.rs b/cli/golem-cli/src/command_handler/repl/typescript.rs index b38f11e59d..a2450c44f1 100644 --- a/cli/golem-cli/src/command_handler/repl/typescript.rs +++ b/cli/golem-cli/src/command_handler/repl/typescript.rs @@ -22,8 +22,8 @@ use crate::command_handler::repl::load_repl_metadata; use crate::command_handler::repl::supervisor::{ReplCommandSpec, reload_channel, run_repl_session}; use crate::context::Context; use crate::log::{LogIndent, Output, log_action, log_skipping_up_to_date, logln, set_log_output}; -use crate::model::GuestLanguage; use crate::model::app::BuildConfig; +use crate::model::language::GuestLanguage; use crate::model::repl::{BridgeReplArgs, ReplMetadata, ReplScriptSource}; use crate::process::{CommandExt, ExitStatusExt, which}; use crate::sdk_overrides::sdk_overrides; diff --git a/cli/golem-cli/src/command_handler/secret.rs b/cli/golem-cli/src/command_handler/secret.rs index daa511dd37..9476c0b57b 100644 --- a/cli/golem-cli/src/command_handler/secret.rs +++ b/cli/golem-cli/src/command_handler/secret.rs @@ -19,8 +19,8 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_error; -use crate::model::GuestLanguage; use crate::model::environment::EnvironmentResolveMode; +use crate::model::language::GuestLanguage; use crate::model::text::secret::{ SecretCreateView, SecretDeleteView, SecretGetView, SecretListView, SecretUpdateView, }; diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 4802233177..871422cff0 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -19,6 +19,7 @@ use crate::bridge_gen::{ use crate::fs; use crate::log::LogColorize; use crate::model::app::app_builder::{build_application, build_application_preload}; +use crate::model::app_raw; use crate::model::cascade::layer::Layer; use crate::model::cascade::property::Property; use crate::model::cascade::property::json::JsonProperty; @@ -26,9 +27,9 @@ use crate::model::cascade::property::map::{MapMergeMode, MapProperty}; use crate::model::cascade::property::optional::OptionalProperty; use crate::model::cascade::property::vec::{VecMergeMode, VecProperty}; use crate::model::cascade::store::Store; +use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; use crate::model::template::Template; -use crate::model::{GuestLanguage, app_raw}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{Context, anyhow}; use golem_common::model::agent::AgentTypeName; @@ -4491,7 +4492,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &alpha_agent, - crate::model::GuestLanguage::Rust, + crate::model::language::GuestLanguage::Rust, BridgeMode::External ), app_tmp_dir @@ -4505,7 +4506,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &beta_agent, - crate::model::GuestLanguage::Rust, + crate::model::language::GuestLanguage::Rust, BridgeMode::External ), app_tmp_dir @@ -4547,7 +4548,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &alpha_agent, - crate::model::GuestLanguage::Rust, + crate::model::language::GuestLanguage::Rust, BridgeMode::External ), app.temp_dir() @@ -4561,7 +4562,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &alpha_agent, - crate::model::GuestLanguage::Rust, + crate::model::language::GuestLanguage::Rust, BridgeMode::Guest ), app.temp_dir() @@ -4602,7 +4603,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &alpha_agent, - crate::model::GuestLanguage::Rust, + crate::model::language::GuestLanguage::Rust, BridgeMode::Guest ), app_tmp_dir @@ -4639,7 +4640,7 @@ mod test { let (app, app_tmp_dir) = load_app_for_env(source, "local", &[]); assert_eq!( - app.tool_bridge_sdk_dir("MyTool", crate::model::GuestLanguage::Rust), + app.tool_bridge_sdk_dir("MyTool", crate::model::language::GuestLanguage::Rust), app_tmp_dir.path().join("bridge-sdk/rust-guest").join( crate::bridge_gen::tool_bridge_client_directory_name("MyTool") ) @@ -4647,7 +4648,7 @@ mod test { let used_modes = app.bridge_sdks().for_all_used_modes(); assert_eq!(used_modes.len(), 1); - assert_eq!(used_modes[0].0, crate::model::GuestLanguage::Rust); + assert_eq!(used_modes[0].0, crate::model::language::GuestLanguage::Rust); assert_eq!(used_modes[0].1, BridgeMode::Guest); } @@ -4675,7 +4676,7 @@ mod test { let (app, app_tmp_dir) = load_app_for_env(source, "local", &[]); assert_eq!( - app.tool_bridge_sdk_dir("MyTool", crate::model::GuestLanguage::MoonBit), + app.tool_bridge_sdk_dir("MyTool", crate::model::language::GuestLanguage::MoonBit), app_tmp_dir.path().join("bridge-sdk/moonbit-guest").join( crate::bridge_gen::tool_bridge_client_directory_name("MyTool") ) @@ -4683,7 +4684,10 @@ mod test { let used_modes = app.bridge_sdks().for_all_used_modes(); assert_eq!(used_modes.len(), 1); - assert_eq!(used_modes[0].0, crate::model::GuestLanguage::MoonBit); + assert_eq!( + used_modes[0].0, + crate::model::language::GuestLanguage::MoonBit + ); assert_eq!(used_modes[0].1, BridgeMode::Guest); } @@ -4999,7 +5003,7 @@ mod test { assert_eq!( app.bridge_sdk_dir( &agent_type_name, - crate::model::GuestLanguage::Scala, + crate::model::language::GuestLanguage::Scala, BridgeMode::Guest ), app_tmp_dir diff --git a/cli/golem-cli/src/model/app_raw.rs b/cli/golem-cli/src/model/app_raw.rs index acea46b456..176de1e29b 100644 --- a/cli/golem-cli/src/model/app_raw.rs +++ b/cli/golem-cli/src/model/app_raw.rs @@ -14,10 +14,10 @@ use crate::bridge_gen::BridgeMode; use crate::log::LogColorize; -use crate::model::GuestLanguage; use crate::model::cascade::property::map::MapMergeMode; use crate::model::cascade::property::vec::VecMergeMode; use crate::model::format::Format; +use crate::model::language::GuestLanguage; use crate::{APP_MANIFEST_JSON_SCHEMA, fs}; use anyhow::{Context, anyhow}; use golem_common::model::agent::AgentTypeName; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index f1b6c06c77..eff6784ae2 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3761,12 +3761,12 @@ fn arb_template_description() -> BoxedStrategy BoxedStrategy { +fn arb_guest_language() -> BoxedStrategy { prop_oneof![ - Just(crate::model::GuestLanguage::TypeScript), - Just(crate::model::GuestLanguage::Rust), - Just(crate::model::GuestLanguage::Scala), - Just(crate::model::GuestLanguage::MoonBit) + Just(crate::model::language::GuestLanguage::TypeScript), + Just(crate::model::language::GuestLanguage::Rust), + Just(crate::model::language::GuestLanguage::Scala), + Just(crate::model::language::GuestLanguage::MoonBit) ] .boxed() } diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index 32c5fd02e6..b3e69a11ff 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -15,11 +15,11 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::command::shared_args::{ForceBuildArg, PostDeployArgs}; use crate::error::service::ServiceError; -use crate::model::GuestLanguage; use crate::model::agent::RawAgentId; use crate::model::component::{ render_agent_constructor, render_input_schema, render_output_schema, }; +use crate::model::language::GuestLanguage; use crate::model::masking::{ MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, }; diff --git a/cli/golem-cli/src/model/language.rs b/cli/golem-cli/src/model/language.rs new file mode 100644 index 0000000000..354beec20c --- /dev/null +++ b/cli/golem-cli/src/model/language.rs @@ -0,0 +1,104 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::ValueEnum; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Formatter}; +use std::str::FromStr; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +// NOTE: the order of languages (currently) is NOT alphabetical, rather based on recommendation +#[derive( + Debug, + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + EnumIter, + Serialize, + Deserialize, + ValueEnum, +)] +#[clap(rename_all = "lower")] +pub enum GuestLanguage { + #[value(alias = "ts")] + TypeScript, + Rust, + Scala, + MoonBit, +} + +impl GuestLanguage { + pub fn from_string(s: impl AsRef) -> Option { + match s.as_ref().to_lowercase().as_str() { + "rust" => Some(GuestLanguage::Rust), + "ts" | "typescript" => Some(GuestLanguage::TypeScript), + "scala" => Some(GuestLanguage::Scala), + "moonbit" => Some(GuestLanguage::MoonBit), + _ => None, + } + } + + pub fn from_id_string(s: impl AsRef) -> Option { + match s.as_ref().to_lowercase().as_str() { + "rust" => Some(GuestLanguage::Rust), + "ts" => Some(GuestLanguage::TypeScript), + "scala" => Some(GuestLanguage::Scala), + "moonbit" => Some(GuestLanguage::MoonBit), + _ => None, + } + } + + pub fn id(&self) -> &'static str { + match self { + GuestLanguage::Rust => "rust", + GuestLanguage::TypeScript => "ts", + GuestLanguage::Scala => "scala", + GuestLanguage::MoonBit => "moonbit", + } + } + + pub fn name(&self) -> &'static str { + match self { + GuestLanguage::Rust => "Rust", + GuestLanguage::TypeScript => "TypeScript", + GuestLanguage::Scala => "Scala", + GuestLanguage::MoonBit => "MoonBit", + } + } +} + +impl fmt::Display for GuestLanguage { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + +impl FromStr for GuestLanguage { + type Err = String; + + fn from_str(s: &str) -> Result { + GuestLanguage::from_string(s).ok_or({ + let all = GuestLanguage::iter() + .map(|x| format!("\"{x}\"")) + .collect::>() + .join(", "); + format!("Unknown guest language: {s}. Expected one of {all}") + }) + } +} diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 8718c8bd14..b70a7c9838 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -26,100 +26,10 @@ pub mod format; pub mod http_api; pub mod input; pub mod invoke_result_view; +pub mod language; pub mod masking; pub mod plugin; pub mod plugin_manifest; pub mod repl; pub mod template; pub mod text; - -use clap::ValueEnum; -use serde::{Deserialize, Serialize}; -use std::fmt::{self, Formatter}; -use std::str::FromStr; -use strum::IntoEnumIterator; -use strum_macros::EnumIter; - -// NOTE: the order of languages (currently) is NOT alphabetical, rather based on recommendation -#[derive( - Debug, - Copy, - Clone, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - EnumIter, - Serialize, - Deserialize, - ValueEnum, -)] -#[clap(rename_all = "lower")] -pub enum GuestLanguage { - #[value(alias = "ts")] - TypeScript, - Rust, - Scala, - MoonBit, -} - -impl GuestLanguage { - pub fn from_string(s: impl AsRef) -> Option { - match s.as_ref().to_lowercase().as_str() { - "rust" => Some(GuestLanguage::Rust), - "ts" | "typescript" => Some(GuestLanguage::TypeScript), - "scala" => Some(GuestLanguage::Scala), - "moonbit" => Some(GuestLanguage::MoonBit), - _ => None, - } - } - - pub fn from_id_string(s: impl AsRef) -> Option { - match s.as_ref().to_lowercase().as_str() { - "rust" => Some(GuestLanguage::Rust), - "ts" => Some(GuestLanguage::TypeScript), - "scala" => Some(GuestLanguage::Scala), - "moonbit" => Some(GuestLanguage::MoonBit), - _ => None, - } - } - - pub fn id(&self) -> &'static str { - match self { - GuestLanguage::Rust => "rust", - GuestLanguage::TypeScript => "ts", - GuestLanguage::Scala => "scala", - GuestLanguage::MoonBit => "moonbit", - } - } - - pub fn name(&self) -> &'static str { - match self { - GuestLanguage::Rust => "Rust", - GuestLanguage::TypeScript => "TypeScript", - GuestLanguage::Scala => "Scala", - GuestLanguage::MoonBit => "MoonBit", - } - } -} - -impl fmt::Display for GuestLanguage { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name()) - } -} - -impl FromStr for GuestLanguage { - type Err = String; - - fn from_str(s: &str) -> Result { - GuestLanguage::from_string(s).ok_or({ - let all = GuestLanguage::iter() - .map(|x| format!("\"{x}\"")) - .collect::>() - .join(", "); - format!("Unknown guest language: {s}. Expected one of {all}") - }) - } -} diff --git a/cli/golem-cli/src/model/repl.rs b/cli/golem-cli/src/model/repl.rs index 5c30baafc5..0d51406c7f 100644 --- a/cli/golem-cli/src/model/repl.rs +++ b/cli/golem-cli/src/model/repl.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::GuestLanguage; use crate::model::app::CustomBridgeSdkTarget; use crate::model::environment::ResolvedEnvironmentIdentity; +use crate::model::language::GuestLanguage; use clap::ValueEnum; use golem_common::base_model::agent::{AgentMode, AgentTypeName}; use golem_common::model::component::ComponentName; diff --git a/cli/golem-cli/tests/app/agents.rs b/cli/golem-cli/tests/app/agents.rs index 74155e7805..06150c9b9e 100644 --- a/cli/golem-cli/tests/app/agents.rs +++ b/cli/golem-cli/tests/app/agents.rs @@ -10,7 +10,7 @@ use anyhow::Context; use goldenfile::Mint; use golem_cli::fs; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_cli::versions; use indoc::{formatdoc, indoc}; use std::io::Write; diff --git a/cli/golem-cli/tests/app/app.rs b/cli/golem-cli/tests/app/app.rs index b845460ba7..d89e63ce2d 100644 --- a/cli/golem-cli/tests/app/app.rs +++ b/cli/golem-cli/tests/app/app.rs @@ -6,7 +6,7 @@ use crate::app::{ }; use golem_cli::fs; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_cli::versions; use golem_common::schema::SchemaType; use golem_common::schema::graph::SchemaGraph; diff --git a/cli/golem-cli/tests/app/build_and_deploy_all.rs b/cli/golem-cli/tests/app/build_and_deploy_all.rs index 0bd9cb57b3..6a8a720498 100644 --- a/cli/golem-cli/tests/app/build_and_deploy_all.rs +++ b/cli/golem-cli/tests/app/build_and_deploy_all.rs @@ -3,8 +3,8 @@ use crate::app::{TestContext, cmd, flag}; use golem_cli::bridge_gen::BridgeMode; use golem_cli::fs; -use golem_cli::model::GuestLanguage; use golem_cli::model::app::BridgeSdkTargetKind; +use golem_cli::model::language::GuestLanguage; use golem_cli::model::text::agent::AgentTypeListView; use golem_cli::model::text::template::TemplateListView; use strum::IntoEnumIterator; diff --git a/cli/golem-cli/tests/bridge_gen/fixtures.rs b/cli/golem-cli/tests/bridge_gen/fixtures.rs index 19695a08b9..e11ba1bd89 100644 --- a/cli/golem-cli/tests/bridge_gen/fixtures.rs +++ b/cli/golem-cli/tests/bridge_gen/fixtures.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::workspace_path; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_common::model::Empty; use golem_common::model::agent::{AgentConfigSource, AgentMode, AgentTypeName, Snapshotting}; use golem_common::schema::agent::AgentConfigDeclarationSchema; diff --git a/cli/golem-cli/tests/bridge_gen/moonbit.rs b/cli/golem-cli/tests/bridge_gen/moonbit.rs index b5f8524dec..2a547e4d66 100644 --- a/cli/golem-cli/tests/bridge_gen/moonbit.rs +++ b/cli/golem-cli/tests/bridge_gen/moonbit.rs @@ -27,7 +27,7 @@ use golem_cli::bridge_gen::moonbit::{ MoonBitBridgeGenerator, MoonBitBridgeMode, MoonBitTypeName, emit_schema_graph_literal, }; use golem_cli::bridge_gen::type_naming::TypeName; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_cli::sdk_overrides::workspace_root; use golem_common::model::agent::{AgentConfigSource, AgentMode}; use golem_common::schema::agent::AgentConfigDeclarationSchema; diff --git a/cli/golem-cli/tests/bridge_gen/rust.rs b/cli/golem-cli/tests/bridge_gen/rust.rs index 2082295b44..fe73be7831 100644 --- a/cli/golem-cli/tests/bridge_gen/rust.rs +++ b/cli/golem-cli/tests/bridge_gen/rust.rs @@ -20,7 +20,7 @@ use crate::bridge_gen::type_naming::test_type_naming; use camino::{Utf8Path, Utf8PathBuf}; use golem_cli::bridge_gen::rust::{RustBridgeGenerator, RustBridgeMode, RustTypeName}; use golem_cli::bridge_gen::{BridgeGenerator, BridgeMode, bridge_client_directory_name}; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_common::model::Empty; use golem_common::model::agent::{AgentConfigSource, AgentMode, AgentTypeName, Snapshotting}; use golem_common::schema::agent::{ diff --git a/cli/golem-cli/tests/bridge_gen/type_naming.rs b/cli/golem-cli/tests/bridge_gen/type_naming.rs index c5f6ff371b..c31701355b 100644 --- a/cli/golem-cli/tests/bridge_gen/type_naming.rs +++ b/cli/golem-cli/tests/bridge_gen/type_naming.rs @@ -17,7 +17,7 @@ pub use golem_cli::bridge_gen::type_naming::*; use crate::bridge_gen::fixtures::code_first_snippets_agent_type; use crate::bridge_gen::type_naming::{TypeName, TypeNaming}; use golem_cli::bridge_gen::rust::RustTypeName; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use test_r::test; pub(crate) fn test_type_naming(language: GuestLanguage, agent_name: &str) { diff --git a/cli/golem-cli/tests/bridge_gen/typescript.rs b/cli/golem-cli/tests/bridge_gen/typescript.rs index 1a9849027b..6925a1e8f0 100644 --- a/cli/golem-cli/tests/bridge_gen/typescript.rs +++ b/cli/golem-cli/tests/bridge_gen/typescript.rs @@ -26,7 +26,7 @@ use golem_cli::bridge_gen::typescript::{ use golem_cli::bridge_gen::{ BridgeGenerator, BridgeMode, bridge_client_directory_name, tool_bridge_client_directory_name, }; -use golem_cli::model::GuestLanguage; +use golem_cli::model::language::GuestLanguage; use golem_common::model::agent::AgentMode; use golem_common::schema::schema_type::{ BinaryRestrictions, DiscriminatorRule, PathDirection, PathKind, PathSpec, ResultSpec, From 60e247b371f00a77b74753a4d1cbae317376573d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 19:18:30 +0200 Subject: [PATCH 35/70] inline model::agent instance types into the agent module --- cli/golem-cli/src/model/agent/instance.rs | 322 ---------------------- cli/golem-cli/src/model/agent/mod.rs | 309 ++++++++++++++++++++- 2 files changed, 307 insertions(+), 324 deletions(-) delete mode 100644 cli/golem-cli/src/model/agent/instance.rs diff --git a/cli/golem-cli/src/model/agent/instance.rs b/cli/golem-cli/src/model/agent/instance.rs deleted file mode 100644 index c2c76b564e..0000000000 --- a/cli/golem-cli/src/model/agent/instance.rs +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::agent_id_display::SourceLanguage; -use crate::command::shared_args::StreamArgs; -use crate::model::component::ComponentNameMatchKind; -use crate::model::environment::{ - EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, -}; -use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; -use clap::ValueEnum; -use colored::control::SHOULD_COLORIZE; -use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; -use golem_common::model::account::AccountId; -use golem_common::model::agent::{AgentTypeName, ParsedAgentId}; -use golem_common::model::component::{ComponentName, ComponentRevision}; -use golem_common::model::environment::EnvironmentId; -use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; -use golem_common::model::{AgentId, AgentResourceDescription, AgentStatus, Timestamp}; -use serde_derive::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -// TODO: move things to model/agent - -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -pub struct RawAgentId(pub String); - -impl From<&str> for RawAgentId { - fn from(name: &str) -> Self { - RawAgentId(name.to_string()) - } -} - -impl From for RawAgentId { - fn from(name: String) -> Self { - RawAgentId(name) - } -} - -impl Display for RawAgentId { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] -#[clap(rename_all = "kebab-case")] -pub enum AgentUpdateMode { - #[value(alias = "auto")] - Automatic, - Manual, -} - -impl Display for AgentUpdateMode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AgentUpdateMode::Automatic => { - write!(f, "auto") - } - AgentUpdateMode::Manual => { - write!(f, "manual") - } - } - } -} - -impl FromStr for AgentUpdateMode { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "auto" => Ok(AgentUpdateMode::Automatic), - "manual" => Ok(AgentUpdateMode::Manual), - _ => Err(format!( - "Unknown agent update mode: {s}. Expected one of \"auto\", \"manual\"" - )), - } - } -} - -/// Mode selector for the `agent list` CLI command. -/// -/// The default `Durable` excludes ephemeral agents from the listing so that -/// short-lived ephemeral agents from previous runs do not clutter the default -/// output. Use `Ephemeral` to list only ephemeral agents, or `All` to list -/// agents in both modes. -#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] -#[clap(rename_all = "kebab-case")] -pub enum AgentListMode { - Durable, - Ephemeral, - All, -} - -impl Display for AgentListMode { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - AgentListMode::Durable => write!(f, "durable"), - AgentListMode::Ephemeral => write!(f, "ephemeral"), - AgentListMode::All => write!(f, "all"), - } - } -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentMetadataView { - pub component_name: ComponentName, - pub agent_id: RawAgentId, - pub created_by: AccountId, - pub environment_id: EnvironmentId, - pub env: HashMap, - pub default_env: HashMap, - pub config: Vec, - pub default_config: Vec, - pub status: AgentStatus, - pub component_revision: ComponentRevision, - pub retry_count: u32, - - pub pending_invocation_count: u64, - pub updates: Vec, - pub created_at: Timestamp, - pub last_error: Option, - pub component_size: u64, - pub total_linear_memory_size: u64, - pub exported_resource_instances: HashMap, - #[serde(skip)] - pub source_language: SourceLanguage, - #[serde(skip)] - pub secret_config_paths: BTreeSet, -} - -impl From for AgentMetadataView { - fn from(value: AgentMetadata) -> Self { - AgentMetadataView { - component_name: value.component_name, - agent_id: value.agent_id.agent_id.into(), - created_by: value.created_by, - environment_id: value.environment_id, - env: value.env, - default_env: HashMap::new(), - config: value.config, - default_config: Vec::new(), - status: value.status, - component_revision: value.component_revision, - retry_count: value.retry_count, - pending_invocation_count: value.pending_invocation_count, - updates: value.updates, - created_at: value.created_at, - last_error: value.last_error, - component_size: value.component_size, - total_linear_memory_size: value.total_linear_memory_size, - exported_resource_instances: value.exported_resource_instances, - source_language: SourceLanguage::default(), - secret_config_paths: BTreeSet::new(), - } - } -} - -impl AgentMetadataView { - pub fn with_defaults(mut self, defaults: Option) -> Self { - if let Some(defaults) = defaults { - self.default_env = defaults.env.into_iter().collect(); - self.default_config = defaults.config.into_iter().map(Into::into).collect(); - } - self - } - - pub fn with_source_language(mut self, source_language: SourceLanguage) -> Self { - self.source_language = source_language; - self - } - - pub fn with_secret_config_paths(mut self, secret_config_paths: BTreeSet) -> Self { - self.secret_config_paths = secret_config_paths; - self - } -} - -impl Masked for AgentMetadataView { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - self.env = mask_sensitive_map(config, &self.env); - self.default_env = mask_sensitive_map(config, &self.default_env); - self.config = mask_agent_config_entries(config, &self.config, &self.secret_config_paths); - self.default_config = - mask_agent_config_entries(config, &self.default_config, &self.secret_config_paths); - Ok(self) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct AgentMetadata { - pub agent_id: AgentId, - pub component_name: ComponentName, - pub environment_id: EnvironmentId, - pub created_by: AccountId, - pub env: HashMap, - pub config: Vec, - pub status: AgentStatus, - pub component_revision: ComponentRevision, - pub retry_count: u32, - pub pending_invocation_count: u64, - pub updates: Vec, - pub created_at: Timestamp, - pub last_error: Option, - pub component_size: u64, - pub total_linear_memory_size: u64, - pub exported_resource_instances: HashMap, -} - -impl AgentMetadata { - pub fn from( - component_name: ComponentName, - value: golem_client::model::AgentMetadataDto, - ) -> Self { - AgentMetadata { - agent_id: value.agent_id, - component_name, - created_by: value.created_by, - environment_id: value.environment_id, - env: value.env, - config: value.config.into_iter().map(Into::into).collect(), - status: value.status, - component_revision: value.component_revision, - retry_count: value.retry_count, - pending_invocation_count: value.pending_invocation_count, - updates: value.updates, - created_at: value.created_at, - last_error: value.last_error, - component_size: value.component_size, - total_linear_memory_size: value.total_linear_memory_size, - exported_resource_instances: HashMap::from_iter( - value - .exported_resource_instances - .into_iter() - .map(|desc| (desc.key.to_string(), desc.description)), - ), - } - } -} - -#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentsMetadataResponseView { - pub agents: Vec, - pub cursors: BTreeMap, -} - -impl Masked for AgentsMetadataResponseView { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - self.agents = self - .agents - .into_iter() - .map(|agent| agent.masked(config)) - .collect::>>()?; - Ok(self) - } -} - -#[derive(Debug, Clone)] -pub struct AgentLogStreamOptions { - pub colors: bool, - pub show_timestamp: bool, - pub show_level: bool, - /// Only show entries coming from the agent, no output about invocation markers and stream status - pub logs_only: bool, -} - -impl From for AgentLogStreamOptions { - fn from(args: StreamArgs) -> Self { - AgentLogStreamOptions { - colors: SHOULD_COLORIZE.should_colorize(), - show_timestamp: !args.stream_no_timestamp, - show_level: !args.stream_no_log_level, - logs_only: args.logs_only, - } - } -} - -pub struct AgentIdMatch { - pub environment: ResolvedEnvironmentIdentity, - pub component_name_match_kind: ComponentNameMatchKind, - pub component_name: ComponentName, - pub agent_type_name: AgentTypeName, - pub agent_id: RawAgentId, - pub source_language: SourceLanguage, - pub parsed_agent_id: Option, -} - -impl AgentIdMatch { - pub fn environment_reference(&self) -> Option<&EnvironmentReference> { - match &self.environment.source { - ResolvedEnvironmentIdentitySource::Reference(reference) => Some(reference), - ResolvedEnvironmentIdentitySource::DefaultFromManifest => None, - } - } - - /// Updates the canonical agent_id and the parsed form together. Use this - /// after re-canonicalizing or normalizing the agent id so that downstream - /// display code can use the language-specific renderer. - pub fn with_canonical_and_parsed( - mut self, - agent_id: RawAgentId, - parsed_agent_id: Option, - ) -> Self { - self.agent_id = agent_id; - self.parsed_agent_id = parsed_agent_id; - self - } -} diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index a3b557a399..6785345aab 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -13,8 +13,313 @@ // limitations under the License. pub mod extraction; -pub mod instance; pub mod stream; pub mod view; -pub use instance::*; +use crate::agent_id_display::SourceLanguage; +use crate::command::shared_args::StreamArgs; +use crate::model::component::ComponentNameMatchKind; +use crate::model::environment::{ + EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, +}; +use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; +use clap::ValueEnum; +use colored::control::SHOULD_COLORIZE; +use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; +use golem_common::model::account::AccountId; +use golem_common::model::agent::{AgentTypeName, ParsedAgentId}; +use golem_common::model::component::{ComponentName, ComponentRevision}; +use golem_common::model::environment::EnvironmentId; +use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; +use golem_common::model::{AgentId, AgentResourceDescription, AgentStatus, Timestamp}; +use serde_derive::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +pub struct RawAgentId(pub String); + +impl From<&str> for RawAgentId { + fn from(name: &str) -> Self { + RawAgentId(name.to_string()) + } +} + +impl From for RawAgentId { + fn from(name: String) -> Self { + RawAgentId(name) + } +} + +impl Display for RawAgentId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] +#[clap(rename_all = "kebab-case")] +pub enum AgentUpdateMode { + #[value(alias = "auto")] + Automatic, + Manual, +} + +impl Display for AgentUpdateMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AgentUpdateMode::Automatic => { + write!(f, "auto") + } + AgentUpdateMode::Manual => { + write!(f, "manual") + } + } + } +} + +impl FromStr for AgentUpdateMode { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "auto" => Ok(AgentUpdateMode::Automatic), + "manual" => Ok(AgentUpdateMode::Manual), + _ => Err(format!( + "Unknown agent update mode: {s}. Expected one of \"auto\", \"manual\"" + )), + } + } +} + +/// Mode selector for the `agent list` CLI command. +/// +/// The default `Durable` excludes ephemeral agents from the listing so that +/// short-lived ephemeral agents from previous runs do not clutter the default +/// output. Use `Ephemeral` to list only ephemeral agents, or `All` to list +/// agents in both modes. +#[derive(Clone, Copy, PartialEq, Eq, Debug, ValueEnum)] +#[clap(rename_all = "kebab-case")] +pub enum AgentListMode { + Durable, + Ephemeral, + All, +} + +impl Display for AgentListMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + AgentListMode::Durable => write!(f, "durable"), + AgentListMode::Ephemeral => write!(f, "ephemeral"), + AgentListMode::All => write!(f, "all"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentMetadataView { + pub component_name: ComponentName, + pub agent_id: RawAgentId, + pub created_by: AccountId, + pub environment_id: EnvironmentId, + pub env: HashMap, + pub default_env: HashMap, + pub config: Vec, + pub default_config: Vec, + pub status: AgentStatus, + pub component_revision: ComponentRevision, + pub retry_count: u32, + + pub pending_invocation_count: u64, + pub updates: Vec, + pub created_at: Timestamp, + pub last_error: Option, + pub component_size: u64, + pub total_linear_memory_size: u64, + pub exported_resource_instances: HashMap, + #[serde(skip)] + pub source_language: SourceLanguage, + #[serde(skip)] + pub secret_config_paths: BTreeSet, +} + +impl From for AgentMetadataView { + fn from(value: AgentMetadata) -> Self { + AgentMetadataView { + component_name: value.component_name, + agent_id: value.agent_id.agent_id.into(), + created_by: value.created_by, + environment_id: value.environment_id, + env: value.env, + default_env: HashMap::new(), + config: value.config, + default_config: Vec::new(), + status: value.status, + component_revision: value.component_revision, + retry_count: value.retry_count, + pending_invocation_count: value.pending_invocation_count, + updates: value.updates, + created_at: value.created_at, + last_error: value.last_error, + component_size: value.component_size, + total_linear_memory_size: value.total_linear_memory_size, + exported_resource_instances: value.exported_resource_instances, + source_language: SourceLanguage::default(), + secret_config_paths: BTreeSet::new(), + } + } +} + +impl AgentMetadataView { + pub fn with_defaults(mut self, defaults: Option) -> Self { + if let Some(defaults) = defaults { + self.default_env = defaults.env.into_iter().collect(); + self.default_config = defaults.config.into_iter().map(Into::into).collect(); + } + self + } + + pub fn with_source_language(mut self, source_language: SourceLanguage) -> Self { + self.source_language = source_language; + self + } + + pub fn with_secret_config_paths(mut self, secret_config_paths: BTreeSet) -> Self { + self.secret_config_paths = secret_config_paths; + self + } +} + +impl Masked for AgentMetadataView { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + self.env = mask_sensitive_map(config, &self.env); + self.default_env = mask_sensitive_map(config, &self.default_env); + self.config = mask_agent_config_entries(config, &self.config, &self.secret_config_paths); + self.default_config = + mask_agent_config_entries(config, &self.default_config, &self.secret_config_paths); + Ok(self) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct AgentMetadata { + pub agent_id: AgentId, + pub component_name: ComponentName, + pub environment_id: EnvironmentId, + pub created_by: AccountId, + pub env: HashMap, + pub config: Vec, + pub status: AgentStatus, + pub component_revision: ComponentRevision, + pub retry_count: u32, + pub pending_invocation_count: u64, + pub updates: Vec, + pub created_at: Timestamp, + pub last_error: Option, + pub component_size: u64, + pub total_linear_memory_size: u64, + pub exported_resource_instances: HashMap, +} + +impl AgentMetadata { + pub fn from( + component_name: ComponentName, + value: golem_client::model::AgentMetadataDto, + ) -> Self { + AgentMetadata { + agent_id: value.agent_id, + component_name, + created_by: value.created_by, + environment_id: value.environment_id, + env: value.env, + config: value.config.into_iter().map(Into::into).collect(), + status: value.status, + component_revision: value.component_revision, + retry_count: value.retry_count, + pending_invocation_count: value.pending_invocation_count, + updates: value.updates, + created_at: value.created_at, + last_error: value.last_error, + component_size: value.component_size, + total_linear_memory_size: value.total_linear_memory_size, + exported_resource_instances: HashMap::from_iter( + value + .exported_resource_instances + .into_iter() + .map(|desc| (desc.key.to_string(), desc.description)), + ), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentsMetadataResponseView { + pub agents: Vec, + pub cursors: BTreeMap, +} + +impl Masked for AgentsMetadataResponseView { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + self.agents = self + .agents + .into_iter() + .map(|agent| agent.masked(config)) + .collect::>>()?; + Ok(self) + } +} + +#[derive(Debug, Clone)] +pub struct AgentLogStreamOptions { + pub colors: bool, + pub show_timestamp: bool, + pub show_level: bool, + /// Only show entries coming from the agent, no output about invocation markers and stream status + pub logs_only: bool, +} + +impl From for AgentLogStreamOptions { + fn from(args: StreamArgs) -> Self { + AgentLogStreamOptions { + colors: SHOULD_COLORIZE.should_colorize(), + show_timestamp: !args.stream_no_timestamp, + show_level: !args.stream_no_log_level, + logs_only: args.logs_only, + } + } +} + +pub struct AgentIdMatch { + pub environment: ResolvedEnvironmentIdentity, + pub component_name_match_kind: ComponentNameMatchKind, + pub component_name: ComponentName, + pub agent_type_name: AgentTypeName, + pub agent_id: RawAgentId, + pub source_language: SourceLanguage, + pub parsed_agent_id: Option, +} + +impl AgentIdMatch { + pub fn environment_reference(&self) -> Option<&EnvironmentReference> { + match &self.environment.source { + ResolvedEnvironmentIdentitySource::Reference(reference) => Some(reference), + ResolvedEnvironmentIdentitySource::DefaultFromManifest => None, + } + } + + /// Updates the canonical agent_id and the parsed form together. Use this + /// after re-canonicalizing or normalizing the agent id so that downstream + /// display code can use the language-specific renderer. + pub fn with_canonical_and_parsed( + mut self, + agent_id: RawAgentId, + parsed_agent_id: Option, + ) -> Self { + self.agent_id = agent_id; + self.parsed_agent_id = parsed_agent_id; + self + } +} From bdacd98e8410425c1dd19bb26a6279032e557a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 19:30:12 +0200 Subject: [PATCH 36/70] inline model::agent AgentTypeView into the agent module --- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/model/agent/mod.rs | 23 +++++++++++-- cli/golem-cli/src/model/agent/view.rs | 35 -------------------- cli/golem-cli/src/model/cli_output/tests.rs | 2 +- cli/golem-cli/src/model/text/agent/mod.rs | 2 +- 5 files changed, 23 insertions(+), 41 deletions(-) delete mode 100644 cli/golem-cli/src/model/agent/view.rs diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 28b65dd973..ba40eec5d4 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -41,8 +41,8 @@ use crate::log::{ log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; +use crate::model::agent::AgentTypeView; use crate::model::agent::AgentUpdateMode; -use crate::model::agent::view::AgentTypeView; use crate::model::app::{ AppBuildStep, ApplicationComponentSelectMode, BuildConfig, CleanMode, DynamicHelpSections, WithSource, diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index 6785345aab..f0b7b1e753 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -14,11 +14,10 @@ pub mod extraction; pub mod stream; -pub mod view; use crate::agent_id_display::SourceLanguage; use crate::command::shared_args::StreamArgs; -use crate::model::component::ComponentNameMatchKind; +use crate::model::component::{ComponentNameMatchKind, render_agent_constructor}; use crate::model::environment::{ EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, }; @@ -27,7 +26,7 @@ use clap::ValueEnum; use colored::control::SHOULD_COLORIZE; use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; use golem_common::model::account::AccountId; -use golem_common::model::agent::{AgentTypeName, ParsedAgentId}; +use golem_common::model::agent::{AgentTypeName, DeployedRegisteredAgentType, ParsedAgentId}; use golem_common::model::component::{ComponentName, ComponentRevision}; use golem_common::model::environment::EnvironmentId; use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; @@ -323,3 +322,21 @@ impl AgentIdMatch { self } } + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentTypeView { + pub agent_type: String, + pub constructor: String, + pub description: String, +} + +impl AgentTypeView { + pub fn new(value: &DeployedRegisteredAgentType, wrapper_naming: bool) -> Self { + Self { + agent_type: value.agent_type.type_name.to_string(), + constructor: render_agent_constructor(&value.agent_type, wrapper_naming, false), + description: value.agent_type.description.clone(), + } + } +} diff --git a/cli/golem-cli/src/model/agent/view.rs b/cli/golem-cli/src/model/agent/view.rs deleted file mode 100644 index 0ec197d002..0000000000 --- a/cli/golem-cli/src/model/agent/view.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::model::component::render_agent_constructor; -use golem_common::model::agent::DeployedRegisteredAgentType; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentTypeView { - pub agent_type: String, - pub constructor: String, - pub description: String, -} - -impl AgentTypeView { - pub fn new(value: &DeployedRegisteredAgentType, wrapper_naming: bool) -> Self { - Self { - agent_type: value.agent_type.type_name.to_string(), - constructor: render_agent_constructor(&value.agent_type, wrapper_naming, false), - description: value.agent_type.description.clone(), - } - } -} diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index eff6784ae2..4a4153f25a 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -2090,7 +2090,7 @@ fn arb_generate_bridge_result() -> OutputDocumentStrategy { fn arb_agent_type_get_result() -> OutputDocumentStrategy { serialized_output( (arb_small_string(), arb_small_string(), arb_small_string()).prop_map( - |(agent_type, constructor, description)| crate::model::agent::view::AgentTypeView { + |(agent_type, constructor, description)| crate::model::agent::AgentTypeView { agent_type, constructor, description, diff --git a/cli/golem-cli/src/model/text/agent/mod.rs b/cli/golem-cli/src/model/text/agent/mod.rs index 4caa81890f..c6d090242d 100644 --- a/cli/golem-cli/src/model/text/agent/mod.rs +++ b/cli/golem-cli/src/model/text/agent/mod.rs @@ -16,7 +16,7 @@ pub mod instance; pub use instance::*; -use crate::model::agent::view::AgentTypeView; +use crate::model::agent::AgentTypeView; use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; use crate::model::text::fmt::{ From b30140ade7d066b87ec233f34c966867c8e203e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 19:58:56 +0200 Subject: [PATCH 37/70] consolidate plugin model, drop dead PluginReference --- cli/golem-cli/src/command_handler/plugin.rs | 2 +- cli/golem-cli/src/model/mod.rs | 1 - cli/golem-cli/src/model/plugin.rs | 100 +++++--------------- cli/golem-cli/src/model/plugin_manifest.rs | 43 --------- 4 files changed, 25 insertions(+), 121 deletions(-) delete mode 100644 cli/golem-cli/src/model/plugin_manifest.rs diff --git a/cli/golem-cli/src/command_handler/plugin.rs b/cli/golem-cli/src/command_handler/plugin.rs index af98528204..77e5eb4cb2 100644 --- a/cli/golem-cli/src/command_handler/plugin.rs +++ b/cli/golem-cli/src/command_handler/plugin.rs @@ -19,7 +19,7 @@ use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; use crate::model::environment::EnvironmentResolveMode; use crate::model::input::PathBufOrStdin; -use crate::model::plugin_manifest::{PluginManifest, PluginTypeSpecificManifest}; +use crate::model::plugin::{PluginManifest, PluginTypeSpecificManifest}; use crate::model::text::plugin::{ PluginListEntry, PluginListView, PluginRegistrationGetView, PluginRegistrationRegisterView, PluginSource, PluginUnregisterResult, diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index b70a7c9838..8c24cde503 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -29,7 +29,6 @@ pub mod invoke_result_view; pub mod language; pub mod masking; pub mod plugin; -pub mod plugin_manifest; pub mod repl; pub mod template; pub mod text; diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index c37ffb300e..f56edf177c 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -12,84 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::log::LogColorize; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum PluginReference { - RelativeToCurrentAccount { - name: String, - version: String, - }, - FullyQualified { - account_email: String, - name: String, - version: String, - }, -} - -impl PluginReference { - pub fn account_email(&self) -> Option { - match self { - Self::FullyQualified { account_email, .. } => Some(account_email.clone()), - Self::RelativeToCurrentAccount { .. } => None, - } - } - - pub fn plugin_name(&self) -> String { - match self { - Self::FullyQualified { name, .. } => name.clone(), - Self::RelativeToCurrentAccount { name, .. } => name.clone(), - } - } - - pub fn plugin_version(&self) -> String { - match self { - Self::FullyQualified { version, .. } => version.clone(), - Self::RelativeToCurrentAccount { version, .. } => version.clone(), - } - } +use golem_common::model::component::ComponentRevision; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use std::path::PathBuf; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum PluginTypeSpecificManifest { + OplogProcessor(OplogProcessorManifest), } -impl FromStr for PluginReference { - type Err = String; - - fn from_str(s: &str) -> Result { - let mut segments = s.split("/").collect::>(); - match segments.len() { - 2 => { - let version = segments.pop().unwrap().to_string(); - let name = segments.pop().unwrap().to_string(); - Ok(Self::RelativeToCurrentAccount { name, version }) - } - 3 => { - let version = segments.pop().unwrap().to_string(); - let name = segments.pop().unwrap().to_string(); - let account_email = segments.pop().unwrap().to_string(); - Ok(Self::FullyQualified { - account_email, - name, - version, - }) - } - _ => Err(format!( - "Unknown format for plugin: {}. Expected either / or //", - s.log_color_highlight() - )), - } - } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct OplogProcessorManifest { + pub component_id: Uuid, + pub component_revision: ComponentRevision, } -impl Display for PluginReference { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - Self::RelativeToCurrentAccount { name, version } => write!(f, "{name}/{version}"), - Self::FullyQualified { - account_email, - name, - version, - } => write!(f, "{account_email}/{name}/{version}"), - } - } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginManifest { + pub name: String, + pub version: String, + pub description: String, + pub icon: PathBuf, + pub homepage: String, + pub specs: PluginTypeSpecificManifest, } diff --git a/cli/golem-cli/src/model/plugin_manifest.rs b/cli/golem-cli/src/model/plugin_manifest.rs deleted file mode 100644 index f56edf177c..0000000000 --- a/cli/golem-cli/src/model/plugin_manifest.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use golem_common::model::component::ComponentRevision; -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use std::path::PathBuf; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum PluginTypeSpecificManifest { - OplogProcessor(OplogProcessorManifest), -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct OplogProcessorManifest { - pub component_id: Uuid, - pub component_revision: ComponentRevision, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginManifest { - pub name: String, - pub version: String, - pub description: String, - pub icon: PathBuf, - pub homepage: String, - pub specs: PluginTypeSpecificManifest, -} From ecc61cc9d1184d2dd09668f09d8dd7a579671904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 22:35:51 +0200 Subject: [PATCH 38/70] move agent action-result views into text/agent --- .../src/command_handler/agent/mod.rs | 2 +- .../src/command_handler/component/mod.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 30 +-- cli/golem-cli/src/model/text/action_result.rs | 172 --------------- .../src/model/text/agent/action_result.rs | 200 ++++++++++++++++++ cli/golem-cli/src/model/text/agent/mod.rs | 1 + 6 files changed, 218 insertions(+), 189 deletions(-) create mode 100644 cli/golem-cli/src/model/text/agent/action_result.rs diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 734cb7d394..fe959f90d6 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -32,7 +32,7 @@ use crate::log::{ use crate::model::component::ComponentNameMatchKind; use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersResult}; use crate::model::invoke_result_view::InvokeResultView; -use crate::model::text::action_result::{ +use crate::model::text::agent::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, }; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 5a3e45a27b..e677cbaf6b 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -42,7 +42,7 @@ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::language::GuestLanguage; -use crate::model::text::action_result::{ +use crate::model::text::agent::action_result::{ AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; use crate::model::text::component::{ diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 4a4153f25a..1504af6cbd 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -2817,11 +2817,11 @@ fn arb_agent_update_meta() -> BoxedStrategy BoxedStrategy { +-> BoxedStrategy { arb_agent_transition_fields() .prop_map( |(component_name, agent_id, from_revision, revision, from_version, version)| { - crate::model::text::action_result::AgentRedeploymentMeta { + crate::model::text::agent::action_result::AgentRedeploymentMeta { component_name, agent_id, from_revision, @@ -2834,11 +2834,11 @@ fn arb_agent_redeployment_meta() .boxed() } -fn arb_agent_deletion_meta() -> BoxedStrategy -{ +fn arb_agent_deletion_meta() +-> BoxedStrategy { (arb_small_string(), arb_small_string()) .prop_map(|(component_name, agent_id)| { - crate::model::text::action_result::AgentDeletionMeta { + crate::model::text::agent::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), agent_id: crate::model::agent::RawAgentId(agent_id), } @@ -3009,7 +3009,7 @@ fn arb_agent_resource_description() -> BoxedStrategy OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, agent)| { - crate::model::text::action_result::AgentDeleteResult { + crate::model::text::agent::action_result::AgentDeleteResult { deleted, agent_id: agent, } @@ -3027,7 +3027,7 @@ fn arb_agent_file_contents_result() -> OutputDocumentStrategy { arb_small_u64(), ) .prop_map(|(saved, agent, path, output_path, bytes)| { - crate::model::text::action_result::AgentFileContentsResult { + crate::model::text::agent::action_result::AgentFileContentsResult { saved, agent_id: agent, path, @@ -3041,7 +3041,7 @@ fn arb_agent_file_contents_result() -> OutputDocumentStrategy { fn arb_agent_interrupt_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(interrupted, agent)| { - crate::model::text::action_result::AgentInterruptResult { + crate::model::text::agent::action_result::AgentInterruptResult { interrupted, agent_id: agent, } @@ -3052,7 +3052,7 @@ fn arb_agent_interrupt_result() -> OutputDocumentStrategy { fn arb_agent_resume_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(resumed, agent)| { - crate::model::text::action_result::AgentResumeResult { + crate::model::text::agent::action_result::AgentResumeResult { resumed, agent_id: agent, } @@ -3063,7 +3063,7 @@ fn arb_agent_resume_result() -> OutputDocumentStrategy { fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(simulated, agent)| { - crate::model::text::action_result::AgentSimulateCrashResult { + crate::model::text::agent::action_result::AgentSimulateCrashResult { simulated, agent_id: agent, } @@ -3363,7 +3363,7 @@ fn arb_agent_cancel_invocation_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string(), arb_small_string()).prop_map( |(canceled, agent, idempotency_key)| { - crate::model::text::action_result::AgentCancelInvocationResult { + crate::model::text::agent::action_result::AgentCancelInvocationResult { canceled, agent_id: agent, idempotency_key, @@ -3380,7 +3380,7 @@ fn arb_agent_delete_all_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_agent_deletion_meta(), 0..5), ) .prop_map(|(deleted, agents)| { - crate::model::text::action_result::AgentDeleteAllResult { deleted, agents } + crate::model::text::agent::action_result::AgentDeleteAllResult { deleted, agents } }), ) } @@ -3392,7 +3392,7 @@ fn arb_agent_redeploy_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_agent_redeployment_meta(), 0..5), ) .prop_map(|(redeployed, agents)| { - crate::model::text::action_result::AgentRedeployResult { redeployed, agents } + crate::model::text::agent::action_result::AgentRedeployResult { redeployed, agents } }), ) } @@ -3407,7 +3407,7 @@ fn arb_agent_revert_result() -> OutputDocumentStrategy { ) .prop_map( |(reverted, agent, last_oplog_index, number_of_invocations)| { - crate::model::text::action_result::AgentRevertResult { + crate::model::text::agent::action_result::AgentRevertResult { reverted, agent_id: agent, last_oplog_index, @@ -3427,7 +3427,7 @@ fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { 0i32..1000, ) .prop_map(|(activated, agent, plugin, priority)| { - crate::model::text::action_result::AgentPluginToggleResult { + crate::model::text::agent::action_result::AgentPluginToggleResult { activated, agent_id: agent, plugin, diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs index 76f55aac3f..5a4b177ba7 100644 --- a/cli/golem-cli/src/model/text/action_result.rs +++ b/cli/golem-cli/src/model/text/action_result.rs @@ -22,183 +22,11 @@ //! to stderr (see `Context::new`) and these structured payloads are //! emitted on stdout so that automation can rely on a stable schema. -use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; use crate::model::text::fmt::{NoTextOutput, TextOutput}; -use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentDeleteResult { - pub deleted: bool, - pub agent_id: String, -} - -impl NoTextOutput for AgentDeleteResult {} -impl TextOutput for AgentDeleteResult {} - -impl StructuredOutput for AgentDeleteResult { - const KIND: &'static str = "agent.delete"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentFileContentsResult { - pub saved: bool, - pub agent_id: String, - pub path: String, - pub output_path: PathBuf, - pub bytes: usize, -} - -impl NoTextOutput for AgentFileContentsResult {} -impl TextOutput for AgentFileContentsResult {} - -impl StructuredOutput for AgentFileContentsResult { - const KIND: &'static str = "agent.file-contents"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentInterruptResult { - pub interrupted: bool, - pub agent_id: String, -} - -impl NoTextOutput for AgentInterruptResult {} -impl TextOutput for AgentInterruptResult {} - -impl StructuredOutput for AgentInterruptResult { - const KIND: &'static str = "agent.interrupt"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentResumeResult { - pub resumed: bool, - pub agent_id: String, -} - -impl NoTextOutput for AgentResumeResult {} -impl TextOutput for AgentResumeResult {} - -impl StructuredOutput for AgentResumeResult { - const KIND: &'static str = "agent.resume"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentSimulateCrashResult { - pub simulated: bool, - pub agent_id: String, -} - -impl NoTextOutput for AgentSimulateCrashResult {} -impl TextOutput for AgentSimulateCrashResult {} - -impl StructuredOutput for AgentSimulateCrashResult { - const KIND: &'static str = "agent.simulate-crash"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentRevertResult { - pub reverted: bool, - pub agent_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub last_oplog_index: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub number_of_invocations: Option, -} - -impl NoTextOutput for AgentRevertResult {} -impl TextOutput for AgentRevertResult {} - -impl StructuredOutput for AgentRevertResult { - const KIND: &'static str = "agent.revert"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentCancelInvocationResult { - pub canceled: bool, - pub agent_id: String, - pub idempotency_key: String, -} - -impl NoTextOutput for AgentCancelInvocationResult {} -impl TextOutput for AgentCancelInvocationResult {} - -impl StructuredOutput for AgentCancelInvocationResult { - const KIND: &'static str = "agent.cancel-invocation"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentRedeployResult { - pub redeployed: bool, - pub agents: Vec, -} - -impl NoTextOutput for AgentRedeployResult {} -impl TextOutput for AgentRedeployResult {} - -impl StructuredOutput for AgentRedeployResult { - const KIND: &'static str = "agent.redeploy"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentRedeploymentMeta { - pub component_name: ComponentName, - pub agent_id: RawAgentId, - pub from_revision: ComponentRevision, - pub revision: ComponentRevision, - #[serde(skip_serializing_if = "Option::is_none")] - pub from_version: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub version: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentDeleteAllResult { - pub deleted: bool, - pub agents: Vec, -} - -impl NoTextOutput for AgentDeleteAllResult {} -impl TextOutput for AgentDeleteAllResult {} - -impl StructuredOutput for AgentDeleteAllResult { - const KIND: &'static str = "agent.delete-all"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentDeletionMeta { - pub component_name: ComponentName, - pub agent_id: RawAgentId, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentPluginToggleResult { - pub activated: bool, - pub agent_id: String, - pub plugin: String, - pub priority: i32, -} - -impl NoTextOutput for AgentPluginToggleResult {} -impl TextOutput for AgentPluginToggleResult {} - -impl StructuredOutput for AgentPluginToggleResult { - const KIND: &'static str = "agent.plugin-toggle"; -} - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CleanResult { diff --git a/cli/golem-cli/src/model/text/agent/action_result.rs b/cli/golem-cli/src/model/text/agent/action_result.rs new file mode 100644 index 0000000000..fa024aea04 --- /dev/null +++ b/cli/golem-cli/src/model/text/agent/action_result.rs @@ -0,0 +1,200 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Lightweight structured result views for commands whose human-readable +//! output is mostly progress text printed during the run. +//! +//! Each view implements `NoTextOutput`: when `--format text` is used +//! (the default), the user has already seen the progress lines on stdout +//! and adding another rendering of the same information would just be +//! noise. When `--format json/yaml/toon` is used, the progress text is routed +//! to stderr (see `Context::new`) and these structured payloads are +//! emitted on stdout so that automation can rely on a stable schema. + +use crate::model::agent::RawAgentId; +use crate::model::cli_output::StructuredOutput; +use crate::model::text::fmt::{NoTextOutput, TextOutput}; +use golem_common::model::component::{ComponentName, ComponentRevision}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDeleteResult { + pub deleted: bool, + pub agent_id: String, +} + +impl NoTextOutput for AgentDeleteResult {} +impl TextOutput for AgentDeleteResult {} + +impl StructuredOutput for AgentDeleteResult { + const KIND: &'static str = "agent.delete"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentFileContentsResult { + pub saved: bool, + pub agent_id: String, + pub path: String, + pub output_path: PathBuf, + pub bytes: usize, +} + +impl NoTextOutput for AgentFileContentsResult {} +impl TextOutput for AgentFileContentsResult {} + +impl StructuredOutput for AgentFileContentsResult { + const KIND: &'static str = "agent.file-contents"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentInterruptResult { + pub interrupted: bool, + pub agent_id: String, +} + +impl NoTextOutput for AgentInterruptResult {} +impl TextOutput for AgentInterruptResult {} + +impl StructuredOutput for AgentInterruptResult { + const KIND: &'static str = "agent.interrupt"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentResumeResult { + pub resumed: bool, + pub agent_id: String, +} + +impl NoTextOutput for AgentResumeResult {} +impl TextOutput for AgentResumeResult {} + +impl StructuredOutput for AgentResumeResult { + const KIND: &'static str = "agent.resume"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentSimulateCrashResult { + pub simulated: bool, + pub agent_id: String, +} + +impl NoTextOutput for AgentSimulateCrashResult {} +impl TextOutput for AgentSimulateCrashResult {} + +impl StructuredOutput for AgentSimulateCrashResult { + const KIND: &'static str = "agent.simulate-crash"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentRevertResult { + pub reverted: bool, + pub agent_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub last_oplog_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub number_of_invocations: Option, +} + +impl NoTextOutput for AgentRevertResult {} +impl TextOutput for AgentRevertResult {} + +impl StructuredOutput for AgentRevertResult { + const KIND: &'static str = "agent.revert"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentCancelInvocationResult { + pub canceled: bool, + pub agent_id: String, + pub idempotency_key: String, +} + +impl NoTextOutput for AgentCancelInvocationResult {} +impl TextOutput for AgentCancelInvocationResult {} + +impl StructuredOutput for AgentCancelInvocationResult { + const KIND: &'static str = "agent.cancel-invocation"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentRedeployResult { + pub redeployed: bool, + pub agents: Vec, +} + +impl NoTextOutput for AgentRedeployResult {} +impl TextOutput for AgentRedeployResult {} + +impl StructuredOutput for AgentRedeployResult { + const KIND: &'static str = "agent.redeploy"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentRedeploymentMeta { + pub component_name: ComponentName, + pub agent_id: RawAgentId, + pub from_revision: ComponentRevision, + pub revision: ComponentRevision, + #[serde(skip_serializing_if = "Option::is_none")] + pub from_version: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDeleteAllResult { + pub deleted: bool, + pub agents: Vec, +} + +impl NoTextOutput for AgentDeleteAllResult {} +impl TextOutput for AgentDeleteAllResult {} + +impl StructuredOutput for AgentDeleteAllResult { + const KIND: &'static str = "agent.delete-all"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentDeletionMeta { + pub component_name: ComponentName, + pub agent_id: RawAgentId, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentPluginToggleResult { + pub activated: bool, + pub agent_id: String, + pub plugin: String, + pub priority: i32, +} + +impl NoTextOutput for AgentPluginToggleResult {} +impl TextOutput for AgentPluginToggleResult {} + +impl StructuredOutput for AgentPluginToggleResult { + const KIND: &'static str = "agent.plugin-toggle"; +} diff --git a/cli/golem-cli/src/model/text/agent/mod.rs b/cli/golem-cli/src/model/text/agent/mod.rs index c6d090242d..8bd53d2e9f 100644 --- a/cli/golem-cli/src/model/text/agent/mod.rs +++ b/cli/golem-cli/src/model/text/agent/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod action_result; pub mod instance; pub use instance::*; From 80a9bbc6c044318063d99f17f7978bdf59a524d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 23:17:25 +0200 Subject: [PATCH 39/70] split text/agent/instance into mod, oplog and files submodules --- .../src/command_handler/agent/mod.rs | 5 +- cli/golem-cli/src/model/cli_output/tests.rs | 14 +- cli/golem-cli/src/model/text/agent/files.rs | 64 ++ cli/golem-cli/src/model/text/agent/mod.rs | 530 +++++++++++++++- .../text/agent/{instance.rs => oplog.rs} | 574 +----------------- 5 files changed, 606 insertions(+), 581 deletions(-) create mode 100644 cli/golem-cli/src/model/text/agent/files.rs rename cli/golem-cli/src/model/text/agent/{instance.rs => oplog.rs} (72%) diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index fe959f90d6..6762d05baf 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -36,9 +36,10 @@ use crate::model::text::agent::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, }; +use crate::model::text::agent::files::{AgentFilesView, FileNodeView}; +use crate::model::text::agent::oplog::AgentOplogEntryView; use crate::model::text::agent::{ - AgentCreateView, AgentFilesView, AgentGetView, AgentOplogEntryView, FileNodeView, - format_agent_id_match, format_timestamp, + AgentCreateView, AgentGetView, format_agent_id_match, format_timestamp, }; use crate::model::text::fmt::{log_fuzzy_match, log_text_view}; use crate::model::text::help::{ diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 1504af6cbd..f92cb24ed8 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -1117,7 +1117,7 @@ fn cli_output_schema_validates_schema_native_component_and_agent_outputs() { agent_types: vec![agent_type], }) .expect("agent-type.list should serialize"), - to_structured_output_value(crate::model::text::agent::AgentOplogEntryView { + to_structured_output_value(crate::model::text::agent::oplog::AgentOplogEntryView { index: 0, entry: sample_public_oplog_entries() .into_iter() @@ -2578,11 +2578,11 @@ fn arb_path_segment() -> BoxedStrategy fn arb_agent_files_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::agent::AgentFilesView { nodes }), + .prop_map(|nodes| crate::model::text::agent::files::AgentFilesView { nodes }), ) } -fn arb_file_node() -> BoxedStrategy { +fn arb_file_node() -> BoxedStrategy { ( arb_small_string(), arb_small_string(), @@ -2591,7 +2591,7 @@ fn arb_file_node() -> BoxedStrategy { arb_small_u64(), ) .prop_map(|(name, last_modified, kind, permissions, size)| { - crate::model::text::agent::FileNodeView { + crate::model::text::agent::files::FileNodeView { name, last_modified, kind, @@ -2670,9 +2670,9 @@ fn arb_agent_oplog_result() -> OutputDocumentStrategy { arb_typed_value_oplog_entry(), ], ) - .prop_map( - |(index, entry)| crate::model::text::agent::AgentOplogEntryView { index, entry }, - ), + .prop_map(|(index, entry)| { + crate::model::text::agent::oplog::AgentOplogEntryView { index, entry } + }), ) } diff --git a/cli/golem-cli/src/model/text/agent/files.rs b/cli/golem-cli/src/model/text/agent/files.rs new file mode 100644 index 0000000000..3436695935 --- /dev/null +++ b/cli/golem-cli/src/model/text/agent/files.rs @@ -0,0 +1,64 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::log::logln; +use crate::model::cli_output::StructuredOutput; +use crate::model::text::fmt::*; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentFilesView { + pub nodes: Vec, +} + +impl StructuredOutput for AgentFilesView { + const KIND: &'static str = "agent.files"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FileNodeView { + pub name: String, + pub last_modified: String, // Human-readable timestamp + pub kind: String, + pub permissions: String, + pub size: u64, +} + +impl TextOutput for AgentFilesView { + fn log(&self) { + if self.nodes.is_empty() { + logln("No files found."); + } else { + let mut table = new_table_full_condensed(vec![ + Column::new("Name"), + Column::new("Kind").fixed(), + Column::new("Permissions").fixed(), + Column::new("Size").fixed_right(), + Column::new("Last Modified").fixed_right(), + ]); + for node in &self.nodes { + table.add_row(vec![ + node.name.clone(), + node.kind.clone(), + node.permissions.clone(), + format_binary_size(&node.size), + node.last_modified.clone(), + ]); + } + log_table(table); + } + } +} diff --git a/cli/golem-cli/src/model/text/agent/mod.rs b/cli/golem-cli/src/model/text/agent/mod.rs index 8bd53d2e9f..0f4962ae65 100644 --- a/cli/golem-cli/src/model/text/agent/mod.rs +++ b/cli/golem-cli/src/model/text/agent/mod.rs @@ -13,19 +13,34 @@ // limitations under the License. pub mod action_result; -pub mod instance; +pub mod files; +pub mod oplog; -pub use instance::*; +use crate::log::{LogColorize, logln}; +use crate::model::agent::{ + AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, +}; +use crate::model::cli_output::StructuredOutput; +use crate::model::deploy::TryUpdateAllWorkersResult; +use crate::model::environment::EnvironmentReference; +use crate::model::invoke_result_view::InvokeResultView; +use crate::model::masking::{Masked, MaskingConfig}; +use crate::model::text::fmt::*; +use chrono::DateTime; use crate::model::agent::AgentTypeView; -use crate::model::cli_output::StructuredOutput; -use crate::model::masking::Masked; -use crate::model::text::fmt::{ - Column, FieldsBuilder, MessageWithFields, TextOutput, format_message_highlight, log_table, - new_table_full_condensed, -}; +use colored::Colorize; +use comfy_table::Color as ComfyColor; +use golem_common::model::AgentStatus; use golem_common::model::agent::DeployedRegisteredAgentType; +use golem_common::model::component::ComponentName; +use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; +use indoc::indoc; +use itertools::Itertools; +use serde::Serializer; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::Write; impl MessageWithFields for AgentTypeView { fn message(&self) -> String { @@ -82,3 +97,502 @@ impl TextOutput for AgentTypeListView { log_table(table); } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentCreateView { + pub component_name: ComponentName, + pub agent_id: RawAgentId, +} + +impl Masked for AgentCreateView {} + +impl MessageWithFields for AgentCreateView { + fn message(&self) -> String { + format!( + "Created new agent {}", + format_message_highlight(&self.agent_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields + .fmt_field("Component name", &self.component_name, format_id) + .fmt_field("Agent ID", &self.agent_id, |agent_id| { + format_agent_id_in( + &agent_id.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }); + + fields.build() + } +} + +impl StructuredOutput for AgentCreateView { + const KIND: &'static str = "agent.new"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentGetView { + pub metadata: AgentMetadataView, + pub precise: bool, +} + +impl AgentGetView { + pub fn from_metadata(metadata: AgentMetadataView, precise: bool) -> Self { + Self { metadata, precise } + } +} + +impl Masked for AgentGetView { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + self.metadata = self.metadata.masked(config)?; + Ok(self) + } +} + +fn format_untyped_config(config: &[AgentConfigEntryDto]) -> String { + config + .iter() + .map(|entry| { + format!( + "{}={}", + entry.path.join(".").log_color_highlight(), + entry.value.0 + ) + }) + .join("\n") +} + +fn to_sorted_btree_map(map: &HashMap) -> BTreeMap { + map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() +} + +impl MessageWithFields for AgentGetView { + fn message(&self) -> String { + format!( + "Got metadata for agent {}", + format_message_highlight(&self.metadata.agent_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + let mut update_history = String::new(); + for update in &self.metadata.updates { + match update { + UpdateRecord::PendingUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Pending update to {}", + update.timestamp, update.target_revision + ) + .bright_black() + ); + } + UpdateRecord::SuccessfulUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Successful update to {}", + update.timestamp, update.target_revision + ) + .green() + .bold() + ); + } + UpdateRecord::FailedUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Failed update to {}{}", + update.timestamp, + update.target_revision, + update + .details + .as_ref() + .map(|details| format!(": {details}")) + .unwrap_or_default() + ) + .yellow() + ); + } + } + } + + fields + .fmt_field("Component name", &self.metadata.component_name, format_id) + .fmt_field( + "Component revision", + &self.metadata.component_revision, + format_id, + ) + .fmt_field("Agent ID", &self.metadata.agent_id, |agent_id| { + format_agent_id_in( + &agent_id.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }) + .field("Created at", &self.metadata.created_at) + .fmt_field( + "Component size", + &self.metadata.component_size, + format_binary_size, + ) + .fmt_field( + "Total linear memory size", + &self.metadata.total_linear_memory_size, + format_binary_size, + ) + .fmt_field_optional( + "Environment variables - defaults", + &self.metadata.default_env, + !self.metadata.default_env.is_empty(), + |env| format_env(&to_sorted_btree_map(env)), + ) + .fmt_field_optional( + "Environment variables - overrides", + &self.metadata.env, + !self.metadata.env.is_empty(), + |env| format_env(&to_sorted_btree_map(env)), + ) + .fmt_field_optional( + "Config - defaults", + &self.metadata.default_config, + !self.metadata.default_config.is_empty(), + |config| format_untyped_config(config), + ) + .fmt_field_optional( + "Config - overrides", + &self.metadata.config, + !self.metadata.config.is_empty(), + |config| format_untyped_config(config), + ) + .fmt_field_optional("Status", &self.metadata.status, self.precise, format_status) + .fmt_field_optional( + "Retry count", + &self.metadata.retry_count, + self.precise, + format_retry_count, + ) + .fmt_field_optional( + "Pending invocation count", + &self.metadata.pending_invocation_count, + self.metadata.pending_invocation_count > 0, + |n| n.to_string(), + ) + .fmt_field_optional( + "Last error", + &self.metadata.last_error, + self.metadata.last_error.is_some() && self.precise, + |err| format_stack(err.as_ref().unwrap()), + ) + .fmt_field_optional( + "WARNING", + "The presented agent metadata may not be up-to-date", + !self.precise, + format_warn, + ); + + fields.build() + } +} + +impl StructuredOutput for AgentGetView { + const KIND: &'static str = "agent.get"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl StructuredOutput for AgentsMetadataResponseView { + const KIND: &'static str = "agent.list"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl StructuredOutput for TryUpdateAllWorkersResult { + const KIND: &'static str = "agent.update"; +} + +impl TextOutput for AgentsMetadataResponseView { + fn log(&self) { + let colorize = colored::control::SHOULD_COLORIZE.should_colorize(); + let term_width = terminal_width(); + logln(Self::format_table_wide( + &self.agents, + term_width, + colorize, + false, + )); + + if !self.cursors.is_empty() { + logln(""); + } + for (component_name, cursor) in &self.cursors { + logln(format!( + "Cursor for more results for component {}: {}", + component_name.log_color_highlight(), + cursor.log_color_highlight() + )); + } + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + self.masked(config)?.log(); + Ok(()) + } +} + +/// Agent-list component-name column: capped at `MAX` so it cannot eat the agent +/// id budget, squeezable to `MIN` when ids need the room. +const MAX_COMPONENT_NAME_WIDTH: usize = 28; +const MIN_COMPONENT_NAME_WIDTH: usize = 12; + +/// Below this the agent id column is left unformatted for the table to wrap. +const MIN_AGENT_NAME_WIDTH: usize = 24; + +impl AgentsMetadataResponseView { + fn status_color(status: &AgentStatus, colorize: bool) -> ComfyColor { + if colorize { + match status { + AgentStatus::Running => ComfyColor::Green, + AgentStatus::Idle => ComfyColor::Cyan, + AgentStatus::Suspended => ComfyColor::Yellow, + AgentStatus::Interrupted => ComfyColor::Red, + AgentStatus::Retrying => ComfyColor::Yellow, + AgentStatus::Failed => ComfyColor::Red, + AgentStatus::Exited => ComfyColor::White, + } + } else { + ComfyColor::Reset + } + } + + fn format_table_wide( + agents: &[AgentMetadataView], + term_width: u16, + colorize: bool, + full_width: bool, + ) -> String { + // Agent ids are self-formatted (broken at their own structure), so the + // column width must be known before the cells are built; + // `self_formatting_table` budgets it. `Range` marks the component name as + // the squeezable column. + let headers = vec![ + Column::new("Component name") + .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), + Column::new("Agent ID"), + Column::new("Revision").content_right(), + Column::new("Status").content_right(), + Column::new("Pending").content_right(), + Column::new("Created at").content(), + ]; + + let format_agent_id = |raw: &str, width: Option| match width { + Some(width) => format_agent_id_in(raw, colorize, width), + None => raw.to_string(), + }; + + let rows = agents + .iter() + .map(|agent| { + vec![ + TableCell::new(agent.component_name.to_string()), + TableCell::new(agent.agent_id.0.clone()), + TableCell::new(agent.component_revision.to_string()).right(), + TableCell::new(agent.status.to_string()) + .right() + .color(Self::status_color(&agent.status, colorize)), + TableCell::new(agent.pending_invocation_count.to_string()).right(), + TableCell::new(agent.created_at.to_string()), + ] + }) + .collect(); + + self_formatting_table(SelfFormattingTableSpec { + preset: TablePreset::FullCondensed, + term_width, + full_width, + headers, + flex: FlexColumn { + index: 1, + min_width: MIN_AGENT_NAME_WIDTH, + format: &format_agent_id, + }, + rows, + }) + .to_string() + } +} + +impl TruncatableTextOutput for AgentsMetadataResponseView { + fn render_truncated(&self, max_lines: usize, colorize: bool) -> String { + let cursor_lines = if self.cursors.is_empty() { + 0 + } else { + 1 + self.cursors.len() + }; + let available_for_table = max_lines.saturating_sub(cursor_lines); + + let term_width = terminal_width(); + let table_str = Self::format_table_wide(&self.agents, term_width, colorize, true); + + let mut out = truncate_rendered(table_str, available_for_table); + + if !self.cursors.is_empty() { + out.push('\n'); + for (component_name, cursor) in &self.cursors { + out.push('\n'); + out.push_str(&format!( + "Cursor for more results for component {}: {}", + component_name.log_color_highlight(), + cursor.log_color_highlight() + )); + } + } + + out + } + + fn render_truncated_masked( + &self, + max_lines: usize, + colorize: bool, + config: MaskingConfig, + ) -> anyhow::Result { + Ok(self + .clone() + .masked(config)? + .render_truncated(max_lines, colorize)) + } +} + +impl TextOutput for TryUpdateAllWorkersResult { + fn log(&self) { + // NOP + } +} + +impl TextOutput for InvokeResultView { + fn log(&self) { + fn log_result_format(format: Option<&str>, multiple: bool) { + let result_label = if multiple { "results" } else { "result" }; + match format { + Some(format) => logln(format!( + "Invocation {result_label} in {}:", + format_message_highlight(format), + )), + None => logln(format!("Invocation {result_label}:")), + } + } + + if self.is_void_result { + log_result_format(None, false); + logln("void"); + return; + } + + if self.result.is_none() && self.result_json.is_none() { + return; + } + + if let Some(result) = &self.result { + log_result_format(self.result_format.as_deref(), false); + logln(result); + } else if let Some(json) = &self.result_json { + logln(format_warn(indoc!( + " + Failed to convert invocation result to the requested format. + At the moment it does not support Handle (aka Resource) data type. + " + ))); + log_result_format(Some("JSON"), false); + logln(serde_json::to_string_pretty(json).unwrap()); + } + } +} + +/// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). +fn format_agent_id_in(agent_id: &str, colorize: bool, width: usize) -> String { + crate::agent_id_display::format_agent_id_for_terminal(agent_id, colorize, Some(width)) +} + +// Helper function to convert Unix timestamp to human-readable format +pub fn format_timestamp(timestamp: u64) -> String { + if let Some(datetime) = DateTime::from_timestamp(timestamp as i64, 0) { + datetime.format("%Y-%m-%d %H:%M:%S").to_string() + } else { + format!("{timestamp}") // Fallback to raw timestamp if conversion fails + } +} + +pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { + let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, + &agent_id_match.agent_id.0, + ); + + format!( + "{}{}/{}", + match &agent_id_match.environment_reference() { + Some(environment_reference) => { + match environment_reference { + EnvironmentReference::Environment { environment_name } => { + format!("{}/", environment_name.0.blue().bold()) + } + EnvironmentReference::ApplicationEnvironment { + application_name, + environment_name, + } => { + format!( + "{}/{}/", + application_name.0.blue().bold(), + environment_name.0.blue().bold() + ) + } + EnvironmentReference::AccountApplicationEnvironment { + account_email, + application_name, + environment_name, + } => { + format!( + "{}/{}/{}/", + account_email.blue().bold(), + application_name.0.blue().bold(), + environment_name.0.blue().bold() + ) + } + } + } + None => "".to_string(), + }, + agent_id_match.component_name.0.blue().bold(), + rendered_agent_id.green().bold(), + ) +} diff --git a/cli/golem-cli/src/model/text/agent/instance.rs b/cli/golem-cli/src/model/text/agent/oplog.rs similarity index 72% rename from cli/golem-cli/src/model/text/agent/instance.rs rename to cli/golem-cli/src/model/text/agent/oplog.rs index c83b054d26..9c01923452 100644 --- a/cli/golem-cli/src/model/text/agent/instance.rs +++ b/cli/golem-cli/src/model/text/agent/oplog.rs @@ -13,476 +13,28 @@ // limitations under the License. use crate::agent_id_display::SourceLanguage; -use crate::log::{LogColorize, logln}; -use crate::model::agent::{ - AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, -}; +use crate::log::logln; use crate::model::cli_output::StructuredOutput; -use crate::model::deploy::TryUpdateAllWorkersResult; -use crate::model::environment::EnvironmentReference; -use crate::model::invoke_result_view::InvokeResultView; -use crate::model::masking::{Masked, MaskingConfig}; use crate::model::text::fmt::*; use base64::Engine; use base64::prelude::BASE64_STANDARD; -use chrono::DateTime; - -use colored::Colorize; -use comfy_table::Color as ComfyColor; -use golem_common::model::component::ComponentName; +use golem_common::model::Timestamp; use golem_common::model::oplog::{ MultipartPartData, PluginInstallationDescription, PublicAgentInvocation, PublicAgentInvocationResult, PublicAttributeValue, PublicOplogEntry, PublicSnapshotData, PublicUpdateDescription, StringAttributeValue, }; -use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; -use golem_common::model::{AgentStatus, Timestamp}; use golem_common::schema::TypedSchemaValue; -use indoc::indoc; -use itertools::Itertools; -use serde::Serializer; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap}; -use std::fmt::Write; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentCreateView { - pub component_name: ComponentName, - pub agent_id: RawAgentId, -} - -impl Masked for AgentCreateView {} - -impl MessageWithFields for AgentCreateView { - fn message(&self) -> String { - format!( - "Created new agent {}", - format_message_highlight(&self.agent_id) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("Component name", &self.component_name, format_id) - .fmt_field("Agent ID", &self.agent_id, |agent_id| { - format_agent_id_in( - &agent_id.0, - colored::control::SHOULD_COLORIZE.should_colorize(), - field_value_width::(), - ) - }); - - fields.build() - } -} - -impl StructuredOutput for AgentCreateView { - const KIND: &'static str = "agent.new"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentGetView { - pub metadata: AgentMetadataView, - pub precise: bool, -} - -impl AgentGetView { - pub fn from_metadata(metadata: AgentMetadataView, precise: bool) -> Self { - Self { metadata, precise } - } -} - -impl Masked for AgentGetView { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - self.metadata = self.metadata.masked(config)?; - Ok(self) - } -} - -fn format_untyped_config(config: &[AgentConfigEntryDto]) -> String { - config - .iter() - .map(|entry| { - format!( - "{}={}", - entry.path.join(".").log_color_highlight(), - entry.value.0 - ) - }) - .join("\n") -} - -fn to_sorted_btree_map(map: &HashMap) -> BTreeMap { - map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() -} - -impl MessageWithFields for AgentGetView { - fn message(&self) -> String { - format!( - "Got metadata for agent {}", - format_message_highlight(&self.metadata.agent_id) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - let mut update_history = String::new(); - for update in &self.metadata.updates { - match update { - UpdateRecord::PendingUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Pending update to {}", - update.timestamp, update.target_revision - ) - .bright_black() - ); - } - UpdateRecord::SuccessfulUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Successful update to {}", - update.timestamp, update.target_revision - ) - .green() - .bold() - ); - } - UpdateRecord::FailedUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Failed update to {}{}", - update.timestamp, - update.target_revision, - update - .details - .as_ref() - .map(|details| format!(": {details}")) - .unwrap_or_default() - ) - .yellow() - ); - } - } - } - - fields - .fmt_field("Component name", &self.metadata.component_name, format_id) - .fmt_field( - "Component revision", - &self.metadata.component_revision, - format_id, - ) - .fmt_field("Agent ID", &self.metadata.agent_id, |agent_id| { - format_agent_id_in( - &agent_id.0, - colored::control::SHOULD_COLORIZE.should_colorize(), - field_value_width::(), - ) - }) - .field("Created at", &self.metadata.created_at) - .fmt_field( - "Component size", - &self.metadata.component_size, - format_binary_size, - ) - .fmt_field( - "Total linear memory size", - &self.metadata.total_linear_memory_size, - format_binary_size, - ) - .fmt_field_optional( - "Environment variables - defaults", - &self.metadata.default_env, - !self.metadata.default_env.is_empty(), - |env| format_env(&to_sorted_btree_map(env)), - ) - .fmt_field_optional( - "Environment variables - overrides", - &self.metadata.env, - !self.metadata.env.is_empty(), - |env| format_env(&to_sorted_btree_map(env)), - ) - .fmt_field_optional( - "Config - defaults", - &self.metadata.default_config, - !self.metadata.default_config.is_empty(), - |config| format_untyped_config(config), - ) - .fmt_field_optional( - "Config - overrides", - &self.metadata.config, - !self.metadata.config.is_empty(), - |config| format_untyped_config(config), - ) - .fmt_field_optional("Status", &self.metadata.status, self.precise, format_status) - .fmt_field_optional( - "Retry count", - &self.metadata.retry_count, - self.precise, - format_retry_count, - ) - .fmt_field_optional( - "Pending invocation count", - &self.metadata.pending_invocation_count, - self.metadata.pending_invocation_count > 0, - |n| n.to_string(), - ) - .fmt_field_optional( - "Last error", - &self.metadata.last_error, - self.metadata.last_error.is_some() && self.precise, - |err| format_stack(err.as_ref().unwrap()), - ) - .fmt_field_optional( - "WARNING", - "The presented agent metadata may not be up-to-date", - !self.precise, - format_warn, - ); - - fields.build() - } -} - -impl StructuredOutput for AgentGetView { - const KIND: &'static str = "agent.get"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl StructuredOutput for AgentsMetadataResponseView { - const KIND: &'static str = "agent.list"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl StructuredOutput for TryUpdateAllWorkersResult { - const KIND: &'static str = "agent.update"; -} - -impl TextOutput for AgentsMetadataResponseView { - fn log(&self) { - let colorize = colored::control::SHOULD_COLORIZE.should_colorize(); - let term_width = terminal_width(); - logln(Self::format_table_wide( - &self.agents, - term_width, - colorize, - false, - )); - - if !self.cursors.is_empty() { - logln(""); - } - for (component_name, cursor) in &self.cursors { - logln(format!( - "Cursor for more results for component {}: {}", - component_name.log_color_highlight(), - cursor.log_color_highlight() - )); - } - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - self.masked(config)?.log(); - Ok(()) - } -} - -/// Agent-list component-name column: capped at `MAX` so it cannot eat the agent -/// id budget, squeezable to `MIN` when ids need the room. -const MAX_COMPONENT_NAME_WIDTH: usize = 28; -const MIN_COMPONENT_NAME_WIDTH: usize = 12; - -/// Below this the agent id column is left unformatted for the table to wrap. -const MIN_AGENT_NAME_WIDTH: usize = 24; - -impl AgentsMetadataResponseView { - fn status_color(status: &AgentStatus, colorize: bool) -> ComfyColor { - if colorize { - match status { - AgentStatus::Running => ComfyColor::Green, - AgentStatus::Idle => ComfyColor::Cyan, - AgentStatus::Suspended => ComfyColor::Yellow, - AgentStatus::Interrupted => ComfyColor::Red, - AgentStatus::Retrying => ComfyColor::Yellow, - AgentStatus::Failed => ComfyColor::Red, - AgentStatus::Exited => ComfyColor::White, - } - } else { - ComfyColor::Reset - } - } - - fn format_table_wide( - agents: &[AgentMetadataView], - term_width: u16, - colorize: bool, - full_width: bool, - ) -> String { - // Agent ids are self-formatted (broken at their own structure), so the - // column width must be known before the cells are built; - // `self_formatting_table` budgets it. `Range` marks the component name as - // the squeezable column. - let headers = vec![ - Column::new("Component name") - .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), - Column::new("Agent ID"), - Column::new("Revision").content_right(), - Column::new("Status").content_right(), - Column::new("Pending").content_right(), - Column::new("Created at").content(), - ]; - - let format_agent_id = |raw: &str, width: Option| match width { - Some(width) => format_agent_id_in(raw, colorize, width), - None => raw.to_string(), - }; - - let rows = agents - .iter() - .map(|agent| { - vec![ - TableCell::new(agent.component_name.to_string()), - TableCell::new(agent.agent_id.0.clone()), - TableCell::new(agent.component_revision.to_string()).right(), - TableCell::new(agent.status.to_string()) - .right() - .color(Self::status_color(&agent.status, colorize)), - TableCell::new(agent.pending_invocation_count.to_string()).right(), - TableCell::new(agent.created_at.to_string()), - ] - }) - .collect(); - - self_formatting_table(SelfFormattingTableSpec { - preset: TablePreset::FullCondensed, - term_width, - full_width, - headers, - flex: FlexColumn { - index: 1, - min_width: MIN_AGENT_NAME_WIDTH, - format: &format_agent_id, - }, - rows, - }) - .to_string() - } -} - -impl TruncatableTextOutput for AgentsMetadataResponseView { - fn render_truncated(&self, max_lines: usize, colorize: bool) -> String { - let cursor_lines = if self.cursors.is_empty() { - 0 - } else { - 1 + self.cursors.len() - }; - let available_for_table = max_lines.saturating_sub(cursor_lines); - - let term_width = terminal_width(); - let table_str = Self::format_table_wide(&self.agents, term_width, colorize, true); - - let mut out = truncate_rendered(table_str, available_for_table); - - if !self.cursors.is_empty() { - out.push('\n'); - for (component_name, cursor) in &self.cursors { - out.push('\n'); - out.push_str(&format!( - "Cursor for more results for component {}: {}", - component_name.log_color_highlight(), - cursor.log_color_highlight() - )); - } - } - - out - } - - fn render_truncated_masked( - &self, - max_lines: usize, - colorize: bool, - config: MaskingConfig, - ) -> anyhow::Result { - Ok(self - .clone() - .masked(config)? - .render_truncated(max_lines, colorize)) - } -} - -impl TextOutput for TryUpdateAllWorkersResult { - fn log(&self) { - // NOP - } -} - -impl TextOutput for InvokeResultView { - fn log(&self) { - fn log_result_format(format: Option<&str>, multiple: bool) { - let result_label = if multiple { "results" } else { "result" }; - match format { - Some(format) => logln(format!( - "Invocation {result_label} in {}:", - format_message_highlight(format), - )), - None => logln(format!("Invocation {result_label}:")), - } - } - - if self.is_void_result { - log_result_format(None, false); - logln("void"); - return; - } - if self.result.is_none() && self.result_json.is_none() { - return; - } - - if let Some(result) = &self.result { - log_result_format(self.result_format.as_deref(), false); - logln(result); - } else if let Some(json) = &self.result_json { - logln(format_warn(indoc!( - " - Failed to convert invocation result to the requested format. - At the moment it does not support Handle (aka Resource) data type. - " - ))); - log_result_format(Some("JSON"), false); - logln(serde_json::to_string_pretty(json).unwrap()); - } - } -} +#[cfg(test)] +use crate::model::agent::{AgentMetadataView, AgentsMetadataResponseView, RawAgentId}; +#[cfg(test)] +use golem_common::model::AgentStatus; +#[cfg(test)] +use golem_common::model::component::ComponentName; +#[cfg(test)] +use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1358,11 +910,6 @@ fn render_typed_schema_value_line( format!("{pad} {rendered}") } -/// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). -fn format_agent_id_in(agent_id: &str, colorize: bool, width: usize) -> String { - crate::agent_id_display::format_agent_id_for_terminal(agent_id, colorize, Some(width)) -} - fn log_optional_error(pad: &str, error: &Option) { match error { None => { @@ -1703,104 +1250,3 @@ mod tests { } } } - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentFilesView { - pub nodes: Vec, -} - -impl StructuredOutput for AgentFilesView { - const KIND: &'static str = "agent.files"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct FileNodeView { - pub name: String, - pub last_modified: String, // Human-readable timestamp - pub kind: String, - pub permissions: String, - pub size: u64, -} - -impl TextOutput for AgentFilesView { - fn log(&self) { - if self.nodes.is_empty() { - logln("No files found."); - } else { - let mut table = new_table_full_condensed(vec![ - Column::new("Name"), - Column::new("Kind").fixed(), - Column::new("Permissions").fixed(), - Column::new("Size").fixed_right(), - Column::new("Last Modified").fixed_right(), - ]); - for node in &self.nodes { - table.add_row(vec![ - node.name.clone(), - node.kind.clone(), - node.permissions.clone(), - format_binary_size(&node.size), - node.last_modified.clone(), - ]); - } - log_table(table); - } - } -} - -// Helper function to convert Unix timestamp to human-readable format -pub fn format_timestamp(timestamp: u64) -> String { - if let Some(datetime) = DateTime::from_timestamp(timestamp as i64, 0) { - datetime.format("%Y-%m-%d %H:%M:%S").to_string() - } else { - format!("{timestamp}") // Fallback to raw timestamp if conversion fails - } -} - -pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { - let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( - agent_id_match.parsed_agent_id.as_ref(), - &agent_id_match.source_language, - &agent_id_match.agent_id.0, - ); - - format!( - "{}{}/{}", - match &agent_id_match.environment_reference() { - Some(environment_reference) => { - match environment_reference { - EnvironmentReference::Environment { environment_name } => { - format!("{}/", environment_name.0.blue().bold()) - } - EnvironmentReference::ApplicationEnvironment { - application_name, - environment_name, - } => { - format!( - "{}/{}/", - application_name.0.blue().bold(), - environment_name.0.blue().bold() - ) - } - EnvironmentReference::AccountApplicationEnvironment { - account_email, - application_name, - environment_name, - } => { - format!( - "{}/{}/{}/", - account_email.blue().bold(), - application_name.0.blue().bold(), - environment_name.0.blue().bold() - ) - } - } - } - None => "".to_string(), - }, - agent_id_match.component_name.0.blue().bold(), - rendered_agent_id.green().bold(), - ) -} From 5aa615c521953bd01b80817e9d3a307f3821066b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 30 Jul 2026 23:33:33 +0200 Subject: [PATCH 40/70] relocate domain renderers from text/fmt into their domain modules --- cli/golem-cli/src/app/context.rs | 2 +- cli/golem-cli/src/model/text/agent/mod.rs | 22 +++++ cli/golem-cli/src/model/text/component.rs | 81 ++++++++++++++++++- cli/golem-cli/src/model/text/fmt.rs | 99 ----------------------- 4 files changed, 102 insertions(+), 102 deletions(-) diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index d352123e42..32f19ea73f 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -32,9 +32,9 @@ use crate::model::app::{ use crate::model::app_raw; use crate::model::format::Format; use crate::model::language::GuestLanguage; +use crate::model::text::component::format_component_applied_layers; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::DecoratedIndent; -use crate::model::text::fmt::format_component_applied_layers; use crate::model::text::server::ToFormattedServerContext; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/model/text/agent/mod.rs b/cli/golem-cli/src/model/text/agent/mod.rs index 0f4962ae65..ca005c22ce 100644 --- a/cli/golem-cli/src/model/text/agent/mod.rs +++ b/cli/golem-cli/src/model/text/agent/mod.rs @@ -596,3 +596,25 @@ pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { rendered_agent_id.green().bold(), ) } + +fn format_status(status: &AgentStatus) -> String { + let status_name = status.to_string(); + match status { + AgentStatus::Running => status_name.green(), + AgentStatus::Idle => status_name.cyan(), + AgentStatus::Suspended => status_name.yellow(), + AgentStatus::Interrupted => status_name.red(), + AgentStatus::Retrying => status_name.yellow(), + AgentStatus::Failed => status_name.bright_red(), + AgentStatus::Exited => status_name.white(), + } + .to_string() +} + +fn format_retry_count(retry_count: &u32) -> String { + if *retry_count == 0 { + retry_count.to_string() + } else { + format_warn(&retry_count.to_string()) + } +} diff --git a/cli/golem-cli/src/model/text/component.rs b/cli/golem-cli/src/model/text/component.rs index d1dee54598..1b840bc311 100644 --- a/cli/golem-cli/src/model/text/component.rs +++ b/cli/golem-cli/src/model/text/component.rs @@ -12,14 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::app::ComponentLayerProperties; +use crate::log::LogColorize; +use crate::model::app::{ComponentLayerId, ComponentLayerProperties}; use crate::model::cli_output::StructuredOutput; use crate::model::component::ComponentView; use crate::model::masking::{Masked, MaskingConfig, is_sensitive_key, mask_secret}; use crate::model::text::fmt::*; +use colored::Colorize; use colored::control::SHOULD_COLORIZE; use golem_common::model::card::PolymorphicCard; -use golem_common::model::component::ComponentName; +use golem_common::model::component::{ComponentName, InitialAgentFile, InstalledPlugin}; +use golem_common::model::worker::TypedAgentConfigEntry; +use itertools::Itertools; use serde::Serializer; use serde::ser::Error; use serde::{Deserialize, Serialize}; @@ -413,3 +417,76 @@ fn mask_sensitive_keyed_values(value: &mut Value) { _ => {} } } + +fn format_files(files: &[InitialAgentFile]) -> String { + files + .iter() + .map(|file| { + format!( + "{} {} {}", + file.permissions.as_compact_str(), + file.path.as_path().as_str().log_color_highlight(), + file.content_hash.0.to_string().black() + ) + }) + .join("\n") +} + +fn format_plugins(plugins: &[InstalledPlugin]) -> String { + plugins + .iter() + .map(|plugin| { + let plugin_id = format!( + "{}: {}/{}", + plugin.priority, + plugin.plugin_name.log_color_highlight(), + plugin.plugin_version.log_color_highlight(), + ); + + if plugin.parameters.is_empty() { + plugin_id + } else { + format!( + "{}:\n{}", + plugin_id, + plugin + .parameters + .iter() + .map(|(k, v)| format!(" {}={}", k, v)) + .join("\n") + ) + } + }) + .join("\n") +} + +fn format_typed_config(config: &[TypedAgentConfigEntry]) -> String { + config + .iter() + .map(|entry| { + let key = entry.path.join("."); + let value = golem_common::schema::render::to_json_value( + entry.value.graph(), + entry.value.root_type(), + entry.value.value(), + ) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "".to_string()); + format!("{}={}", key.log_color_highlight(), value) + }) + .join("\n") +} + +pub fn format_component_applied_layers( + applied_layers: &[(ComponentLayerId, Option)], +) -> String { + applied_layers + .iter() + .map(|(id, selection)| match selection { + Some(selection) => { + format!("{}[{}]", id.name(), selection.as_str()) + } + None => id.name().to_string(), + }) + .join(", ") +} diff --git a/cli/golem-cli/src/model/text/fmt.rs b/cli/golem-cli/src/model/text/fmt.rs index 17ab74fa86..b43947141d 100644 --- a/cli/golem-cli/src/model/text/fmt.rs +++ b/cli/golem-cli/src/model/text/fmt.rs @@ -19,7 +19,6 @@ pub use crate::log::terminal_width; use crate::log::{ INDENT, LogColorize, LogIndent, WRAP_PADDING, current_indent_width, log_warn_action, }; -use crate::model::app::ComponentLayerId; use crate::model::format::Format; use crate::model::masking::{Masked, MaskingConfig}; use anyhow::anyhow; @@ -29,9 +28,6 @@ pub use comfy_table::Table as ComfyTable; use comfy_table::{ Cell, CellAlignment, Color as ComfyColor, ColumnConstraint, ContentArrangement, Width, }; -use golem_common::model::AgentStatus; -use golem_common::model::component::{InitialAgentFile, InstalledPlugin}; -use golem_common::model::worker::TypedAgentConfigEntry; use itertools::Itertools; use regex::Regex; use serde::Serialize; @@ -297,28 +293,6 @@ pub fn format_binary_size(size: &u64) -> String { humansize::format_size(*size, humansize::BINARY) } -pub fn format_status(status: &AgentStatus) -> String { - let status_name = status.to_string(); - match status { - AgentStatus::Running => status_name.green(), - AgentStatus::Idle => status_name.cyan(), - AgentStatus::Suspended => status_name.yellow(), - AgentStatus::Interrupted => status_name.red(), - AgentStatus::Retrying => status_name.yellow(), - AgentStatus::Failed => status_name.bright_red(), - AgentStatus::Exited => status_name.white(), - } - .to_string() -} - -pub fn format_retry_count(retry_count: &u32) -> String { - if *retry_count == 0 { - retry_count.to_string() - } else { - format_warn(&retry_count.to_string()) - } -} - static BUILTIN_TYPES: phf::Set<&'static str> = phf::phf_set! { // WIT primitives "bool", @@ -399,71 +373,12 @@ pub fn format_exports(exports: &[String]) -> String { exports.iter().map(|e| format_export(e.as_str())).join("\n") } -pub fn format_files(files: &[InitialAgentFile]) -> String { - files - .iter() - .map(|file| { - format!( - "{} {} {}", - file.permissions.as_compact_str(), - file.path.as_path().as_str().log_color_highlight(), - file.content_hash.0.to_string().black() - ) - }) - .join("\n") -} - -pub fn format_plugins(plugins: &[InstalledPlugin]) -> String { - plugins - .iter() - .map(|plugin| { - let plugin_id = format!( - "{}: {}/{}", - plugin.priority, - plugin.plugin_name.log_color_highlight(), - plugin.plugin_version.log_color_highlight(), - ); - - if plugin.parameters.is_empty() { - plugin_id - } else { - format!( - "{}:\n{}", - plugin_id, - plugin - .parameters - .iter() - .map(|(k, v)| format!(" {}={}", k, v)) - .join("\n") - ) - } - }) - .join("\n") -} - pub fn format_env(env: &BTreeMap) -> String { env.iter() .map(|(k, v)| format!("{}={}", k, v.log_color_highlight())) .join("\n") } -pub fn format_typed_config(config: &[TypedAgentConfigEntry]) -> String { - config - .iter() - .map(|entry| { - let key = entry.path.join("."); - let value = golem_common::schema::render::to_json_value( - entry.value.graph(), - entry.value.root_type(), - entry.value.value(), - ) - .map(|v| v.to_string()) - .unwrap_or_else(|_| "".to_string()); - format!("{}={}", key.log_color_highlight(), value) - }) - .join("\n") -} - /// Describes a single table column: its header title, whether it is pinned to content /// width, and whether its data rows should be right-aligned. pub struct Column { @@ -1003,20 +918,6 @@ pub fn to_colored_yaml(value: &T) -> anyhow::Result { Ok(output) } -pub fn format_component_applied_layers( - applied_layers: &[(ComponentLayerId, Option)], -) -> String { - applied_layers - .iter() - .map(|(id, selection)| match selection { - Some(selection) => { - format!("{}[{}]", id.name(), selection.as_str()) - } - None => id.name().to_string(), - }) - .join(", ") -} - #[cfg(test)] mod tests { use super::*; From bd6cc470e727b831b509f68de170593025557099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 19:50:19 +0200 Subject: [PATCH 41/70] expose api commands in the repl --- cli/golem-cli/src/command.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index f7f11d796b..63ef9f0f61 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -84,7 +84,6 @@ impl GolemCliCommand { &CliMetadataFilter { command_path_prefix_exclude: vec![ vec!["account"], - vec!["api"], // TODO: recheck after code-first routes is implemented vec!["api-token"], vec!["clean"], vec!["completion"], From ac8e46b15a831bafb428a20ac187b526a92e2bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 19:56:11 +0200 Subject: [PATCH 42/70] move plugin views into model/plugin --- .../src/command_handler/component/mod.rs | 2 +- .../src/command_handler/component/staging.rs | 2 +- .../src/command_handler/environment.rs | 2 +- cli/golem-cli/src/command_handler/plugin.rs | 4 +- cli/golem-cli/src/context.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 21 +- cli/golem-cli/src/model/plugin.rs | 178 ++++++++++++++++ cli/golem-cli/src/model/text/mod.rs | 1 - cli/golem-cli/src/model/text/plugin.rs | 194 ------------------ 9 files changed, 194 insertions(+), 212 deletions(-) delete mode 100644 cli/golem-cli/src/model/text/plugin.rs diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index e677cbaf6b..982e2a1ead 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -42,6 +42,7 @@ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::language::GuestLanguage; +use crate::model::plugin::PluginNameAndVersion; use crate::model::text::agent::action_result::{ AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; @@ -50,7 +51,6 @@ use crate::model::text::component::{ }; use crate::model::text::fmt::log_text_view; use crate::model::text::help::ComponentNameHelp; -use crate::model::text::plugin::PluginNameAndVersion; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; use futures_util::future::OptionFuture; diff --git a/cli/golem-cli/src/command_handler/component/staging.rs b/cli/golem-cli/src/command_handler/component/staging.rs index f7a01afefa..583699428a 100644 --- a/cli/golem-cli/src/command_handler/component/staging.rs +++ b/cli/golem-cli/src/command_handler/component/staging.rs @@ -25,7 +25,7 @@ use crate::model::app_raw; use crate::model::component::initial_permission_recipient_context; use crate::model::component::{AgentTypeManifestProvisionConfig, ComponentDeployProperties}; use crate::model::environment::ResolvedEnvironmentIdentity; -use crate::model::text::plugin::PluginNameAndVersion; +use crate::model::plugin::PluginNameAndVersion; use anyhow::{Context as AnyhowContext, anyhow}; use golem_client::model::EnvironmentPluginGrantWithDetails; use golem_common::model::agent::AgentTypeName; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index d0b6166515..ee5bf79b66 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -25,13 +25,13 @@ use crate::log::{ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; +use crate::model::plugin::PluginNameAndVersion; use crate::model::text::diff::log_unified_diff; use crate::model::text::environment::{ EnvironmentListView, EnvironmentSyncDeploymentOptionsResult, }; use crate::model::text::fmt::log_text_view; use crate::model::text::help::EnvironmentNameHelp; -use crate::model::text::plugin::PluginNameAndVersion; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; use golem_client::model::{EnvironmentCreation, EnvironmentPluginGrantWithDetails}; diff --git a/cli/golem-cli/src/command_handler/plugin.rs b/cli/golem-cli/src/command_handler/plugin.rs index 77e5eb4cb2..10f032082c 100644 --- a/cli/golem-cli/src/command_handler/plugin.rs +++ b/cli/golem-cli/src/command_handler/plugin.rs @@ -19,11 +19,11 @@ use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; use crate::model::environment::EnvironmentResolveMode; use crate::model::input::PathBufOrStdin; -use crate::model::plugin::{PluginManifest, PluginTypeSpecificManifest}; -use crate::model::text::plugin::{ +use crate::model::plugin::{ PluginListEntry, PluginListView, PluginRegistrationGetView, PluginRegistrationRegisterView, PluginSource, PluginUnregisterResult, }; +use crate::model::plugin::{PluginManifest, PluginTypeSpecificManifest}; use anyhow::{Context as AnyhowContext, anyhow}; use golem_client::api::PluginClient; use golem_client::model::PluginRegistrationCreation; diff --git a/cli/golem-cli/src/context.rs b/cli/golem-cli/src/context.rs index 0e83221c96..9d08a389d7 100644 --- a/cli/golem-cli/src/context.rs +++ b/cli/golem-cli/src/context.rs @@ -36,8 +36,8 @@ use crate::model::app_raw::{ use crate::model::environment::{EnvironmentReference, SelectedManifestEnvironment}; use crate::model::format::Format; use crate::model::masking::MaskingConfig; +use crate::model::plugin::PluginNameAndVersion; use crate::model::repl::ReplLanguage; -use crate::model::text::plugin::PluginNameAndVersion; use crate::model::text::server::ToFormattedServerContext; use anyhow::{anyhow, bail}; use colored::control::SHOULD_COLORIZE; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index f92cb24ed8..af68ab4be3 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -4796,7 +4796,7 @@ fn arb_plugin_unregister_result() -> OutputDocumentStrategy { arb_small_string(), ) .prop_map(|(unregistered, plugin_id, name, version)| { - crate::model::text::plugin::PluginUnregisterResult { + crate::model::plugin::PluginUnregisterResult { unregistered, plugin_id, name, @@ -4808,35 +4808,34 @@ fn arb_plugin_unregister_result() -> OutputDocumentStrategy { fn arb_plugin_get_result() -> OutputDocumentStrategy { serialized_output( - arb_plugin_registration().prop_map(crate::model::text::plugin::PluginRegistrationGetView), + arb_plugin_registration().prop_map(crate::model::plugin::PluginRegistrationGetView), ) } fn arb_plugin_register_result() -> OutputDocumentStrategy { serialized_output( - arb_plugin_registration() - .prop_map(crate::model::text::plugin::PluginRegistrationRegisterView), + arb_plugin_registration().prop_map(crate::model::plugin::PluginRegistrationRegisterView), ) } fn arb_plugin_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_plugin_list_entry(), 0..5) - .prop_map(|plugins| crate::model::text::plugin::PluginListView { plugins }), + .prop_map(|plugins| crate::model::plugin::PluginListView { plugins }), ) } -fn arb_plugin_list_entry() -> BoxedStrategy { +fn arb_plugin_list_entry() -> BoxedStrategy { (arb_plugin_registration(), arb_plugin_source()) - .prop_map(|(plugin, source)| crate::model::text::plugin::PluginListEntry { plugin, source }) + .prop_map(|(plugin, source)| crate::model::plugin::PluginListEntry { plugin, source }) .boxed() } -fn arb_plugin_source() -> BoxedStrategy { +fn arb_plugin_source() -> BoxedStrategy { prop_oneof![ - Just(crate::model::text::plugin::PluginSource::Own), - Just(crate::model::text::plugin::PluginSource::Builtin), - Just(crate::model::text::plugin::PluginSource::Shared), + Just(crate::model::plugin::PluginSource::Own), + Just(crate::model::plugin::PluginSource::Builtin), + Just(crate::model::plugin::PluginSource::Shared), ] .boxed() } diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index f56edf177c..77626c093b 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -12,7 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::model::cli_output::StructuredOutput; +use crate::model::masking::Masked; +use crate::model::text::fmt::{ + Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, + format_message_highlight, log_table, new_table_full_condensed, +}; use golem_common::model::component::ComponentRevision; +use golem_common::model::plugin_registration::PluginRegistrationDto; use serde::{Deserialize, Serialize}; use std::fmt::Debug; use std::path::PathBuf; @@ -41,3 +48,174 @@ pub struct PluginManifest { pub homepage: String, pub specs: PluginTypeSpecificManifest, } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PluginNameAndVersion { + pub name: String, + pub version: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum PluginSource { + Own, + Builtin, + Shared, +} + +impl std::fmt::Display for PluginSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PluginSource::Own => write!(f, "own"), + PluginSource::Builtin => write!(f, "builtin"), + PluginSource::Shared => write!(f, "shared"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginListEntry { + pub plugin: PluginRegistrationDto, + pub source: PluginSource, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginListView { + pub plugins: Vec, +} + +impl StructuredOutput for PluginListView { + const KIND: &'static str = "plugin.list"; +} + +impl TextOutput for PluginListView { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Plugin name").fixed(), + Column::new("Plugin version").fixed(), + Column::new("Source").fixed(), + Column::new("Type").fixed(), + Column::new("Description"), + Column::new("Homepage"), + ]); + for entry in &self.plugins { + table.add_row(vec![ + entry.plugin.name.clone(), + entry.plugin.version.clone(), + entry.source.to_string(), + entry.plugin.typ_as_str().to_string(), + entry.plugin.description.clone(), + entry.plugin.homepage.clone(), + ]); + } + log_table(table); + } +} + +impl TextOutput for Vec { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Plugin name").fixed(), + Column::new("Plugin version").fixed(), + Column::new("Type").fixed(), + Column::new("Description"), + Column::new("Homepage"), + ]); + for plugin in self { + table.add_row(vec![ + plugin.name.clone(), + plugin.version.clone(), + plugin.typ_as_str().to_string(), + plugin.description.clone(), + plugin.homepage.clone(), + ]); + } + log_table(table); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginRegistrationRegisterView(pub PluginRegistrationDto); + +impl Masked for PluginRegistrationRegisterView {} + +impl MessageWithFields for PluginRegistrationRegisterView { + fn message(&self) -> String { + format!( + "Registered new plugin {} version {}", + format_message_highlight(&self.0.name), + format_message_highlight(&self.0.version), + ) + } + + fn fields(&self) -> Vec<(String, String)> { + plugin_registration_fields(&self.0) + } +} + +impl StructuredOutput for PluginRegistrationRegisterView { + const KIND: &'static str = "plugin.register"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PluginRegistrationGetView(pub PluginRegistrationDto); + +impl Masked for PluginRegistrationGetView {} + +impl MessageWithFields for PluginRegistrationGetView { + fn message(&self) -> String { + format!( + "Got metadata for plugin {} version {}", + format_message_highlight(&self.0.name), + format_message_highlight(&self.0.version), + ) + } + + fn fields(&self) -> Vec<(String, String)> { + plugin_registration_fields(&self.0) + } +} + +impl StructuredOutput for PluginRegistrationGetView { + const KIND: &'static str = "plugin.get"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PluginUnregisterResult { + pub unregistered: bool, + pub plugin_id: Uuid, + pub name: String, + pub version: String, +} + +impl NoTextOutput for PluginUnregisterResult {} +impl TextOutput for PluginUnregisterResult {} + +impl StructuredOutput for PluginUnregisterResult { + const KIND: &'static str = "plugin.unregister"; +} + +fn plugin_registration_fields(plugin: &PluginRegistrationDto) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields + .fmt_field("Name", &plugin.name, format_main_id) + .fmt_field("Version", &plugin.version, format_main_id) + .field("Description", &plugin.description) + .field("Homepage", &plugin.homepage) + .field("Type", &plugin.typ_as_str()) + .fmt_field_option( + "Component ID", + &plugin.oplog_processor_component_id(), + format_id, + ) + .fmt_field_option( + "Component Version", + &plugin.oplog_processor_component_revision(), + format_id, + ); + + fields.build() +} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 4ba089eaf0..16b55e26d2 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -26,7 +26,6 @@ pub mod help; pub mod http_api_deployment; pub mod http_api_domain; pub mod http_api_security; -pub mod plugin; pub mod profile; pub mod resource_definition; pub mod retry_policy; diff --git a/cli/golem-cli/src/model/text/plugin.rs b/cli/golem-cli/src/model/text/plugin.rs deleted file mode 100644 index cf729fc7a6..0000000000 --- a/cli/golem-cli/src/model/text/plugin.rs +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::model::cli_output::StructuredOutput; -use crate::model::masking::Masked; -use crate::model::text::fmt::{ - Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, - format_message_highlight, log_table, new_table_full_condensed, -}; -use golem_common::model::plugin_registration::PluginRegistrationDto; -use serde_derive::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct PluginNameAndVersion { - pub name: String, - pub version: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum PluginSource { - Own, - Builtin, - Shared, -} - -impl std::fmt::Display for PluginSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PluginSource::Own => write!(f, "own"), - PluginSource::Builtin => write!(f, "builtin"), - PluginSource::Shared => write!(f, "shared"), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginListEntry { - pub plugin: PluginRegistrationDto, - pub source: PluginSource, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginListView { - pub plugins: Vec, -} - -impl StructuredOutput for PluginListView { - const KIND: &'static str = "plugin.list"; -} - -impl TextOutput for PluginListView { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Plugin name").fixed(), - Column::new("Plugin version").fixed(), - Column::new("Source").fixed(), - Column::new("Type").fixed(), - Column::new("Description"), - Column::new("Homepage"), - ]); - for entry in &self.plugins { - table.add_row(vec![ - entry.plugin.name.clone(), - entry.plugin.version.clone(), - entry.source.to_string(), - entry.plugin.typ_as_str().to_string(), - entry.plugin.description.clone(), - entry.plugin.homepage.clone(), - ]); - } - log_table(table); - } -} - -impl TextOutput for Vec { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Plugin name").fixed(), - Column::new("Plugin version").fixed(), - Column::new("Type").fixed(), - Column::new("Description"), - Column::new("Homepage"), - ]); - for plugin in self { - table.add_row(vec![ - plugin.name.clone(), - plugin.version.clone(), - plugin.typ_as_str().to_string(), - plugin.description.clone(), - plugin.homepage.clone(), - ]); - } - log_table(table); - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginRegistrationRegisterView(pub PluginRegistrationDto); - -impl Masked for PluginRegistrationRegisterView {} - -impl MessageWithFields for PluginRegistrationRegisterView { - fn message(&self) -> String { - format!( - "Registered new plugin {} version {}", - format_message_highlight(&self.0.name), - format_message_highlight(&self.0.version), - ) - } - - fn fields(&self) -> Vec<(String, String)> { - plugin_registration_fields(&self.0) - } -} - -impl StructuredOutput for PluginRegistrationRegisterView { - const KIND: &'static str = "plugin.register"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PluginRegistrationGetView(pub PluginRegistrationDto); - -impl Masked for PluginRegistrationGetView {} - -impl MessageWithFields for PluginRegistrationGetView { - fn message(&self) -> String { - format!( - "Got metadata for plugin {} version {}", - format_message_highlight(&self.0.name), - format_message_highlight(&self.0.version), - ) - } - - fn fields(&self) -> Vec<(String, String)> { - plugin_registration_fields(&self.0) - } -} - -impl StructuredOutput for PluginRegistrationGetView { - const KIND: &'static str = "plugin.get"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct PluginUnregisterResult { - pub unregistered: bool, - pub plugin_id: Uuid, - pub name: String, - pub version: String, -} - -impl NoTextOutput for PluginUnregisterResult {} -impl TextOutput for PluginUnregisterResult {} - -impl StructuredOutput for PluginUnregisterResult { - const KIND: &'static str = "plugin.unregister"; -} - -fn plugin_registration_fields(plugin: &PluginRegistrationDto) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("Name", &plugin.name, format_main_id) - .fmt_field("Version", &plugin.version, format_main_id) - .field("Description", &plugin.description) - .field("Homepage", &plugin.homepage) - .field("Type", &plugin.typ_as_str()) - .fmt_field_option( - "Component ID", - &plugin.oplog_processor_component_id(), - format_id, - ) - .fmt_field_option( - "Component Version", - &plugin.oplog_processor_component_revision(), - format_id, - ); - - fields.build() -} From e7f884f36a76fdbd344c1a9a3cb53c3198a9b3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:05:38 +0200 Subject: [PATCH 43/70] move component and environment views into their model modules --- cli/golem-cli/src/app/context.rs | 2 +- .../src/command_handler/component/mod.rs | 4 +- .../src/command_handler/environment.rs | 4 +- cli/golem-cli/src/model/cli_output/tests.rs | 42 +- cli/golem-cli/src/model/component.rs | 477 ++++++++++++++++- cli/golem-cli/src/model/environment.rs | 98 +++- cli/golem-cli/src/model/text/component.rs | 492 ------------------ cli/golem-cli/src/model/text/environment.rs | 118 ----- cli/golem-cli/src/model/text/mod.rs | 2 - 9 files changed, 593 insertions(+), 646 deletions(-) delete mode 100644 cli/golem-cli/src/model/text/component.rs delete mode 100644 cli/golem-cli/src/model/text/environment.rs diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 32f19ea73f..f97693c5ae 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -30,9 +30,9 @@ use crate::model::app::{ includes_from_yaml_file, }; use crate::model::app_raw; +use crate::model::component::format_component_applied_layers; use crate::model::format::Format; use crate::model::language::GuestLanguage; -use crate::model::text::component::format_component_applied_layers; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::DecoratedIndent; use crate::model::text::server::ToFormattedServerContext; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 982e2a1ead..2a19471e24 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -33,6 +33,7 @@ use crate::model::component::{ ComponentRevisionSelection, ComponentView, SelectedComponents, initial_permission_from_manifest_card, initial_permission_recipient_context, }; +use crate::model::component::{ComponentGetView, ComponentListView, ComponentManifestTraceView}; use crate::model::config::{collect_unused_leaf_paths, value_at_path}; use crate::model::deploy::{ DeployConfig, TryUpdateAllWorkersResult, UpdateStagedComponentError, @@ -46,9 +47,6 @@ use crate::model::plugin::PluginNameAndVersion; use crate::model::text::agent::action_result::{ AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; -use crate::model::text::component::{ - ComponentGetView, ComponentListView, ComponentManifestTraceView, -}; use crate::model::text::fmt::log_text_view; use crate::model::text::help::ComponentNameHelp; use crate::validation::ValidationBuilder; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index ee5bf79b66..a897376404 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -22,14 +22,12 @@ use crate::error::service::MapServiceError; use crate::log::{ LogColorize, LogIndent, log_action, log_error, log_skipping_up_to_date, log_warn_action, logln, }; +use crate::model::environment::{EnvironmentListView, EnvironmentSyncDeploymentOptionsResult}; use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::plugin::PluginNameAndVersion; use crate::model::text::diff::log_unified_diff; -use crate::model::text::environment::{ - EnvironmentListView, EnvironmentSyncDeploymentOptionsResult, -}; use crate::model::text::fmt::log_text_view; use crate::model::text::help::EnvironmentNameHelp; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index af68ab4be3..e25ebc9f97 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -680,10 +680,9 @@ fn agent_get_structured_output_masks_secret_config_paths() { fn component_get_and_list_structured_outputs_mask_secret_payloads() { let component = sample_component_view(); - let get = hidden_structured_output(crate::model::text::component::ComponentGetView( - component.clone(), - )); - let list = hidden_structured_output(crate::model::text::component::ComponentListView { + let get = + hidden_structured_output(crate::model::component::ComponentGetView(component.clone())); + let list = hidden_structured_output(crate::model::component::ComponentListView { components: vec![component], }); @@ -702,11 +701,10 @@ fn component_get_and_list_structured_outputs_mask_secret_payloads() { #[test] fn component_manifest_trace_structured_output_masks_secret_payloads() { - let value = - hidden_structured_output(crate::model::text::component::ComponentManifestTraceView { - component_name: golem_common::model::component::ComponentName("component".to_string()), - properties: sample_component_layer_properties(), - }); + let value = hidden_structured_output(crate::model::component::ComponentManifestTraceView { + component_name: golem_common::model::component::ComponentName("component".to_string()), + properties: sample_component_layer_properties(), + }); assert_no_plaintext( &value, @@ -1105,11 +1103,9 @@ fn cli_output_schema_validates_schema_native_component_and_agent_outputs() { .current(); let outputs = vec![ - to_structured_output_value(crate::model::text::component::ComponentGetView( - component.clone(), - )) - .expect("component.get should serialize"), - to_structured_output_value(crate::model::text::component::ComponentListView { + to_structured_output_value(crate::model::component::ComponentGetView(component.clone())) + .expect("component.get should serialize"), + to_structured_output_value(crate::model::component::ComponentListView { components: vec![component], }) .expect("component.list should serialize"), @@ -3772,26 +3768,22 @@ fn arb_guest_language() -> BoxedStrategy } fn arb_component_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_component_view().prop_map(crate::model::text::component::ComponentGetView), - ) + serialized_output(arb_component_view().prop_map(crate::model::component::ComponentGetView)) } fn arb_component_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_component_view(), 0..5) - .prop_map(|components| crate::model::text::component::ComponentListView { components }), + .prop_map(|components| crate::model::component::ComponentListView { components }), ) } fn arb_component_manifest_trace_result() -> OutputDocumentStrategy { serialized_output( (arb_small_string(), arb_component_layer_properties()).prop_map( - |(component_name, properties)| { - crate::model::text::component::ComponentManifestTraceView { - component_name: golem_common::model::component::ComponentName(component_name), - properties, - } + |(component_name, properties)| crate::model::component::ComponentManifestTraceView { + component_name: golem_common::model::component::ComponentName(component_name), + properties, }, ), ) @@ -4625,14 +4617,14 @@ fn arb_deployment_list_result() -> OutputDocumentStrategy { fn arb_environment_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_environment_with_details(), 0..5).prop_map(|environments| { - crate::model::text::environment::EnvironmentListView { environments } + crate::model::environment::EnvironmentListView { environments } }), ) } fn arb_environment_sync_deployment_options_result() -> OutputDocumentStrategy { serialized_output(any::().prop_map(|updated| { - crate::model::text::environment::EnvironmentSyncDeploymentOptionsResult { updated } + crate::model::environment::EnvironmentSyncDeploymentOptionsResult { updated } })) } diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index 38a565aa6c..7c917e17f8 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -14,15 +14,23 @@ use crate::agent_id_display::SourceLanguage; use crate::agent_id_display::render_type_for_language; +use crate::log::LogColorize; use crate::model::agent::RawAgentId; +use crate::model::app::{ComponentLayerId, ComponentLayerProperties}; use crate::model::app_raw; +use crate::model::cli_output::StructuredOutput; use crate::model::environment::ResolvedEnvironmentIdentity; use crate::model::masking::{ - Masked, MaskingConfig, mask_sensitive_map, mask_typed_agent_config_entries, + Masked, MaskingConfig, is_sensitive_key, mask_secret, mask_sensitive_map, + mask_typed_agent_config_entries, }; +use crate::model::text::fmt::*; use chrono::{DateTime, Utc}; +use colored::Colorize; +use colored::control::SHOULD_COLORIZE; use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; use golem_common::model::agent::{AgentConfigSource, AgentTypeName}; +use golem_common::model::card::PolymorphicCard; use golem_common::model::card::PolymorphicManifestPermissionPattern; use golem_common::model::card::recipient::{RecipientMonomorphizationContext, RecipientPattern}; use golem_common::model::component::{ @@ -33,12 +41,17 @@ use golem_common::model::component::{ ArchiveFilePath, PluginInstallation, }; use golem_common::model::component::{AgentFilePermissions, ComponentName}; +use golem_common::model::component::{InitialAgentFile, InstalledPlugin}; use golem_common::model::environment::EnvironmentId; +use golem_common::model::worker::TypedAgentConfigEntry; use golem_common::schema::agent::{AgentTypeSchema, FieldSource, InputSchema, OutputSchema}; use golem_common::schema::graph::SchemaGraph; use heck::{ToLowerCamelCase, ToSnakeCase}; use itertools::Itertools; +use serde::Serializer; +use serde::ser::Error; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use std::str::FromStr; @@ -597,3 +610,465 @@ mod tests { } } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ComponentListView { + pub components: Vec, +} + +impl Masked for ComponentListView { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + self.components = self + .components + .into_iter() + .map(|component| component.masked(config)) + .collect::>>()?; + Ok(self) + } +} + +impl StructuredOutput for ComponentListView { + const KIND: &'static str = "component.list"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl TextOutput for ComponentListView { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Name"), + Column::new("Revision").fixed_right(), + Column::new("Version").fixed_right(), + Column::new("Size").fixed_right(), + Column::new("Exports").fixed_right(), + ]); + for comp in &self.components { + table.add_row(vec![ + comp.component_name.to_string(), + comp.component_revision.to_string(), + comp.component_version.clone().unwrap_or_default(), + format_binary_size(&comp.component_size), + comp.exports.len().to_string(), + ]); + } + log_table(table); + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + self.masked(config)?.log(); + Ok(()) + } +} + +fn component_view_fields(view: &ComponentView) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields + .fmt_field("Component name", &view.component_name, format_main_id) + .fmt_field("Component ID", &view.component_id, format_id) + .fmt_field("Component revision", &view.component_revision, format_id) + .fmt_field_option("Component version", &view.component_version, format_id) + .fmt_field("Environment ID", &view.environment_id, format_id) + .fmt_field("Component size", &view.component_size, format_binary_size) + .fmt_field("Created at", &view.created_at, |d| d.to_string()) + .fmt_field("Exports", &view.exports, |e| format_exports(e.as_slice())); + + for (agent_type_name, provision_config) in &view.agent_type_provision_configs { + let prefix = format!("[{}] ", agent_type_name.0); + fields + .fmt_field_optional( + &format!("{}Environment", prefix), + &provision_config.env, + !provision_config.env.is_empty(), + format_env, + ) + .fmt_field_optional( + &format!("{}Agent config", prefix), + provision_config.config.as_slice(), + !provision_config.config.is_empty(), + format_typed_config, + ) + .fmt_field_optional( + &format!("{}Initial file system", prefix), + provision_config.files.as_slice(), + !provision_config.files.is_empty(), + format_files, + ) + .fmt_field_optional( + &format!("{}Plugins", prefix), + provision_config.plugins.as_slice(), + !provision_config.plugins.is_empty(), + format_plugins, + ) + .fmt_field_optional( + &format!("{}Initial permissions", prefix), + &provision_config.initial_permissions, + !initial_permission_is_empty(&provision_config.initial_permissions), + format_initial_permission, + ); + } + + fields.build() +} + +fn initial_permission_is_empty(card: &PolymorphicCard) -> bool { + card.lower_positive.is_empty() + && card.lower_negative.is_empty() + && card.upper_positive.is_empty() + && card.upper_negative.is_empty() +} + +fn format_initial_permission(card: &PolymorphicCard) -> String { + let mut sections = Vec::new(); + push_initial_permission_section(&mut sections, "lower positive", &card.lower_positive); + push_initial_permission_section(&mut sections, "lower negative", &card.lower_negative); + push_initial_permission_section(&mut sections, "upper positive", &card.upper_positive); + push_initial_permission_section(&mut sections, "upper negative", &card.upper_negative); + sections.join("\n") +} + +fn push_initial_permission_section( + sections: &mut Vec, + name: &str, + permissions: &[golem_common::model::card::PolymorphicPermissionPattern], +) { + if permissions.is_empty() { + return; + } + + let grants = permissions + .iter() + .map(|permission| { + permission + .render() + .unwrap_or_else(|error| format!("")) + }) + .map(|grant| format!(" - {grant}")) + .collect::>() + .join("\n"); + sections.push(format!("{name}:\n{grants}")); +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComponentCreateView(pub ComponentView); + +impl Masked for ComponentCreateView { + fn masked(self, config: MaskingConfig) -> anyhow::Result { + Ok(Self(self.0.masked(config)?)) + } +} + +impl MessageWithFields for ComponentCreateView { + fn message(&self) -> String { + format!( + "Created new component {}", + format_message_highlight(&self.0.component_name) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + component_view_fields(&self.0) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComponentUpdateView(pub ComponentView); + +impl Masked for ComponentUpdateView { + fn masked(self, config: MaskingConfig) -> anyhow::Result { + Ok(Self(self.0.masked(config)?)) + } +} + +impl MessageWithFields for ComponentUpdateView { + fn message(&self) -> String { + format!( + "Updated component {} to revision {}", + format_message_highlight(&self.0.component_name), + format_message_highlight(&self.0.component_revision), + ) + } + + fn fields(&self) -> Vec<(String, String)> { + component_view_fields(&self.0) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComponentGetView(pub ComponentView); + +impl Masked for ComponentGetView { + fn masked(self, config: MaskingConfig) -> anyhow::Result { + Ok(Self(self.0.masked(config)?)) + } +} + +impl MessageWithFields for ComponentGetView { + fn message(&self) -> String { + format!( + "Got metadata for component {}", + format_message_highlight(&self.0.component_name) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + component_view_fields(&self.0) + } +} + +impl StructuredOutput for ComponentGetView { + const KIND: &'static str = "component.get"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ComponentManifestTraceView { + pub component_name: ComponentName, + pub properties: ComponentLayerProperties, +} + +impl StructuredOutput for ComponentManifestTraceView { + const KIND: &'static str = "component.manifest-trace"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.to_masked_value(config) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +impl TextOutput for ComponentManifestTraceView { + fn log(&self) { + log_manifest_trace_properties(&self.properties); + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + if config.show_secrets { + self.log(); + } else { + let mut properties = serde_json::to_value(&self.properties)?; + mask_component_layer_properties(&mut properties); + log_manifest_trace_value(&properties); + } + Ok(()) + } +} + +impl ComponentManifestTraceView { + fn to_masked_value(&self, config: MaskingConfig) -> anyhow::Result { + let mut value = serde_json::to_value(self)?; + if !config.show_secrets + && let Some(properties) = value + .as_object_mut() + .and_then(|object| object.get_mut("properties")) + { + mask_component_layer_properties(properties); + } + Ok(value) + } +} + +fn log_manifest_trace_properties(properties: &ComponentLayerProperties) { + let rendered = if SHOULD_COLORIZE.should_colorize() { + to_colored_json(properties) + } else { + serde_json::to_string_pretty(properties).map_err(Into::into) + }; + + log_manifest_trace_rendered(rendered); +} + +fn log_manifest_trace_value(properties: &Value) { + let rendered = if SHOULD_COLORIZE.should_colorize() { + to_colored_json(properties) + } else { + serde_json::to_string_pretty(properties).map_err(Into::into) + }; + + log_manifest_trace_rendered(rendered); +} + +fn log_manifest_trace_rendered(rendered: anyhow::Result) { + match rendered { + Ok(rendered) => { + for line in rendered.lines() { + logln(line); + } + } + Err(error) => logln(format!("")), + } +} + +fn mask_component_layer_properties(properties: &mut Value) { + let Some(properties) = properties.as_object_mut() else { + return; + }; + + if let Some(config) = properties.get_mut("config") { + mask_config_property_payloads(config); + } + if let Some(env) = properties.get_mut("env") { + mask_sensitive_keyed_values(env); + } + if let Some(plugins) = properties.get_mut("plugins") { + mask_sensitive_keyed_values(plugins); + } +} + +fn mask_config_property_payloads(value: &mut Value) { + match value { + Value::Object(object) => { + for (key, value) in object { + match key.as_str() { + "value" | "newValue" => mask_json_leaf_values(value), + "insertedEntries" | "updatedEntries" => mask_json_object_values(value), + _ => mask_config_property_payloads(value), + } + } + } + Value::Array(values) => { + for value in values { + mask_config_property_payloads(value); + } + } + _ => {} + } +} + +fn mask_json_object_values(value: &mut Value) { + if let Some(object) = value.as_object_mut() { + for value in object.values_mut() { + mask_json_leaf_values(value); + } + } +} + +fn mask_json_leaf_values(value: &mut Value) { + match value { + Value::Null => {} + Value::Array(values) => { + for value in values { + mask_json_leaf_values(value); + } + } + Value::Object(object) => { + for value in object.values_mut() { + mask_json_leaf_values(value); + } + } + _ => *value = Value::String(mask_secret()), + } +} + +fn mask_sensitive_keyed_values(value: &mut Value) { + match value { + Value::Object(object) => { + for (key, value) in object { + if is_sensitive_key(key) { + mask_json_leaf_values(value); + } else { + mask_sensitive_keyed_values(value); + } + } + } + Value::Array(values) => { + for value in values { + mask_sensitive_keyed_values(value); + } + } + _ => {} + } +} + +fn format_files(files: &[InitialAgentFile]) -> String { + files + .iter() + .map(|file| { + format!( + "{} {} {}", + file.permissions.as_compact_str(), + file.path.as_path().as_str().log_color_highlight(), + file.content_hash.0.to_string().black() + ) + }) + .join("\n") +} + +fn format_plugins(plugins: &[InstalledPlugin]) -> String { + plugins + .iter() + .map(|plugin| { + let plugin_id = format!( + "{}: {}/{}", + plugin.priority, + plugin.plugin_name.log_color_highlight(), + plugin.plugin_version.log_color_highlight(), + ); + + if plugin.parameters.is_empty() { + plugin_id + } else { + format!( + "{}:\n{}", + plugin_id, + plugin + .parameters + .iter() + .map(|(k, v)| format!(" {}={}", k, v)) + .join("\n") + ) + } + }) + .join("\n") +} + +fn format_typed_config(config: &[TypedAgentConfigEntry]) -> String { + config + .iter() + .map(|entry| { + let key = entry.path.join("."); + let value = golem_common::schema::render::to_json_value( + entry.value.graph(), + entry.value.root_type(), + entry.value.value(), + ) + .map(|v| v.to_string()) + .unwrap_or_else(|_| "".to_string()); + format!("{}={}", key.log_color_highlight(), value) + }) + .join("\n") +} + +pub fn format_component_applied_layers( + applied_layers: &[(ComponentLayerId, Option)], +) -> String { + applied_layers + .iter() + .map(|(id, selection)| match selection { + Some(selection) => { + format!("{}[{}]", id.name(), selection.as_str()) + } + None => id.name().to_string(), + }) + .join(", ") +} diff --git a/cli/golem-cli/src/model/environment.rs b/cli/golem-cli/src/model/environment.rs index d0a260d8af..c4d612fe9b 100644 --- a/cli/golem-cli/src/model/environment.rs +++ b/cli/golem-cli/src/model/environment.rs @@ -16,7 +16,8 @@ use crate::error::HintError; use crate::log::log_warn; use crate::log::{LogColorize, logln}; use crate::model::app_raw::Environment; -use crate::model::text::environment::format_resolved_environment_identity; +use crate::model::cli_output::StructuredOutput; +use crate::model::text::fmt::*; use anyhow::bail; use golem_common::model::account::AccountId; use golem_common::model::application::{ApplicationId, ApplicationName}; @@ -25,6 +26,7 @@ use golem_common::model::environment::{ EnvironmentCurrentDeploymentView, EnvironmentId, EnvironmentName, EnvironmentWithDetails, }; use indoc::formatdoc; +use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::future::Future; use std::str::FromStr; @@ -274,3 +276,97 @@ pub struct SelectedManifestEnvironment { pub environment_name: EnvironmentName, pub environment: Environment, } + +pub fn format_resolved_environment_identity(environment: &ResolvedEnvironmentIdentity) -> String { + match &environment.source { + ResolvedEnvironmentIdentitySource::Reference(environment_reference) => { + match environment_reference { + EnvironmentReference::Environment { environment_name } => { + format!( + "{}/{}", + environment.application_name.0.log_color_highlight(), + environment_name.0.log_color_highlight() + ) + } + EnvironmentReference::ApplicationEnvironment { + application_name, + environment_name, + } => { + format!( + "{}/{}", + application_name.0.log_color_highlight(), + environment_name.0.log_color_highlight() + ) + } + EnvironmentReference::AccountApplicationEnvironment { + account_email, + application_name, + environment_name, + } => { + format!( + "{}/{}/{}", + account_email.log_color_highlight(), + application_name.0.log_color_highlight(), + environment_name.0.log_color_highlight() + ) + } + } + } + ResolvedEnvironmentIdentitySource::DefaultFromManifest => format!( + "{}/{}", + environment.application_name.0.log_color_highlight(), + environment.environment_name.0.log_color_highlight(), + ), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnvironmentListView { + pub environments: Vec, +} + +impl StructuredOutput for EnvironmentListView { + const KIND: &'static str = "environment.list"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnvironmentSyncDeploymentOptionsResult { + pub updated: bool, +} + +impl StructuredOutput for EnvironmentSyncDeploymentOptionsResult { + const KIND: &'static str = "environment.sync-deployment-options"; +} + +impl NoTextOutput for EnvironmentSyncDeploymentOptionsResult {} +impl TextOutput for EnvironmentSyncDeploymentOptionsResult {} + +impl TextOutput for EnvironmentListView { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Application Name"), + Column::new("Environment Name"), + Column::new("Deployment Revision").fixed_right(), + Column::new("Deployment Version").fixed(), + ]); + for env in &self.environments { + table.add_row(vec![ + env.application.name.0.clone(), + env.environment.name.0.clone(), + env.environment + .current_deployment + .as_ref() + .map(|d| d.deployment_revision.get().to_string()) + .unwrap_or_default(), + env.environment + .current_deployment + .as_ref() + .map(|d| d.deployment_version.0.clone()) + .unwrap_or_default(), + ]); + } + log_table(table); + } +} diff --git a/cli/golem-cli/src/model/text/component.rs b/cli/golem-cli/src/model/text/component.rs deleted file mode 100644 index 1b840bc311..0000000000 --- a/cli/golem-cli/src/model/text/component.rs +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::log::LogColorize; -use crate::model::app::{ComponentLayerId, ComponentLayerProperties}; -use crate::model::cli_output::StructuredOutput; -use crate::model::component::ComponentView; -use crate::model::masking::{Masked, MaskingConfig, is_sensitive_key, mask_secret}; -use crate::model::text::fmt::*; -use colored::Colorize; -use colored::control::SHOULD_COLORIZE; -use golem_common::model::card::PolymorphicCard; -use golem_common::model::component::{ComponentName, InitialAgentFile, InstalledPlugin}; -use golem_common::model::worker::TypedAgentConfigEntry; -use itertools::Itertools; -use serde::Serializer; -use serde::ser::Error; -use serde::{Deserialize, Serialize}; -use serde_json::Value; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentListView { - pub components: Vec, -} - -impl Masked for ComponentListView { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - self.components = self - .components - .into_iter() - .map(|component| component.masked(config)) - .collect::>>()?; - Ok(self) - } -} - -impl StructuredOutput for ComponentListView { - const KIND: &'static str = "component.list"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl TextOutput for ComponentListView { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Name"), - Column::new("Revision").fixed_right(), - Column::new("Version").fixed_right(), - Column::new("Size").fixed_right(), - Column::new("Exports").fixed_right(), - ]); - for comp in &self.components { - table.add_row(vec![ - comp.component_name.to_string(), - comp.component_revision.to_string(), - comp.component_version.clone().unwrap_or_default(), - format_binary_size(&comp.component_size), - comp.exports.len().to_string(), - ]); - } - log_table(table); - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - self.masked(config)?.log(); - Ok(()) - } -} - -fn component_view_fields(view: &ComponentView) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("Component name", &view.component_name, format_main_id) - .fmt_field("Component ID", &view.component_id, format_id) - .fmt_field("Component revision", &view.component_revision, format_id) - .fmt_field_option("Component version", &view.component_version, format_id) - .fmt_field("Environment ID", &view.environment_id, format_id) - .fmt_field("Component size", &view.component_size, format_binary_size) - .fmt_field("Created at", &view.created_at, |d| d.to_string()) - .fmt_field("Exports", &view.exports, |e| format_exports(e.as_slice())); - - for (agent_type_name, provision_config) in &view.agent_type_provision_configs { - let prefix = format!("[{}] ", agent_type_name.0); - fields - .fmt_field_optional( - &format!("{}Environment", prefix), - &provision_config.env, - !provision_config.env.is_empty(), - format_env, - ) - .fmt_field_optional( - &format!("{}Agent config", prefix), - provision_config.config.as_slice(), - !provision_config.config.is_empty(), - format_typed_config, - ) - .fmt_field_optional( - &format!("{}Initial file system", prefix), - provision_config.files.as_slice(), - !provision_config.files.is_empty(), - format_files, - ) - .fmt_field_optional( - &format!("{}Plugins", prefix), - provision_config.plugins.as_slice(), - !provision_config.plugins.is_empty(), - format_plugins, - ) - .fmt_field_optional( - &format!("{}Initial permissions", prefix), - &provision_config.initial_permissions, - !initial_permission_is_empty(&provision_config.initial_permissions), - format_initial_permission, - ); - } - - fields.build() -} - -fn initial_permission_is_empty(card: &PolymorphicCard) -> bool { - card.lower_positive.is_empty() - && card.lower_negative.is_empty() - && card.upper_positive.is_empty() - && card.upper_negative.is_empty() -} - -fn format_initial_permission(card: &PolymorphicCard) -> String { - let mut sections = Vec::new(); - push_initial_permission_section(&mut sections, "lower positive", &card.lower_positive); - push_initial_permission_section(&mut sections, "lower negative", &card.lower_negative); - push_initial_permission_section(&mut sections, "upper positive", &card.upper_positive); - push_initial_permission_section(&mut sections, "upper negative", &card.upper_negative); - sections.join("\n") -} - -fn push_initial_permission_section( - sections: &mut Vec, - name: &str, - permissions: &[golem_common::model::card::PolymorphicPermissionPattern], -) { - if permissions.is_empty() { - return; - } - - let grants = permissions - .iter() - .map(|permission| { - permission - .render() - .unwrap_or_else(|error| format!("")) - }) - .map(|grant| format!(" - {grant}")) - .collect::>() - .join("\n"); - sections.push(format!("{name}:\n{grants}")); -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ComponentCreateView(pub ComponentView); - -impl Masked for ComponentCreateView { - fn masked(self, config: MaskingConfig) -> anyhow::Result { - Ok(Self(self.0.masked(config)?)) - } -} - -impl MessageWithFields for ComponentCreateView { - fn message(&self) -> String { - format!( - "Created new component {}", - format_message_highlight(&self.0.component_name) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - component_view_fields(&self.0) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ComponentUpdateView(pub ComponentView); - -impl Masked for ComponentUpdateView { - fn masked(self, config: MaskingConfig) -> anyhow::Result { - Ok(Self(self.0.masked(config)?)) - } -} - -impl MessageWithFields for ComponentUpdateView { - fn message(&self) -> String { - format!( - "Updated component {} to revision {}", - format_message_highlight(&self.0.component_name), - format_message_highlight(&self.0.component_revision), - ) - } - - fn fields(&self) -> Vec<(String, String)> { - component_view_fields(&self.0) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ComponentGetView(pub ComponentView); - -impl Masked for ComponentGetView { - fn masked(self, config: MaskingConfig) -> anyhow::Result { - Ok(Self(self.0.masked(config)?)) - } -} - -impl MessageWithFields for ComponentGetView { - fn message(&self) -> String { - format!( - "Got metadata for component {}", - format_message_highlight(&self.0.component_name) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - component_view_fields(&self.0) - } -} - -impl StructuredOutput for ComponentGetView { - const KIND: &'static str = "component.get"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ComponentManifestTraceView { - pub component_name: ComponentName, - pub properties: ComponentLayerProperties, -} - -impl StructuredOutput for ComponentManifestTraceView { - const KIND: &'static str = "component.manifest-trace"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.to_masked_value(config) - .map_err(S::Error::custom)? - .serialize(serializer) - } -} - -impl TextOutput for ComponentManifestTraceView { - fn log(&self) { - log_manifest_trace_properties(&self.properties); - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - if config.show_secrets { - self.log(); - } else { - let mut properties = serde_json::to_value(&self.properties)?; - mask_component_layer_properties(&mut properties); - log_manifest_trace_value(&properties); - } - Ok(()) - } -} - -impl ComponentManifestTraceView { - fn to_masked_value(&self, config: MaskingConfig) -> anyhow::Result { - let mut value = serde_json::to_value(self)?; - if !config.show_secrets - && let Some(properties) = value - .as_object_mut() - .and_then(|object| object.get_mut("properties")) - { - mask_component_layer_properties(properties); - } - Ok(value) - } -} - -fn log_manifest_trace_properties(properties: &ComponentLayerProperties) { - let rendered = if SHOULD_COLORIZE.should_colorize() { - to_colored_json(properties) - } else { - serde_json::to_string_pretty(properties).map_err(Into::into) - }; - - log_manifest_trace_rendered(rendered); -} - -fn log_manifest_trace_value(properties: &Value) { - let rendered = if SHOULD_COLORIZE.should_colorize() { - to_colored_json(properties) - } else { - serde_json::to_string_pretty(properties).map_err(Into::into) - }; - - log_manifest_trace_rendered(rendered); -} - -fn log_manifest_trace_rendered(rendered: anyhow::Result) { - match rendered { - Ok(rendered) => { - for line in rendered.lines() { - logln(line); - } - } - Err(error) => logln(format!("")), - } -} - -fn mask_component_layer_properties(properties: &mut Value) { - let Some(properties) = properties.as_object_mut() else { - return; - }; - - if let Some(config) = properties.get_mut("config") { - mask_config_property_payloads(config); - } - if let Some(env) = properties.get_mut("env") { - mask_sensitive_keyed_values(env); - } - if let Some(plugins) = properties.get_mut("plugins") { - mask_sensitive_keyed_values(plugins); - } -} - -fn mask_config_property_payloads(value: &mut Value) { - match value { - Value::Object(object) => { - for (key, value) in object { - match key.as_str() { - "value" | "newValue" => mask_json_leaf_values(value), - "insertedEntries" | "updatedEntries" => mask_json_object_values(value), - _ => mask_config_property_payloads(value), - } - } - } - Value::Array(values) => { - for value in values { - mask_config_property_payloads(value); - } - } - _ => {} - } -} - -fn mask_json_object_values(value: &mut Value) { - if let Some(object) = value.as_object_mut() { - for value in object.values_mut() { - mask_json_leaf_values(value); - } - } -} - -fn mask_json_leaf_values(value: &mut Value) { - match value { - Value::Null => {} - Value::Array(values) => { - for value in values { - mask_json_leaf_values(value); - } - } - Value::Object(object) => { - for value in object.values_mut() { - mask_json_leaf_values(value); - } - } - _ => *value = Value::String(mask_secret()), - } -} - -fn mask_sensitive_keyed_values(value: &mut Value) { - match value { - Value::Object(object) => { - for (key, value) in object { - if is_sensitive_key(key) { - mask_json_leaf_values(value); - } else { - mask_sensitive_keyed_values(value); - } - } - } - Value::Array(values) => { - for value in values { - mask_sensitive_keyed_values(value); - } - } - _ => {} - } -} - -fn format_files(files: &[InitialAgentFile]) -> String { - files - .iter() - .map(|file| { - format!( - "{} {} {}", - file.permissions.as_compact_str(), - file.path.as_path().as_str().log_color_highlight(), - file.content_hash.0.to_string().black() - ) - }) - .join("\n") -} - -fn format_plugins(plugins: &[InstalledPlugin]) -> String { - plugins - .iter() - .map(|plugin| { - let plugin_id = format!( - "{}: {}/{}", - plugin.priority, - plugin.plugin_name.log_color_highlight(), - plugin.plugin_version.log_color_highlight(), - ); - - if plugin.parameters.is_empty() { - plugin_id - } else { - format!( - "{}:\n{}", - plugin_id, - plugin - .parameters - .iter() - .map(|(k, v)| format!(" {}={}", k, v)) - .join("\n") - ) - } - }) - .join("\n") -} - -fn format_typed_config(config: &[TypedAgentConfigEntry]) -> String { - config - .iter() - .map(|entry| { - let key = entry.path.join("."); - let value = golem_common::schema::render::to_json_value( - entry.value.graph(), - entry.value.root_type(), - entry.value.value(), - ) - .map(|v| v.to_string()) - .unwrap_or_else(|_| "".to_string()); - format!("{}={}", key.log_color_highlight(), value) - }) - .join("\n") -} - -pub fn format_component_applied_layers( - applied_layers: &[(ComponentLayerId, Option)], -) -> String { - applied_layers - .iter() - .map(|(id, selection)| match selection { - Some(selection) => { - format!("{}[{}]", id.name(), selection.as_str()) - } - None => id.name().to_string(), - }) - .join(", ") -} diff --git a/cli/golem-cli/src/model/text/environment.rs b/cli/golem-cli/src/model/text/environment.rs deleted file mode 100644 index 555e46d4fe..0000000000 --- a/cli/golem-cli/src/model/text/environment.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::log::LogColorize; -use crate::model::cli_output::StructuredOutput; -use crate::model::environment::{ - EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, -}; -use crate::model::text::fmt::{ - Column, NoTextOutput, TextOutput, log_table, new_table_full_condensed, -}; -use golem_client::model::EnvironmentWithDetails; -use serde::{Deserialize, Serialize}; - -pub fn format_resolved_environment_identity(environment: &ResolvedEnvironmentIdentity) -> String { - match &environment.source { - ResolvedEnvironmentIdentitySource::Reference(environment_reference) => { - match environment_reference { - EnvironmentReference::Environment { environment_name } => { - format!( - "{}/{}", - environment.application_name.0.log_color_highlight(), - environment_name.0.log_color_highlight() - ) - } - EnvironmentReference::ApplicationEnvironment { - application_name, - environment_name, - } => { - format!( - "{}/{}", - application_name.0.log_color_highlight(), - environment_name.0.log_color_highlight() - ) - } - EnvironmentReference::AccountApplicationEnvironment { - account_email, - application_name, - environment_name, - } => { - format!( - "{}/{}/{}", - account_email.log_color_highlight(), - application_name.0.log_color_highlight(), - environment_name.0.log_color_highlight() - ) - } - } - } - ResolvedEnvironmentIdentitySource::DefaultFromManifest => format!( - "{}/{}", - environment.application_name.0.log_color_highlight(), - environment.environment_name.0.log_color_highlight(), - ), - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EnvironmentListView { - pub environments: Vec, -} - -impl StructuredOutput for EnvironmentListView { - const KIND: &'static str = "environment.list"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EnvironmentSyncDeploymentOptionsResult { - pub updated: bool, -} - -impl StructuredOutput for EnvironmentSyncDeploymentOptionsResult { - const KIND: &'static str = "environment.sync-deployment-options"; -} - -impl NoTextOutput for EnvironmentSyncDeploymentOptionsResult {} -impl TextOutput for EnvironmentSyncDeploymentOptionsResult {} - -impl TextOutput for EnvironmentListView { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Application Name"), - Column::new("Environment Name"), - Column::new("Deployment Revision").fixed_right(), - Column::new("Deployment Version").fixed(), - ]); - for env in &self.environments { - table.add_row(vec![ - env.application.name.0.clone(), - env.environment.name.0.clone(), - env.environment - .current_deployment - .as_ref() - .map(|d| d.deployment_revision.get().to_string()) - .unwrap_or_default(), - env.environment - .current_deployment - .as_ref() - .map(|d| d.deployment_version.0.clone()) - .unwrap_or_default(), - ]); - } - log_table(table); - } -} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 16b55e26d2..915c7f5c8a 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -16,10 +16,8 @@ pub mod account; pub mod action_result; pub mod agent; pub mod card; -pub mod component; pub mod deployment; pub mod diff; -pub mod environment; pub mod fmt; pub mod grant; pub mod help; From 221ba6cba67393b5db8cabc1eeb4f36af1f4e123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:09:03 +0200 Subject: [PATCH 44/70] move grant helpers into model/grant --- cli/golem-cli/src/model/{text => }/grant.rs | 0 cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/text/account.rs | 2 +- cli/golem-cli/src/model/text/card.rs | 2 +- cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 3 insertions(+), 3 deletions(-) rename cli/golem-cli/src/model/{text => }/grant.rs (100%) diff --git a/cli/golem-cli/src/model/text/grant.rs b/cli/golem-cli/src/model/grant.rs similarity index 100% rename from cli/golem-cli/src/model/text/grant.rs rename to cli/golem-cli/src/model/grant.rs diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 8c24cde503..834b68e001 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -23,6 +23,7 @@ pub mod config; pub mod deploy; pub mod environment; pub mod format; +pub mod grant; pub mod http_api; pub mod input; pub mod invoke_result_view; diff --git a/cli/golem-cli/src/model/text/account.rs b/cli/golem-cli/src/model/text/account.rs index 352fff4cfc..dce9ef256a 100644 --- a/cli/golem-cli/src/model/text/account.rs +++ b/cli/golem-cli/src/model/text/account.rs @@ -13,9 +13,9 @@ // limitations under the License. use crate::model::cli_output::StructuredOutput; +use crate::model::grant::{format_grants, grant_count}; use crate::model::masking::Masked; use crate::model::text::fmt::*; -use crate::model::text::grant::{format_grants, grant_count}; use golem_client::model::{Account, PermissionShare}; use golem_common::model::account::AccountId; use golem_common::model::permission_share::PermissionShareId; diff --git a/cli/golem-cli/src/model/text/card.rs b/cli/golem-cli/src/model/text/card.rs index 5be642a8f7..648d152c7a 100644 --- a/cli/golem-cli/src/model/text/card.rs +++ b/cli/golem-cli/src/model/text/card.rs @@ -13,9 +13,9 @@ // limitations under the License. use crate::model::cli_output::StructuredOutput; +use crate::model::grant::format_grants; use crate::model::masking::Masked; use crate::model::text::fmt::*; -use crate::model::text::grant::format_grants; use golem_client::model::{CardManagedBy, StoredCard}; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 915c7f5c8a..70a05120d2 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -19,7 +19,6 @@ pub mod card; pub mod deployment; pub mod diff; pub mod fmt; -pub mod grant; pub mod help; pub mod http_api_deployment; pub mod http_api_domain; From 70703a421ad3e97132198f094adaa2105e0c0a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:09:56 +0200 Subject: [PATCH 45/70] move account views into model/account --- cli/golem-cli/src/command_handler/account.rs | 2 +- cli/golem-cli/src/model/{text => }/account.rs | 0 cli/golem-cli/src/model/cli_output/tests.rs | 18 +++++++++--------- cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 11 insertions(+), 11 deletions(-) rename cli/golem-cli/src/model/{text => }/account.rs (100%) diff --git a/cli/golem-cli/src/command_handler/account.rs b/cli/golem-cli/src/command_handler/account.rs index 24a0ab56ae..3c119941f1 100644 --- a/cli/golem-cli/src/command_handler/account.rs +++ b/cli/golem-cli/src/command_handler/account.rs @@ -20,7 +20,7 @@ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_warn_action; -use crate::model::text::account::{ +use crate::model::account::{ AccountDeleteResult, AccountGetView, AccountNewView, AccountUpdateView, PermissionShareDeleteResult, PermissionShareGetView, PermissionShareListView, PermissionShareNewView, PermissionShareUpdateView, diff --git a/cli/golem-cli/src/model/text/account.rs b/cli/golem-cli/src/model/account.rs similarity index 100% rename from cli/golem-cli/src/model/text/account.rs rename to cli/golem-cli/src/model/account.rs diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index e25ebc9f97..067232a94c 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3070,7 +3070,7 @@ fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { fn arb_account_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, account_id)| { - crate::model::text::account::AccountDeleteResult { + crate::model::account::AccountDeleteResult { deleted, account_id: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&account_id).expect("generated UUID should parse"), @@ -3081,15 +3081,15 @@ fn arb_account_delete_result() -> OutputDocumentStrategy { } fn arb_account_get_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountGetView)) + serialized_output(arb_account().prop_map(crate::model::account::AccountGetView)) } fn arb_account_new_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountNewView)) + serialized_output(arb_account().prop_map(crate::model::account::AccountNewView)) } fn arb_account_update_result() -> OutputDocumentStrategy { - serialized_output(arb_account().prop_map(crate::model::text::account::AccountUpdateView)) + serialized_output(arb_account().prop_map(crate::model::account::AccountUpdateView)) } fn arb_account() -> BoxedStrategy { @@ -3130,7 +3130,7 @@ fn arb_account_role() -> BoxedStrategy { fn arb_permission_share_delete_result() -> OutputDocumentStrategy { serialized_output((any::(), arb_small_string()).prop_map( - |(deleted, permission_share_id)| crate::model::text::account::PermissionShareDeleteResult { + |(deleted, permission_share_id)| crate::model::account::PermissionShareDeleteResult { deleted, permission_share_id: golem_common::model::permission_share::PermissionShareId( uuid::Uuid::parse_str(&permission_share_id).expect("generated UUID should parse"), @@ -3141,26 +3141,26 @@ fn arb_permission_share_delete_result() -> OutputDocumentStrategy { fn arb_permission_share_get_result() -> OutputDocumentStrategy { serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareGetView), + arb_permission_share().prop_map(crate::model::account::PermissionShareGetView), ) } fn arb_permission_share_new_result() -> OutputDocumentStrategy { serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareNewView), + arb_permission_share().prop_map(crate::model::account::PermissionShareNewView), ) } fn arb_permission_share_update_result() -> OutputDocumentStrategy { serialized_output( - arb_permission_share().prop_map(crate::model::text::account::PermissionShareUpdateView), + arb_permission_share().prop_map(crate::model::account::PermissionShareUpdateView), ) } fn arb_permission_share_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_permission_share(), 0..5).prop_map(|permission_shares| { - crate::model::text::account::PermissionShareListView { permission_shares } + crate::model::account::PermissionShareListView { permission_shares } }), ) } diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 834b68e001..c9122a49b4 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod account; pub mod agent; pub mod app; pub mod app_raw; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 70a05120d2..a36ad6b4a6 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod account; pub mod action_result; pub mod agent; pub mod card; From 2ff9b1a926191843ed59d7ef487b858ee13524d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:10:43 +0200 Subject: [PATCH 46/70] move card views into model/card --- cli/golem-cli/src/command_handler/card.rs | 2 +- cli/golem-cli/src/model/{text => }/card.rs | 0 cli/golem-cli/src/model/cli_output/tests.rs | 9 ++++----- cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 6 insertions(+), 7 deletions(-) rename cli/golem-cli/src/model/{text => }/card.rs (100%) diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index a31160aaad..856d6d5182 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -20,7 +20,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_warn_action; use crate::model::agent::RawAgentId; -use crate::model::text::card::{CardGetView, CardListView, CardRevokeResult}; +use crate::model::card::{CardGetView, CardListView, CardRevokeResult}; use anyhow::bail; use golem_client::api::{CardClient, WorkerClient}; use golem_common::model::account::AccountId; diff --git a/cli/golem-cli/src/model/text/card.rs b/cli/golem-cli/src/model/card.rs similarity index 100% rename from cli/golem-cli/src/model/text/card.rs rename to cli/golem-cli/src/model/card.rs diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 067232a94c..0df5836297 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3216,21 +3216,20 @@ fn arb_permission_share_data() } fn arb_card_get_result() -> OutputDocumentStrategy { - serialized_output(arb_stored_card().prop_map(crate::model::text::card::CardGetView)) + serialized_output(arb_stored_card().prop_map(crate::model::card::CardGetView)) } fn arb_card_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_stored_card(), 0..5) - .prop_map(|cards| crate::model::text::card::CardListView { cards }), + .prop_map(|cards| crate::model::card::CardListView { cards }), ) } fn arb_card_revoke_result() -> OutputDocumentStrategy { serialized_output( - proptest::collection::vec(arb_uuid(), 0..5).prop_map(|revoked_card_ids| { - crate::model::text::card::CardRevokeResult { revoked_card_ids } - }), + proptest::collection::vec(arb_uuid(), 0..5) + .prop_map(|revoked_card_ids| crate::model::card::CardRevokeResult { revoked_card_ids }), ) } diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index c9122a49b4..244e11fabb 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -16,6 +16,7 @@ pub mod account; pub mod agent; pub mod app; pub mod app_raw; +pub mod card; pub mod cascade; pub mod cli_command_metadata; pub mod cli_output; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index a36ad6b4a6..db6c6fc3dd 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -14,7 +14,6 @@ pub mod action_result; pub mod agent; -pub mod card; pub mod deployment; pub mod diff; pub mod fmt; From f4806725a61dbf6280bdde9cbb015ed4c237ab5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:11:33 +0200 Subject: [PATCH 47/70] move token views into model/token --- cli/golem-cli/src/command_handler/api_token.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 6 +++--- cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/text/mod.rs | 1 - cli/golem-cli/src/model/{text => }/token.rs | 0 5 files changed, 5 insertions(+), 5 deletions(-) rename cli/golem-cli/src/model/{text => }/token.rs (100%) diff --git a/cli/golem-cli/src/command_handler/api_token.rs b/cli/golem-cli/src/command_handler/api_token.rs index fab91a5044..076691916e 100644 --- a/cli/golem-cli/src/command_handler/api_token.rs +++ b/cli/golem-cli/src/command_handler/api_token.rs @@ -17,7 +17,7 @@ use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; use crate::log::{LogColorize, log_warn_action}; -use crate::model::text::token::{TokenDeleteResult, TokenListView, TokenNewView}; +use crate::model::token::{TokenDeleteResult, TokenListView, TokenNewView}; use chrono::{DateTime, Utc}; use golem_client::api::{AccountClient, TokenClient}; use golem_client::model::TokenCreation; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 0df5836297..2a1069c9d9 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3435,7 +3435,7 @@ fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { fn arb_token_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, token_id)| { - crate::model::text::token::TokenDeleteResult { + crate::model::token::TokenDeleteResult { deleted, token_id: golem_common::model::auth::TokenId( uuid::Uuid::parse_str(&token_id).expect("generated UUID should parse"), @@ -3448,12 +3448,12 @@ fn arb_token_delete_result() -> OutputDocumentStrategy { fn arb_token_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_token(), 0..5) - .prop_map(|tokens| crate::model::text::token::TokenListView { tokens }), + .prop_map(|tokens| crate::model::token::TokenListView { tokens }), ) } fn arb_token_new_result() -> OutputDocumentStrategy { - serialized_output(arb_token_with_secret().prop_map(crate::model::text::token::TokenNewView)) + serialized_output(arb_token_with_secret().prop_map(crate::model::token::TokenNewView)) } fn arb_token() -> BoxedStrategy { diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 244e11fabb..313667cb29 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -35,3 +35,4 @@ pub mod plugin; pub mod repl; pub mod template; pub mod text; +pub mod token; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index db6c6fc3dd..21c1459da3 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -27,4 +27,3 @@ pub mod retry_policy; pub mod secret; pub mod server; pub mod template; -pub mod token; diff --git a/cli/golem-cli/src/model/text/token.rs b/cli/golem-cli/src/model/token.rs similarity index 100% rename from cli/golem-cli/src/model/text/token.rs rename to cli/golem-cli/src/model/token.rs From 0021604f3cf04167b9bde43331d14c1460118140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:12:25 +0200 Subject: [PATCH 48/70] move secret views into model/secret --- cli/golem-cli/src/command_handler/secret.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 20 ++++++++++---------- cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/{text => }/secret.rs | 0 cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 12 insertions(+), 12 deletions(-) rename cli/golem-cli/src/model/{text => }/secret.rs (100%) diff --git a/cli/golem-cli/src/command_handler/secret.rs b/cli/golem-cli/src/command_handler/secret.rs index 9476c0b57b..1bb83a988c 100644 --- a/cli/golem-cli/src/command_handler/secret.rs +++ b/cli/golem-cli/src/command_handler/secret.rs @@ -21,7 +21,7 @@ use crate::error::service::MapServiceError; use crate::log::log_error; use crate::model::environment::EnvironmentResolveMode; use crate::model::language::GuestLanguage; -use crate::model::text::secret::{ +use crate::model::secret::{ SecretCreateView, SecretDeleteView, SecretGetView, SecretListView, SecretUpdateView, }; use anyhow::bail; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 2a1069c9d9..d9497b4cef 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -1033,27 +1033,27 @@ fn cli_output_schema_validates_schema_native_secret_outputs() { let outputs = vec![ to_structured_output_value_masked( - crate::model::text::secret::SecretCreateView(secret.clone().into()), + crate::model::secret::SecretCreateView(secret.clone().into()), MaskingConfig::hide_secrets(), ) .expect("secret.create should serialize"), to_structured_output_value_masked( - crate::model::text::secret::SecretDeleteView(secret.clone().into()), + crate::model::secret::SecretDeleteView(secret.clone().into()), MaskingConfig::hide_secrets(), ) .expect("secret.delete should serialize"), to_structured_output_value_masked( - crate::model::text::secret::SecretGetView(secret.clone().into()), + crate::model::secret::SecretGetView(secret.clone().into()), MaskingConfig::hide_secrets(), ) .expect("secret.get should serialize"), to_structured_output_value_masked( - crate::model::text::secret::SecretUpdateView(secret.clone().into()), + crate::model::secret::SecretUpdateView(secret.clone().into()), MaskingConfig::hide_secrets(), ) .expect("secret.update-value should serialize"), to_structured_output_value_masked( - crate::model::text::secret::SecretListView { + crate::model::secret::SecretListView { secrets: vec![secret.into()], environment_name: "generated-environment".to_string(), show_ids: false, @@ -5406,7 +5406,7 @@ fn arb_secret_create_result() -> OutputDocumentStrategy { arb_secret() .prop_map(|secret| { to_structured_output_value_masked( - crate::model::text::secret::SecretCreateView(secret.into()), + crate::model::secret::SecretCreateView(secret.into()), MaskingConfig::hide_secrets(), ) .expect("generated secret create should serialize") @@ -5418,7 +5418,7 @@ fn arb_secret_delete_result() -> OutputDocumentStrategy { arb_secret() .prop_map(|secret| { to_structured_output_value_masked( - crate::model::text::secret::SecretDeleteView(secret.into()), + crate::model::secret::SecretDeleteView(secret.into()), MaskingConfig::hide_secrets(), ) .expect("generated secret delete should serialize") @@ -5430,7 +5430,7 @@ fn arb_secret_get_result() -> OutputDocumentStrategy { arb_secret() .prop_map(|secret| { to_structured_output_value_masked( - crate::model::text::secret::SecretGetView(secret.into()), + crate::model::secret::SecretGetView(secret.into()), MaskingConfig::hide_secrets(), ) .expect("generated secret get should serialize") @@ -5442,7 +5442,7 @@ fn arb_secret_update_value_result() -> OutputDocumentStrategy { arb_secret() .prop_map(|secret| { to_structured_output_value_masked( - crate::model::text::secret::SecretUpdateView(secret.into()), + crate::model::secret::SecretUpdateView(secret.into()), MaskingConfig::hide_secrets(), ) .expect("generated secret update should serialize") @@ -5454,7 +5454,7 @@ fn arb_secret_list_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_secret(), 0..5) .prop_map(|secrets| { to_structured_output_value_masked( - crate::model::text::secret::SecretListView { + crate::model::secret::SecretListView { secrets: secrets.into_iter().map(Into::into).collect(), environment_name: "generated-environment".to_string(), show_ids: false, diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 313667cb29..90ace54814 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -33,6 +33,7 @@ pub mod language; pub mod masking; pub mod plugin; pub mod repl; +pub mod secret; pub mod template; pub mod text; pub mod token; diff --git a/cli/golem-cli/src/model/text/secret.rs b/cli/golem-cli/src/model/secret.rs similarity index 100% rename from cli/golem-cli/src/model/text/secret.rs rename to cli/golem-cli/src/model/secret.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 21c1459da3..3bbee32f8a 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -24,6 +24,5 @@ pub mod http_api_security; pub mod profile; pub mod resource_definition; pub mod retry_policy; -pub mod secret; pub mod server; pub mod template; From 4b82723332d7477442311fe0007d496a08788765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:13:06 +0200 Subject: [PATCH 49/70] move resource definition views into model/resource_definition --- .../src/command_handler/resource_definition.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 10 +++++----- cli/golem-cli/src/model/mod.rs | 1 + .../src/model/{text => }/resource_definition.rs | 0 cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 7 insertions(+), 7 deletions(-) rename cli/golem-cli/src/model/{text => }/resource_definition.rs (100%) diff --git a/cli/golem-cli/src/command_handler/resource_definition.rs b/cli/golem-cli/src/command_handler/resource_definition.rs index d4dc3df8b1..67b5f51013 100644 --- a/cli/golem-cli/src/command_handler/resource_definition.rs +++ b/cli/golem-cli/src/command_handler/resource_definition.rs @@ -19,7 +19,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_error; use crate::model::environment::EnvironmentResolveMode; -use crate::model::text::resource_definition::{ +use crate::model::resource_definition::{ ResourceDefinitionCreateView, ResourceDefinitionDeleteView, ResourceDefinitionGetView, ResourceDefinitionListView, ResourceDefinitionUpdateView, }; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index d9497b4cef..c6b78d1e25 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -4967,35 +4967,35 @@ fn arb_profile_config_set_format_result() -> OutputDocumentStrategy { fn arb_resource_create_result() -> OutputDocumentStrategy { serialized_output( arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionCreateView), + .prop_map(crate::model::resource_definition::ResourceDefinitionCreateView), ) } fn arb_resource_delete_result() -> OutputDocumentStrategy { serialized_output( arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionDeleteView), + .prop_map(crate::model::resource_definition::ResourceDefinitionDeleteView), ) } fn arb_resource_get_result() -> OutputDocumentStrategy { serialized_output( arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionGetView), + .prop_map(crate::model::resource_definition::ResourceDefinitionGetView), ) } fn arb_resource_update_result() -> OutputDocumentStrategy { serialized_output( arb_resource_definition() - .prop_map(crate::model::text::resource_definition::ResourceDefinitionUpdateView), + .prop_map(crate::model::resource_definition::ResourceDefinitionUpdateView), ) } fn arb_resource_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_resource_definition(), 0..5).prop_map(|resources| { - crate::model::text::resource_definition::ResourceDefinitionListView { resources } + crate::model::resource_definition::ResourceDefinitionListView { resources } }), ) } diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 90ace54814..49998f264e 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -33,6 +33,7 @@ pub mod language; pub mod masking; pub mod plugin; pub mod repl; +pub mod resource_definition; pub mod secret; pub mod template; pub mod text; diff --git a/cli/golem-cli/src/model/text/resource_definition.rs b/cli/golem-cli/src/model/resource_definition.rs similarity index 100% rename from cli/golem-cli/src/model/text/resource_definition.rs rename to cli/golem-cli/src/model/resource_definition.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 3bbee32f8a..ed1125ad33 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -22,7 +22,6 @@ pub mod http_api_deployment; pub mod http_api_domain; pub mod http_api_security; pub mod profile; -pub mod resource_definition; pub mod retry_policy; pub mod server; pub mod template; From 6143cf1e8731aea7562734bef061605bb6ac1a7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:13:48 +0200 Subject: [PATCH 50/70] move retry policy views into model/retry_policy --- cli/golem-cli/src/command_handler/retry_policy.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 12 +++++------- cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/{text => }/retry_policy.rs | 0 cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 7 insertions(+), 9 deletions(-) rename cli/golem-cli/src/model/{text => }/retry_policy.rs (100%) diff --git a/cli/golem-cli/src/command_handler/retry_policy.rs b/cli/golem-cli/src/command_handler/retry_policy.rs index 72df49c331..59b68e1c16 100644 --- a/cli/golem-cli/src/command_handler/retry_policy.rs +++ b/cli/golem-cli/src/command_handler/retry_policy.rs @@ -19,7 +19,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_error; use crate::model::environment::EnvironmentResolveMode; -use crate::model::text::retry_policy::{ +use crate::model::retry_policy::{ RetryPolicyCreateView, RetryPolicyDeleteView, RetryPolicyGetView, RetryPolicyListView, RetryPolicyUpdateView, }; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index c6b78d1e25..a9b81eac57 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -5339,32 +5339,30 @@ fn arb_api_retry_policy_with_depth( fn arb_retry_policy_create_result() -> OutputDocumentStrategy { serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyCreateView), + arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyCreateView), ) } fn arb_retry_policy_delete_result() -> OutputDocumentStrategy { serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyDeleteView), + arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyDeleteView), ) } fn arb_retry_policy_get_result() -> OutputDocumentStrategy { - serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyGetView), - ) + serialized_output(arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyGetView)) } fn arb_retry_policy_update_result() -> OutputDocumentStrategy { serialized_output( - arb_retry_policy().prop_map(crate::model::text::retry_policy::RetryPolicyUpdateView), + arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyUpdateView), ) } fn arb_retry_policy_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_retry_policy(), 0..5).prop_map(|retry_policies| { - crate::model::text::retry_policy::RetryPolicyListView { retry_policies } + crate::model::retry_policy::RetryPolicyListView { retry_policies } }), ) } diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 49998f264e..a06597c32e 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -34,6 +34,7 @@ pub mod masking; pub mod plugin; pub mod repl; pub mod resource_definition; +pub mod retry_policy; pub mod secret; pub mod template; pub mod text; diff --git a/cli/golem-cli/src/model/text/retry_policy.rs b/cli/golem-cli/src/model/retry_policy.rs similarity index 100% rename from cli/golem-cli/src/model/text/retry_policy.rs rename to cli/golem-cli/src/model/retry_policy.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index ed1125ad33..13251846dc 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -22,6 +22,5 @@ pub mod http_api_deployment; pub mod http_api_domain; pub mod http_api_security; pub mod profile; -pub mod retry_policy; pub mod server; pub mod template; From b4b3dc014494f1672a03c7d2614017aa97517114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:15:28 +0200 Subject: [PATCH 51/70] move help views into model/help --- cli/golem-cli/src/command_handler/agent/mod.rs | 8 ++++---- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/command_handler/app/template.rs | 2 +- cli/golem-cli/src/command_handler/component/mod.rs | 2 +- cli/golem-cli/src/command_handler/environment.rs | 2 +- cli/golem-cli/src/command_handler/partial_match.rs | 4 ++-- cli/golem-cli/src/command_handler/profile/config.rs | 2 +- cli/golem-cli/src/model/{text => }/help.rs | 0 cli/golem-cli/src/model/mod.rs | 1 + cli/golem-cli/src/model/text/mod.rs | 1 - 10 files changed, 12 insertions(+), 12 deletions(-) rename cli/golem-cli/src/model/{text => }/help.rs (100%) diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 6762d05baf..250d06810f 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -31,6 +31,10 @@ use crate::log::{ }; use crate::model::component::ComponentNameMatchKind; use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersResult}; +use crate::model::help::{ + AgentNameHelp, ArgumentError, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, + ParameterErrorTableView, +}; use crate::model::invoke_result_view::InvokeResultView; use crate::model::text::agent::action_result::{ AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, @@ -42,10 +46,6 @@ use crate::model::text::agent::{ AgentCreateView, AgentGetView, format_agent_id_match, format_timestamp, }; use crate::model::text::fmt::{log_fuzzy_match, log_text_view}; -use crate::model::text::help::{ - AgentNameHelp, ArgumentError, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, - ParameterErrorTableView, -}; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index ba40eec5d4..cb213c298e 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -54,12 +54,12 @@ use crate::model::deploy::{ preferred_source_language_for_setup, }; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; +use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; use crate::model::text::agent::AgentTypeListView; use crate::model::text::deployment::{DeploymentListView, DeploymentNewView}; use crate::model::text::diff::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; -use crate::model::text::help::AvailableComponentNamesHelp; use crate::model::text::server::ToFormattedServerContext; use crate::model::text::template::TemplateListView; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index 36aef117c3..eeef685fed 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -33,10 +33,10 @@ use crate::log::{ LogColorize, LogIndent, log_action, log_anyhow_error, log_error, log_failed_to, log_finished_ok, log_skipping_up_to_date, logln, }; +use crate::model::help::{AppNewNextStepsHint, AppNewNextStepsMode}; use crate::model::language::GuestLanguage; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::log_text_view; -use crate::model::text::help::{AppNewNextStepsHint, AppNewNextStepsMode}; use crate::validation::ValidationBuilder; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 2a19471e24..87f2409bf1 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -42,13 +42,13 @@ use crate::model::deploy::{ use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; +use crate::model::help::ComponentNameHelp; use crate::model::language::GuestLanguage; use crate::model::plugin::PluginNameAndVersion; use crate::model::text::agent::action_result::{ AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; use crate::model::text::fmt::log_text_view; -use crate::model::text::help::ComponentNameHelp; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; use futures_util::future::OptionFuture; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index a897376404..00b4d401ab 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -26,10 +26,10 @@ use crate::model::environment::{EnvironmentListView, EnvironmentSyncDeploymentOp use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; +use crate::model::help::EnvironmentNameHelp; use crate::model::plugin::PluginNameAndVersion; use crate::model::text::diff::log_unified_diff; use crate::model::text::fmt::log_text_view; -use crate::model::text::help::EnvironmentNameHelp; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; use golem_client::model::{EnvironmentCreation, EnvironmentPluginGrantWithDetails}; diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index e017cc1caf..7b431f3b82 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -25,11 +25,11 @@ use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; use crate::model::component::ComponentNameMatchKind; use crate::model::environment::EnvironmentResolveMode; use crate::model::format::Format; -use crate::model::text::fmt::{DecoratedIndent, log_text_view}; -use crate::model::text::help::{ +use crate::model::help::{ AgentNameHelp, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, AvailableProfileNamesHelp, EnvironmentNameHelp, }; +use crate::model::text::fmt::{DecoratedIndent, log_text_view}; use colored::Colorize; use indoc::indoc; use std::sync::Arc; diff --git a/cli/golem-cli/src/command_handler/profile/config.rs b/cli/golem-cli/src/command_handler/profile/config.rs index 874938c169..a302d774da 100644 --- a/cli/golem-cli/src/command_handler/profile/config.rs +++ b/cli/golem-cli/src/command_handler/profile/config.rs @@ -21,8 +21,8 @@ use crate::log::log_error; use crate::log::logln; use crate::log::log_action; use crate::model::format::Format; +use crate::model::help::AvailableProfileNamesHelp; use crate::model::text::fmt::log_text_view; -use crate::model::text::help::AvailableProfileNamesHelp; use crate::model::text::profile::ProfileConfigSetFormatResult; use anyhow::bail; use std::sync::Arc; diff --git a/cli/golem-cli/src/model/text/help.rs b/cli/golem-cli/src/model/help.rs similarity index 100% rename from cli/golem-cli/src/model/text/help.rs rename to cli/golem-cli/src/model/help.rs diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index a06597c32e..b6169f32e9 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -26,6 +26,7 @@ pub mod deploy; pub mod environment; pub mod format; pub mod grant; +pub mod help; pub mod http_api; pub mod input; pub mod invoke_result_view; diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 13251846dc..809db31026 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -17,7 +17,6 @@ pub mod agent; pub mod deployment; pub mod diff; pub mod fmt; -pub mod help; pub mod http_api_deployment; pub mod http_api_domain; pub mod http_api_security; From 346408d0b349ccaa6f4142806ecf040ee2c25cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:20:23 +0200 Subject: [PATCH 52/70] move agent views into model/agent --- .../src/command_handler/agent/mod.rs | 16 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- .../src/command_handler/component/mod.rs | 6 +- .../model/{text => }/agent/action_result.rs | 0 .../src/model/{text => }/agent/files.rs | 0 cli/golem-cli/src/model/agent/mod.rs | 542 ++++++++++++++- .../src/model/{text => }/agent/oplog.rs | 0 cli/golem-cli/src/model/cli_output/tests.rs | 54 +- cli/golem-cli/src/model/deploy.rs | 12 + cli/golem-cli/src/model/invoke_result_view.rs | 43 +- cli/golem-cli/src/model/text/agent/mod.rs | 620 ------------------ cli/golem-cli/src/model/text/mod.rs | 1 - 12 files changed, 632 insertions(+), 664 deletions(-) rename cli/golem-cli/src/model/{text => }/agent/action_result.rs (100%) rename cli/golem-cli/src/model/{text => }/agent/files.rs (100%) rename cli/golem-cli/src/model/{text => }/agent/oplog.rs (100%) delete mode 100644 cli/golem-cli/src/model/text/agent/mod.rs diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 250d06810f..975540c56a 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -29,6 +29,13 @@ use crate::log::{ LogColorize, LogIndent, log_action, log_error, log_error_action, log_failed_to, log_warn, log_warn_action, logln, }; +use crate::model::agent::action_result::{ + AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, + AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, +}; +use crate::model::agent::files::{AgentFilesView, FileNodeView}; +use crate::model::agent::oplog::AgentOplogEntryView; +use crate::model::agent::{AgentCreateView, AgentGetView, format_agent_id_match, format_timestamp}; use crate::model::component::ComponentNameMatchKind; use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersResult}; use crate::model::help::{ @@ -36,15 +43,6 @@ use crate::model::help::{ ParameterErrorTableView, }; use crate::model::invoke_result_view::InvokeResultView; -use crate::model::text::agent::action_result::{ - AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, - AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, -}; -use crate::model::text::agent::files::{AgentFilesView, FileNodeView}; -use crate::model::text::agent::oplog::AgentOplogEntryView; -use crate::model::text::agent::{ - AgentCreateView, AgentGetView, format_agent_id_match, format_timestamp, -}; use crate::model::text::fmt::{log_fuzzy_match, log_text_view}; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index cb213c298e..71fd2e7a05 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -41,6 +41,7 @@ use crate::log::{ log_finished_ok, log_finished_up_to_date, log_preformatted, log_skipping_up_to_date, log_warn, log_warn_action, logged_failed_to, logged_finished_or_failed_to, logln, }; +use crate::model::agent::AgentTypeListView; use crate::model::agent::AgentTypeView; use crate::model::agent::AgentUpdateMode; use crate::model::app::{ @@ -56,7 +57,6 @@ use crate::model::deploy::{ use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; -use crate::model::text::agent::AgentTypeListView; use crate::model::text::deployment::{DeploymentListView, DeploymentNewView}; use crate::model::text::diff::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 87f2409bf1..09e785b1a5 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -25,6 +25,9 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_error, log_warn_action, logln}; use crate::model::agent::AgentUpdateMode; +use crate::model::agent::action_result::{ + AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, +}; use crate::model::app::BuildConfig; use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; use crate::model::app_raw; @@ -45,9 +48,6 @@ use crate::model::environment::{ use crate::model::help::ComponentNameHelp; use crate::model::language::GuestLanguage; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::agent::action_result::{ - AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, -}; use crate::model::text::fmt::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; diff --git a/cli/golem-cli/src/model/text/agent/action_result.rs b/cli/golem-cli/src/model/agent/action_result.rs similarity index 100% rename from cli/golem-cli/src/model/text/agent/action_result.rs rename to cli/golem-cli/src/model/agent/action_result.rs diff --git a/cli/golem-cli/src/model/text/agent/files.rs b/cli/golem-cli/src/model/agent/files.rs similarity index 100% rename from cli/golem-cli/src/model/text/agent/files.rs rename to cli/golem-cli/src/model/agent/files.rs diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index f0b7b1e753..395f959b4d 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -12,18 +12,27 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod action_result; pub mod extraction; +pub mod files; +pub mod oplog; pub mod stream; use crate::agent_id_display::SourceLanguage; use crate::command::shared_args::StreamArgs; +use crate::log::{LogColorize, logln}; +use crate::model::cli_output::StructuredOutput; use crate::model::component::{ComponentNameMatchKind, render_agent_constructor}; use crate::model::environment::{ EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, }; use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; +use crate::model::text::fmt::*; +use chrono::DateTime; use clap::ValueEnum; +use colored::Colorize; use colored::control::SHOULD_COLORIZE; +use comfy_table::Color as ComfyColor; use golem_common::base_model::component_metadata::AgentTypeProvisionConfig; use golem_common::model::account::AccountId; use golem_common::model::agent::{AgentTypeName, DeployedRegisteredAgentType, ParsedAgentId}; @@ -31,9 +40,10 @@ use golem_common::model::component::{ComponentName, ComponentRevision}; use golem_common::model::environment::EnvironmentId; use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; use golem_common::model::{AgentId, AgentResourceDescription, AgentStatus, Timestamp}; -use serde_derive::{Deserialize, Serialize}; +use itertools::Itertools; +use serde::{Deserialize, Serialize, Serializer}; use std::collections::{BTreeMap, BTreeSet, HashMap}; -use std::fmt::{Display, Formatter}; +use std::fmt::{Display, Formatter, Write}; use std::str::FromStr; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] @@ -340,3 +350,531 @@ impl AgentTypeView { } } } + +impl MessageWithFields for AgentTypeView { + fn message(&self) -> String { + format!( + "Got deployed agent type: {} ", + format_message_highlight(&self.agent_type) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields.field("Agent type", &self.agent_type); + fields.field("Constructor", &self.constructor); + fields.field("Description", &self.description); + + fields.build() + } +} + +impl Masked for AgentTypeView {} + +impl StructuredOutput for AgentTypeView { + const KIND: &'static str = "agent-type.get"; +} + +impl From<&DeployedRegisteredAgentType> for AgentTypeView { + fn from(value: &DeployedRegisteredAgentType) -> Self { + AgentTypeView::new(value, true) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentTypeListView { + pub agent_types: Vec, +} + +impl StructuredOutput for AgentTypeListView { + const KIND: &'static str = "agent-type.list"; +} + +impl TextOutput for AgentTypeListView { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Agent Type").fixed(), + Column::new("Constructor"), + Column::new("Description"), + ]); + for agent_type in &self.agent_types { + let view = AgentTypeView::new(agent_type, true); + table.add_row(vec![view.agent_type, view.constructor, view.description]); + } + log_table(table); + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentCreateView { + pub component_name: ComponentName, + pub agent_id: RawAgentId, +} + +impl Masked for AgentCreateView {} + +impl MessageWithFields for AgentCreateView { + fn message(&self) -> String { + format!( + "Created new agent {}", + format_message_highlight(&self.agent_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields + .fmt_field("Component name", &self.component_name, format_id) + .fmt_field("Agent ID", &self.agent_id, |agent_id| { + format_agent_id_in( + &agent_id.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }); + + fields.build() + } +} + +impl StructuredOutput for AgentCreateView { + const KIND: &'static str = "agent.new"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AgentGetView { + pub metadata: AgentMetadataView, + pub precise: bool, +} + +impl AgentGetView { + pub fn from_metadata(metadata: AgentMetadataView, precise: bool) -> Self { + Self { metadata, precise } + } +} + +impl Masked for AgentGetView { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + self.metadata = self.metadata.masked(config)?; + Ok(self) + } +} + +fn format_untyped_config(config: &[AgentConfigEntryDto]) -> String { + config + .iter() + .map(|entry| { + format!( + "{}={}", + entry.path.join(".").log_color_highlight(), + entry.value.0 + ) + }) + .join("\n") +} + +fn to_sorted_btree_map(map: &HashMap) -> BTreeMap { + map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() +} + +impl MessageWithFields for AgentGetView { + fn message(&self) -> String { + format!( + "Got metadata for agent {}", + format_message_highlight(&self.metadata.agent_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + let mut update_history = String::new(); + for update in &self.metadata.updates { + match update { + UpdateRecord::PendingUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Pending update to {}", + update.timestamp, update.target_revision + ) + .bright_black() + ); + } + UpdateRecord::SuccessfulUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Successful update to {}", + update.timestamp, update.target_revision + ) + .green() + .bold() + ); + } + UpdateRecord::FailedUpdate(update) => { + let _ = writeln!( + update_history, + "{}", + format!( + "{}: Failed update to {}{}", + update.timestamp, + update.target_revision, + update + .details + .as_ref() + .map(|details| format!(": {details}")) + .unwrap_or_default() + ) + .yellow() + ); + } + } + } + + fields + .fmt_field("Component name", &self.metadata.component_name, format_id) + .fmt_field( + "Component revision", + &self.metadata.component_revision, + format_id, + ) + .fmt_field("Agent ID", &self.metadata.agent_id, |agent_id| { + format_agent_id_in( + &agent_id.0, + colored::control::SHOULD_COLORIZE.should_colorize(), + field_value_width::(), + ) + }) + .field("Created at", &self.metadata.created_at) + .fmt_field( + "Component size", + &self.metadata.component_size, + format_binary_size, + ) + .fmt_field( + "Total linear memory size", + &self.metadata.total_linear_memory_size, + format_binary_size, + ) + .fmt_field_optional( + "Environment variables - defaults", + &self.metadata.default_env, + !self.metadata.default_env.is_empty(), + |env| format_env(&to_sorted_btree_map(env)), + ) + .fmt_field_optional( + "Environment variables - overrides", + &self.metadata.env, + !self.metadata.env.is_empty(), + |env| format_env(&to_sorted_btree_map(env)), + ) + .fmt_field_optional( + "Config - defaults", + &self.metadata.default_config, + !self.metadata.default_config.is_empty(), + |config| format_untyped_config(config), + ) + .fmt_field_optional( + "Config - overrides", + &self.metadata.config, + !self.metadata.config.is_empty(), + |config| format_untyped_config(config), + ) + .fmt_field_optional("Status", &self.metadata.status, self.precise, format_status) + .fmt_field_optional( + "Retry count", + &self.metadata.retry_count, + self.precise, + format_retry_count, + ) + .fmt_field_optional( + "Pending invocation count", + &self.metadata.pending_invocation_count, + self.metadata.pending_invocation_count > 0, + |n| n.to_string(), + ) + .fmt_field_optional( + "Last error", + &self.metadata.last_error, + self.metadata.last_error.is_some() && self.precise, + |err| format_stack(err.as_ref().unwrap()), + ) + .fmt_field_optional( + "WARNING", + "The presented agent metadata may not be up-to-date", + !self.precise, + format_warn, + ); + + fields.build() + } +} + +impl StructuredOutput for AgentGetView { + const KIND: &'static str = "agent.get"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl StructuredOutput for AgentsMetadataResponseView { + const KIND: &'static str = "agent.list"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl TextOutput for AgentsMetadataResponseView { + fn log(&self) { + let colorize = colored::control::SHOULD_COLORIZE.should_colorize(); + let term_width = terminal_width(); + logln(Self::format_table_wide( + &self.agents, + term_width, + colorize, + false, + )); + + if !self.cursors.is_empty() { + logln(""); + } + for (component_name, cursor) in &self.cursors { + logln(format!( + "Cursor for more results for component {}: {}", + component_name.log_color_highlight(), + cursor.log_color_highlight() + )); + } + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + self.masked(config)?.log(); + Ok(()) + } +} + +/// Agent-list component-name column: capped at `MAX` so it cannot eat the agent +/// id budget, squeezable to `MIN` when ids need the room. +const MAX_COMPONENT_NAME_WIDTH: usize = 28; +const MIN_COMPONENT_NAME_WIDTH: usize = 12; + +/// Below this the agent id column is left unformatted for the table to wrap. +const MIN_AGENT_NAME_WIDTH: usize = 24; + +impl AgentsMetadataResponseView { + fn status_color(status: &AgentStatus, colorize: bool) -> ComfyColor { + if colorize { + match status { + AgentStatus::Running => ComfyColor::Green, + AgentStatus::Idle => ComfyColor::Cyan, + AgentStatus::Suspended => ComfyColor::Yellow, + AgentStatus::Interrupted => ComfyColor::Red, + AgentStatus::Retrying => ComfyColor::Yellow, + AgentStatus::Failed => ComfyColor::Red, + AgentStatus::Exited => ComfyColor::White, + } + } else { + ComfyColor::Reset + } + } + + fn format_table_wide( + agents: &[AgentMetadataView], + term_width: u16, + colorize: bool, + full_width: bool, + ) -> String { + // Agent ids are self-formatted (broken at their own structure), so the + // column width must be known before the cells are built; + // `self_formatting_table` budgets it. `Range` marks the component name as + // the squeezable column. + let headers = vec![ + Column::new("Component name") + .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), + Column::new("Agent ID"), + Column::new("Revision").content_right(), + Column::new("Status").content_right(), + Column::new("Pending").content_right(), + Column::new("Created at").content(), + ]; + + let format_agent_id = |raw: &str, width: Option| match width { + Some(width) => format_agent_id_in(raw, colorize, width), + None => raw.to_string(), + }; + + let rows = agents + .iter() + .map(|agent| { + vec![ + TableCell::new(agent.component_name.to_string()), + TableCell::new(agent.agent_id.0.clone()), + TableCell::new(agent.component_revision.to_string()).right(), + TableCell::new(agent.status.to_string()) + .right() + .color(Self::status_color(&agent.status, colorize)), + TableCell::new(agent.pending_invocation_count.to_string()).right(), + TableCell::new(agent.created_at.to_string()), + ] + }) + .collect(); + + self_formatting_table(SelfFormattingTableSpec { + preset: TablePreset::FullCondensed, + term_width, + full_width, + headers, + flex: FlexColumn { + index: 1, + min_width: MIN_AGENT_NAME_WIDTH, + format: &format_agent_id, + }, + rows, + }) + .to_string() + } +} + +impl TruncatableTextOutput for AgentsMetadataResponseView { + fn render_truncated(&self, max_lines: usize, colorize: bool) -> String { + let cursor_lines = if self.cursors.is_empty() { + 0 + } else { + 1 + self.cursors.len() + }; + let available_for_table = max_lines.saturating_sub(cursor_lines); + + let term_width = terminal_width(); + let table_str = Self::format_table_wide(&self.agents, term_width, colorize, true); + + let mut out = truncate_rendered(table_str, available_for_table); + + if !self.cursors.is_empty() { + out.push('\n'); + for (component_name, cursor) in &self.cursors { + out.push('\n'); + out.push_str(&format!( + "Cursor for more results for component {}: {}", + component_name.log_color_highlight(), + cursor.log_color_highlight() + )); + } + } + + out + } + + fn render_truncated_masked( + &self, + max_lines: usize, + colorize: bool, + config: MaskingConfig, + ) -> anyhow::Result { + Ok(self + .clone() + .masked(config)? + .render_truncated(max_lines, colorize)) + } +} + +/// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). +fn format_agent_id_in(agent_id: &str, colorize: bool, width: usize) -> String { + crate::agent_id_display::format_agent_id_for_terminal(agent_id, colorize, Some(width)) +} + +// Helper function to convert Unix timestamp to human-readable format +pub fn format_timestamp(timestamp: u64) -> String { + if let Some(datetime) = DateTime::from_timestamp(timestamp as i64, 0) { + datetime.format("%Y-%m-%d %H:%M:%S").to_string() + } else { + format!("{timestamp}") // Fallback to raw timestamp if conversion fails + } +} + +pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { + let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( + agent_id_match.parsed_agent_id.as_ref(), + &agent_id_match.source_language, + &agent_id_match.agent_id.0, + ); + + format!( + "{}{}/{}", + match &agent_id_match.environment_reference() { + Some(environment_reference) => { + match environment_reference { + EnvironmentReference::Environment { environment_name } => { + format!("{}/", environment_name.0.blue().bold()) + } + EnvironmentReference::ApplicationEnvironment { + application_name, + environment_name, + } => { + format!( + "{}/{}/", + application_name.0.blue().bold(), + environment_name.0.blue().bold() + ) + } + EnvironmentReference::AccountApplicationEnvironment { + account_email, + application_name, + environment_name, + } => { + format!( + "{}/{}/{}/", + account_email.blue().bold(), + application_name.0.blue().bold(), + environment_name.0.blue().bold() + ) + } + } + } + None => "".to_string(), + }, + agent_id_match.component_name.0.blue().bold(), + rendered_agent_id.green().bold(), + ) +} + +fn format_status(status: &AgentStatus) -> String { + let status_name = status.to_string(); + match status { + AgentStatus::Running => status_name.green(), + AgentStatus::Idle => status_name.cyan(), + AgentStatus::Suspended => status_name.yellow(), + AgentStatus::Interrupted => status_name.red(), + AgentStatus::Retrying => status_name.yellow(), + AgentStatus::Failed => status_name.bright_red(), + AgentStatus::Exited => status_name.white(), + } + .to_string() +} + +fn format_retry_count(retry_count: &u32) -> String { + if *retry_count == 0 { + retry_count.to_string() + } else { + format_warn(&retry_count.to_string()) + } +} diff --git a/cli/golem-cli/src/model/text/agent/oplog.rs b/cli/golem-cli/src/model/agent/oplog.rs similarity index 100% rename from cli/golem-cli/src/model/text/agent/oplog.rs rename to cli/golem-cli/src/model/agent/oplog.rs diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index a9b81eac57..6652f5ab3e 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -666,7 +666,7 @@ fn agent_list_structured_output_masks_secret_config_paths() { #[test] fn agent_get_structured_output_masks_secret_config_paths() { - let value = hidden_structured_output(crate::model::text::agent::AgentGetView { + let value = hidden_structured_output(crate::model::agent::AgentGetView { metadata: sample_agent_metadata_view(), precise: true, }); @@ -1109,11 +1109,11 @@ fn cli_output_schema_validates_schema_native_component_and_agent_outputs() { components: vec![component], }) .expect("component.list should serialize"), - to_structured_output_value(crate::model::text::agent::AgentTypeListView { + to_structured_output_value(crate::model::agent::AgentTypeListView { agent_types: vec![agent_type], }) .expect("agent-type.list should serialize"), - to_structured_output_value(crate::model::text::agent::oplog::AgentOplogEntryView { + to_structured_output_value(crate::model::agent::oplog::AgentOplogEntryView { index: 0, entry: sample_public_oplog_entries() .into_iter() @@ -2098,7 +2098,7 @@ fn arb_agent_type_get_result() -> OutputDocumentStrategy { fn arb_agent_type_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_deployed_registered_agent_type(), 0..3) - .prop_map(|agent_types| crate::model::text::agent::AgentTypeListView { agent_types }), + .prop_map(|agent_types| crate::model::agent::AgentTypeListView { agent_types }), ) } @@ -2574,11 +2574,11 @@ fn arb_path_segment() -> BoxedStrategy fn arb_agent_files_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_file_node(), 0..6) - .prop_map(|nodes| crate::model::text::agent::files::AgentFilesView { nodes }), + .prop_map(|nodes| crate::model::agent::files::AgentFilesView { nodes }), ) } -fn arb_file_node() -> BoxedStrategy { +fn arb_file_node() -> BoxedStrategy { ( arb_small_string(), arb_small_string(), @@ -2587,7 +2587,7 @@ fn arb_file_node() -> BoxedStrategy BoxedStrategy OutputDocumentStrategy { serialized_output( (arb_agent_metadata_view(), any::()).prop_map(|(metadata, precise)| { - crate::model::text::agent::AgentGetView { metadata, precise } + crate::model::agent::AgentGetView { metadata, precise } }), ) } @@ -2650,7 +2650,7 @@ fn arb_agent_list_result() -> OutputDocumentStrategy { fn arb_agent_new_result() -> OutputDocumentStrategy { serialized_output((arb_small_string(), arb_small_string()).prop_map( - |(component_name, agent_id)| crate::model::text::agent::AgentCreateView { + |(component_name, agent_id)| crate::model::agent::AgentCreateView { component_name: golem_common::model::component::ComponentName(component_name), agent_id: crate::model::agent::RawAgentId(agent_id), }, @@ -2666,9 +2666,9 @@ fn arb_agent_oplog_result() -> OutputDocumentStrategy { arb_typed_value_oplog_entry(), ], ) - .prop_map(|(index, entry)| { - crate::model::text::agent::oplog::AgentOplogEntryView { index, entry } - }), + .prop_map( + |(index, entry)| crate::model::agent::oplog::AgentOplogEntryView { index, entry }, + ), ) } @@ -2813,11 +2813,11 @@ fn arb_agent_update_meta() -> BoxedStrategy BoxedStrategy { +-> BoxedStrategy { arb_agent_transition_fields() .prop_map( |(component_name, agent_id, from_revision, revision, from_version, version)| { - crate::model::text::agent::action_result::AgentRedeploymentMeta { + crate::model::agent::action_result::AgentRedeploymentMeta { component_name, agent_id, from_revision, @@ -2830,11 +2830,11 @@ fn arb_agent_redeployment_meta() .boxed() } -fn arb_agent_deletion_meta() --> BoxedStrategy { +fn arb_agent_deletion_meta() -> BoxedStrategy +{ (arb_small_string(), arb_small_string()) .prop_map(|(component_name, agent_id)| { - crate::model::text::agent::action_result::AgentDeletionMeta { + crate::model::agent::action_result::AgentDeletionMeta { component_name: golem_common::model::component::ComponentName(component_name), agent_id: crate::model::agent::RawAgentId(agent_id), } @@ -3005,7 +3005,7 @@ fn arb_agent_resource_description() -> BoxedStrategy OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, agent)| { - crate::model::text::agent::action_result::AgentDeleteResult { + crate::model::agent::action_result::AgentDeleteResult { deleted, agent_id: agent, } @@ -3023,7 +3023,7 @@ fn arb_agent_file_contents_result() -> OutputDocumentStrategy { arb_small_u64(), ) .prop_map(|(saved, agent, path, output_path, bytes)| { - crate::model::text::agent::action_result::AgentFileContentsResult { + crate::model::agent::action_result::AgentFileContentsResult { saved, agent_id: agent, path, @@ -3037,7 +3037,7 @@ fn arb_agent_file_contents_result() -> OutputDocumentStrategy { fn arb_agent_interrupt_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(interrupted, agent)| { - crate::model::text::agent::action_result::AgentInterruptResult { + crate::model::agent::action_result::AgentInterruptResult { interrupted, agent_id: agent, } @@ -3048,7 +3048,7 @@ fn arb_agent_interrupt_result() -> OutputDocumentStrategy { fn arb_agent_resume_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(resumed, agent)| { - crate::model::text::agent::action_result::AgentResumeResult { + crate::model::agent::action_result::AgentResumeResult { resumed, agent_id: agent, } @@ -3059,7 +3059,7 @@ fn arb_agent_resume_result() -> OutputDocumentStrategy { fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(simulated, agent)| { - crate::model::text::agent::action_result::AgentSimulateCrashResult { + crate::model::agent::action_result::AgentSimulateCrashResult { simulated, agent_id: agent, } @@ -3358,7 +3358,7 @@ fn arb_agent_cancel_invocation_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string(), arb_small_string()).prop_map( |(canceled, agent, idempotency_key)| { - crate::model::text::agent::action_result::AgentCancelInvocationResult { + crate::model::agent::action_result::AgentCancelInvocationResult { canceled, agent_id: agent, idempotency_key, @@ -3375,7 +3375,7 @@ fn arb_agent_delete_all_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_agent_deletion_meta(), 0..5), ) .prop_map(|(deleted, agents)| { - crate::model::text::agent::action_result::AgentDeleteAllResult { deleted, agents } + crate::model::agent::action_result::AgentDeleteAllResult { deleted, agents } }), ) } @@ -3387,7 +3387,7 @@ fn arb_agent_redeploy_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_agent_redeployment_meta(), 0..5), ) .prop_map(|(redeployed, agents)| { - crate::model::text::agent::action_result::AgentRedeployResult { redeployed, agents } + crate::model::agent::action_result::AgentRedeployResult { redeployed, agents } }), ) } @@ -3402,7 +3402,7 @@ fn arb_agent_revert_result() -> OutputDocumentStrategy { ) .prop_map( |(reverted, agent, last_oplog_index, number_of_invocations)| { - crate::model::text::agent::action_result::AgentRevertResult { + crate::model::agent::action_result::AgentRevertResult { reverted, agent_id: agent, last_oplog_index, @@ -3422,7 +3422,7 @@ fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { 0i32..1000, ) .prop_map(|(activated, agent, plugin, priority)| { - crate::model::text::agent::action_result::AgentPluginToggleResult { + crate::model::agent::action_result::AgentPluginToggleResult { activated, agent_id: agent, plugin, diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index b3e69a11ff..221fc5f836 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -16,6 +16,7 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::command::shared_args::{ForceBuildArg, PostDeployArgs}; use crate::error::service::ServiceError; use crate::model::agent::RawAgentId; +use crate::model::cli_output::StructuredOutput; use crate::model::component::{ render_agent_constructor, render_input_schema, render_output_schema, }; @@ -23,6 +24,7 @@ use crate::model::language::GuestLanguage; use crate::model::masking::{ MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, }; +use crate::model::text::fmt::TextOutput; use golem_client::model::{AgentSecretDto, RetryPolicyDto}; use golem_common::model::agent::{ AgentConfigSource, HttpEndpointDetails, HttpMethod, HttpMountDetails, PathSegment, @@ -1300,6 +1302,16 @@ impl TryUpdateAllWorkersResult { } } +impl StructuredOutput for TryUpdateAllWorkersResult { + const KIND: &'static str = "agent.update"; +} + +impl TextOutput for TryUpdateAllWorkersResult { + fn log(&self) { + // NOP + } +} + #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AgentUpdateMeta { diff --git a/cli/golem-cli/src/model/invoke_result_view.rs b/cli/golem-cli/src/model/invoke_result_view.rs index 78f9dff9e7..f92b772370 100644 --- a/cli/golem-cli/src/model/invoke_result_view.rs +++ b/cli/golem-cli/src/model/invoke_result_view.rs @@ -13,13 +13,15 @@ // limitations under the License. use crate::agent_id_display::{SourceLanguage, render_typed_schema_value}; -use crate::log::log_error; +use crate::log::{log_error, logln}; use crate::model::cli_output::StructuredOutput; +use crate::model::text::fmt::{TextOutput, format_message_highlight, format_warn}; use anyhow::anyhow; use golem_client::model::AgentInvocationResult; use golem_common::model::IdempotencyKey; use golem_common::schema::TypedSchemaValue; use golem_common::schema::agent::{AgentTypeSchema, OutputSchema}; +use indoc::indoc; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -40,6 +42,45 @@ impl StructuredOutput for InvokeResultView { const KIND: &'static str = "agent.invoke"; } +impl TextOutput for InvokeResultView { + fn log(&self) { + fn log_result_format(format: Option<&str>, multiple: bool) { + let result_label = if multiple { "results" } else { "result" }; + match format { + Some(format) => logln(format!( + "Invocation {result_label} in {}:", + format_message_highlight(format), + )), + None => logln(format!("Invocation {result_label}:")), + } + } + + if self.is_void_result { + log_result_format(None, false); + logln("void"); + return; + } + + if self.result.is_none() && self.result_json.is_none() { + return; + } + + if let Some(result) = &self.result { + log_result_format(self.result_format.as_deref(), false); + logln(result); + } else if let Some(json) = &self.result_json { + logln(format_warn(indoc!( + " + Failed to convert invocation result to the requested format. + At the moment it does not support Handle (aka Resource) data type. + " + ))); + log_result_format(Some("JSON"), false); + logln(serde_json::to_string_pretty(json).unwrap()); + } + } +} + impl InvokeResultView { pub fn new_agent_invoke( idempotency_key: IdempotencyKey, diff --git a/cli/golem-cli/src/model/text/agent/mod.rs b/cli/golem-cli/src/model/text/agent/mod.rs deleted file mode 100644 index ca005c22ce..0000000000 --- a/cli/golem-cli/src/model/text/agent/mod.rs +++ /dev/null @@ -1,620 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod action_result; -pub mod files; -pub mod oplog; - -use crate::log::{LogColorize, logln}; -use crate::model::agent::{ - AgentIdMatch, AgentMetadataView, AgentsMetadataResponseView, RawAgentId, -}; -use crate::model::cli_output::StructuredOutput; -use crate::model::deploy::TryUpdateAllWorkersResult; -use crate::model::environment::EnvironmentReference; -use crate::model::invoke_result_view::InvokeResultView; -use crate::model::masking::{Masked, MaskingConfig}; -use crate::model::text::fmt::*; -use chrono::DateTime; - -use crate::model::agent::AgentTypeView; -use colored::Colorize; -use comfy_table::Color as ComfyColor; -use golem_common::model::AgentStatus; -use golem_common::model::agent::DeployedRegisteredAgentType; -use golem_common::model::component::ComponentName; -use golem_common::model::worker::{AgentConfigEntryDto, UpdateRecord}; -use indoc::indoc; -use itertools::Itertools; -use serde::Serializer; -use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, HashMap}; -use std::fmt::Write; - -impl MessageWithFields for AgentTypeView { - fn message(&self) -> String { - format!( - "Got deployed agent type: {} ", - format_message_highlight(&self.agent_type) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields.field("Agent type", &self.agent_type); - fields.field("Constructor", &self.constructor); - fields.field("Description", &self.description); - - fields.build() - } -} - -impl Masked for AgentTypeView {} - -impl StructuredOutput for AgentTypeView { - const KIND: &'static str = "agent-type.get"; -} - -impl From<&DeployedRegisteredAgentType> for AgentTypeView { - fn from(value: &DeployedRegisteredAgentType) -> Self { - AgentTypeView::new(value, true) - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentTypeListView { - pub agent_types: Vec, -} - -impl StructuredOutput for AgentTypeListView { - const KIND: &'static str = "agent-type.list"; -} - -impl TextOutput for AgentTypeListView { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Agent Type").fixed(), - Column::new("Constructor"), - Column::new("Description"), - ]); - for agent_type in &self.agent_types { - let view = AgentTypeView::new(agent_type, true); - table.add_row(vec![view.agent_type, view.constructor, view.description]); - } - log_table(table); - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentCreateView { - pub component_name: ComponentName, - pub agent_id: RawAgentId, -} - -impl Masked for AgentCreateView {} - -impl MessageWithFields for AgentCreateView { - fn message(&self) -> String { - format!( - "Created new agent {}", - format_message_highlight(&self.agent_id) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("Component name", &self.component_name, format_id) - .fmt_field("Agent ID", &self.agent_id, |agent_id| { - format_agent_id_in( - &agent_id.0, - colored::control::SHOULD_COLORIZE.should_colorize(), - field_value_width::(), - ) - }); - - fields.build() - } -} - -impl StructuredOutput for AgentCreateView { - const KIND: &'static str = "agent.new"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AgentGetView { - pub metadata: AgentMetadataView, - pub precise: bool, -} - -impl AgentGetView { - pub fn from_metadata(metadata: AgentMetadataView, precise: bool) -> Self { - Self { metadata, precise } - } -} - -impl Masked for AgentGetView { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - self.metadata = self.metadata.masked(config)?; - Ok(self) - } -} - -fn format_untyped_config(config: &[AgentConfigEntryDto]) -> String { - config - .iter() - .map(|entry| { - format!( - "{}={}", - entry.path.join(".").log_color_highlight(), - entry.value.0 - ) - }) - .join("\n") -} - -fn to_sorted_btree_map(map: &HashMap) -> BTreeMap { - map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() -} - -impl MessageWithFields for AgentGetView { - fn message(&self) -> String { - format!( - "Got metadata for agent {}", - format_message_highlight(&self.metadata.agent_id) - ) - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - let mut update_history = String::new(); - for update in &self.metadata.updates { - match update { - UpdateRecord::PendingUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Pending update to {}", - update.timestamp, update.target_revision - ) - .bright_black() - ); - } - UpdateRecord::SuccessfulUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Successful update to {}", - update.timestamp, update.target_revision - ) - .green() - .bold() - ); - } - UpdateRecord::FailedUpdate(update) => { - let _ = writeln!( - update_history, - "{}", - format!( - "{}: Failed update to {}{}", - update.timestamp, - update.target_revision, - update - .details - .as_ref() - .map(|details| format!(": {details}")) - .unwrap_or_default() - ) - .yellow() - ); - } - } - } - - fields - .fmt_field("Component name", &self.metadata.component_name, format_id) - .fmt_field( - "Component revision", - &self.metadata.component_revision, - format_id, - ) - .fmt_field("Agent ID", &self.metadata.agent_id, |agent_id| { - format_agent_id_in( - &agent_id.0, - colored::control::SHOULD_COLORIZE.should_colorize(), - field_value_width::(), - ) - }) - .field("Created at", &self.metadata.created_at) - .fmt_field( - "Component size", - &self.metadata.component_size, - format_binary_size, - ) - .fmt_field( - "Total linear memory size", - &self.metadata.total_linear_memory_size, - format_binary_size, - ) - .fmt_field_optional( - "Environment variables - defaults", - &self.metadata.default_env, - !self.metadata.default_env.is_empty(), - |env| format_env(&to_sorted_btree_map(env)), - ) - .fmt_field_optional( - "Environment variables - overrides", - &self.metadata.env, - !self.metadata.env.is_empty(), - |env| format_env(&to_sorted_btree_map(env)), - ) - .fmt_field_optional( - "Config - defaults", - &self.metadata.default_config, - !self.metadata.default_config.is_empty(), - |config| format_untyped_config(config), - ) - .fmt_field_optional( - "Config - overrides", - &self.metadata.config, - !self.metadata.config.is_empty(), - |config| format_untyped_config(config), - ) - .fmt_field_optional("Status", &self.metadata.status, self.precise, format_status) - .fmt_field_optional( - "Retry count", - &self.metadata.retry_count, - self.precise, - format_retry_count, - ) - .fmt_field_optional( - "Pending invocation count", - &self.metadata.pending_invocation_count, - self.metadata.pending_invocation_count > 0, - |n| n.to_string(), - ) - .fmt_field_optional( - "Last error", - &self.metadata.last_error, - self.metadata.last_error.is_some() && self.precise, - |err| format_stack(err.as_ref().unwrap()), - ) - .fmt_field_optional( - "WARNING", - "The presented agent metadata may not be up-to-date", - !self.precise, - format_warn, - ); - - fields.build() - } -} - -impl StructuredOutput for AgentGetView { - const KIND: &'static str = "agent.get"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl StructuredOutput for AgentsMetadataResponseView { - const KIND: &'static str = "agent.list"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl StructuredOutput for TryUpdateAllWorkersResult { - const KIND: &'static str = "agent.update"; -} - -impl TextOutput for AgentsMetadataResponseView { - fn log(&self) { - let colorize = colored::control::SHOULD_COLORIZE.should_colorize(); - let term_width = terminal_width(); - logln(Self::format_table_wide( - &self.agents, - term_width, - colorize, - false, - )); - - if !self.cursors.is_empty() { - logln(""); - } - for (component_name, cursor) in &self.cursors { - logln(format!( - "Cursor for more results for component {}: {}", - component_name.log_color_highlight(), - cursor.log_color_highlight() - )); - } - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - self.masked(config)?.log(); - Ok(()) - } -} - -/// Agent-list component-name column: capped at `MAX` so it cannot eat the agent -/// id budget, squeezable to `MIN` when ids need the room. -const MAX_COMPONENT_NAME_WIDTH: usize = 28; -const MIN_COMPONENT_NAME_WIDTH: usize = 12; - -/// Below this the agent id column is left unformatted for the table to wrap. -const MIN_AGENT_NAME_WIDTH: usize = 24; - -impl AgentsMetadataResponseView { - fn status_color(status: &AgentStatus, colorize: bool) -> ComfyColor { - if colorize { - match status { - AgentStatus::Running => ComfyColor::Green, - AgentStatus::Idle => ComfyColor::Cyan, - AgentStatus::Suspended => ComfyColor::Yellow, - AgentStatus::Interrupted => ComfyColor::Red, - AgentStatus::Retrying => ComfyColor::Yellow, - AgentStatus::Failed => ComfyColor::Red, - AgentStatus::Exited => ComfyColor::White, - } - } else { - ComfyColor::Reset - } - } - - fn format_table_wide( - agents: &[AgentMetadataView], - term_width: u16, - colorize: bool, - full_width: bool, - ) -> String { - // Agent ids are self-formatted (broken at their own structure), so the - // column width must be known before the cells are built; - // `self_formatting_table` budgets it. `Range` marks the component name as - // the squeezable column. - let headers = vec![ - Column::new("Component name") - .width_range(MIN_COMPONENT_NAME_WIDTH, MAX_COMPONENT_NAME_WIDTH), - Column::new("Agent ID"), - Column::new("Revision").content_right(), - Column::new("Status").content_right(), - Column::new("Pending").content_right(), - Column::new("Created at").content(), - ]; - - let format_agent_id = |raw: &str, width: Option| match width { - Some(width) => format_agent_id_in(raw, colorize, width), - None => raw.to_string(), - }; - - let rows = agents - .iter() - .map(|agent| { - vec![ - TableCell::new(agent.component_name.to_string()), - TableCell::new(agent.agent_id.0.clone()), - TableCell::new(agent.component_revision.to_string()).right(), - TableCell::new(agent.status.to_string()) - .right() - .color(Self::status_color(&agent.status, colorize)), - TableCell::new(agent.pending_invocation_count.to_string()).right(), - TableCell::new(agent.created_at.to_string()), - ] - }) - .collect(); - - self_formatting_table(SelfFormattingTableSpec { - preset: TablePreset::FullCondensed, - term_width, - full_width, - headers, - flex: FlexColumn { - index: 1, - min_width: MIN_AGENT_NAME_WIDTH, - format: &format_agent_id, - }, - rows, - }) - .to_string() - } -} - -impl TruncatableTextOutput for AgentsMetadataResponseView { - fn render_truncated(&self, max_lines: usize, colorize: bool) -> String { - let cursor_lines = if self.cursors.is_empty() { - 0 - } else { - 1 + self.cursors.len() - }; - let available_for_table = max_lines.saturating_sub(cursor_lines); - - let term_width = terminal_width(); - let table_str = Self::format_table_wide(&self.agents, term_width, colorize, true); - - let mut out = truncate_rendered(table_str, available_for_table); - - if !self.cursors.is_empty() { - out.push('\n'); - for (component_name, cursor) in &self.cursors { - out.push('\n'); - out.push_str(&format!( - "Cursor for more results for component {}: {}", - component_name.log_color_highlight(), - cursor.log_color_highlight() - )); - } - } - - out - } - - fn render_truncated_masked( - &self, - max_lines: usize, - colorize: bool, - config: MaskingConfig, - ) -> anyhow::Result { - Ok(self - .clone() - .masked(config)? - .render_truncated(max_lines, colorize)) - } -} - -impl TextOutput for TryUpdateAllWorkersResult { - fn log(&self) { - // NOP - } -} - -impl TextOutput for InvokeResultView { - fn log(&self) { - fn log_result_format(format: Option<&str>, multiple: bool) { - let result_label = if multiple { "results" } else { "result" }; - match format { - Some(format) => logln(format!( - "Invocation {result_label} in {}:", - format_message_highlight(format), - )), - None => logln(format!("Invocation {result_label}:")), - } - } - - if self.is_void_result { - log_result_format(None, false); - logln("void"); - return; - } - - if self.result.is_none() && self.result_json.is_none() { - return; - } - - if let Some(result) = &self.result { - log_result_format(self.result_format.as_deref(), false); - logln(result); - } else if let Some(json) = &self.result_json { - logln(format_warn(indoc!( - " - Failed to convert invocation result to the requested format. - At the moment it does not support Handle (aka Resource) data type. - " - ))); - log_result_format(Some("JSON"), false); - logln(serde_json::to_string_pretty(json).unwrap()); - } - } -} - -/// Formats an agent id to a caller-supplied width (see `format_agent_id_for_terminal`). -fn format_agent_id_in(agent_id: &str, colorize: bool, width: usize) -> String { - crate::agent_id_display::format_agent_id_for_terminal(agent_id, colorize, Some(width)) -} - -// Helper function to convert Unix timestamp to human-readable format -pub fn format_timestamp(timestamp: u64) -> String { - if let Some(datetime) = DateTime::from_timestamp(timestamp as i64, 0) { - datetime.format("%Y-%m-%d %H:%M:%S").to_string() - } else { - format!("{timestamp}") // Fallback to raw timestamp if conversion fails - } -} - -pub fn format_agent_id_match(agent_id_match: &AgentIdMatch) -> String { - let rendered_agent_id = crate::agent_id_display::render_agent_id_or_raw( - agent_id_match.parsed_agent_id.as_ref(), - &agent_id_match.source_language, - &agent_id_match.agent_id.0, - ); - - format!( - "{}{}/{}", - match &agent_id_match.environment_reference() { - Some(environment_reference) => { - match environment_reference { - EnvironmentReference::Environment { environment_name } => { - format!("{}/", environment_name.0.blue().bold()) - } - EnvironmentReference::ApplicationEnvironment { - application_name, - environment_name, - } => { - format!( - "{}/{}/", - application_name.0.blue().bold(), - environment_name.0.blue().bold() - ) - } - EnvironmentReference::AccountApplicationEnvironment { - account_email, - application_name, - environment_name, - } => { - format!( - "{}/{}/{}/", - account_email.blue().bold(), - application_name.0.blue().bold(), - environment_name.0.blue().bold() - ) - } - } - } - None => "".to_string(), - }, - agent_id_match.component_name.0.blue().bold(), - rendered_agent_id.green().bold(), - ) -} - -fn format_status(status: &AgentStatus) -> String { - let status_name = status.to_string(); - match status { - AgentStatus::Running => status_name.green(), - AgentStatus::Idle => status_name.cyan(), - AgentStatus::Suspended => status_name.yellow(), - AgentStatus::Interrupted => status_name.red(), - AgentStatus::Retrying => status_name.yellow(), - AgentStatus::Failed => status_name.bright_red(), - AgentStatus::Exited => status_name.white(), - } - .to_string() -} - -fn format_retry_count(retry_count: &u32) -> String { - if *retry_count == 0 { - retry_count.to_string() - } else { - format_warn(&retry_count.to_string()) - } -} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 809db31026..c13321ed07 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -13,7 +13,6 @@ // limitations under the License. pub mod action_result; -pub mod agent; pub mod deployment; pub mod diff; pub mod fmt; From 091389882520af1dffecdfb0969fe006541a2092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:21:51 +0200 Subject: [PATCH 53/70] promote http_api to a directory and move its views in --- .../src/command_handler/api/deployment.rs | 4 +--- .../src/command_handler/api/domain.rs | 2 +- .../command_handler/api/security_scheme.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 24 +++++++++---------- .../deployment.rs} | 0 .../http_api_domain.rs => http_api/domain.rs} | 0 .../model/{http_api.rs => http_api/mod.rs} | 4 ++++ .../security.rs} | 0 cli/golem-cli/src/model/text/mod.rs | 3 --- 9 files changed, 18 insertions(+), 21 deletions(-) rename cli/golem-cli/src/model/{text/http_api_deployment.rs => http_api/deployment.rs} (100%) rename cli/golem-cli/src/model/{text/http_api_domain.rs => http_api/domain.rs} (100%) rename cli/golem-cli/src/model/{http_api.rs => http_api/mod.rs} (96%) rename cli/golem-cli/src/model/{text/http_api_security.rs => http_api/security.rs} (100%) diff --git a/cli/golem-cli/src/command_handler/api/deployment.rs b/cli/golem-cli/src/command_handler/api/deployment.rs index b3d815c75a..a967da0eb4 100644 --- a/cli/golem-cli/src/command_handler/api/deployment.rs +++ b/cli/golem-cli/src/command_handler/api/deployment.rs @@ -18,10 +18,8 @@ use crate::context::Context; use crate::error::service::{MapServiceError, ServiceError}; use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; +use crate::model::http_api::deployment::{HttpApiDeploymentGetView, HttpApiDeploymentListView}; use crate::model::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProperties}; -use crate::model::text::http_api_deployment::{ - HttpApiDeploymentGetView, HttpApiDeploymentListView, -}; use anyhow::{anyhow, bail}; use golem_client::api::{ApiDeploymentClient, McpDeploymentClient}; use golem_common::cache::SimpleCache; diff --git a/cli/golem-cli/src/command_handler/api/domain.rs b/cli/golem-cli/src/command_handler/api/domain.rs index 3470069220..2a6d2394f3 100644 --- a/cli/golem-cli/src/command_handler/api/domain.rs +++ b/cli/golem-cli/src/command_handler/api/domain.rs @@ -15,7 +15,7 @@ use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; -use crate::model::text::http_api_domain::{ +use crate::model::http_api::domain::{ DomainRegistrationDeleteResult, DomainRegistrationNewView, HttpApiDomainListView, }; diff --git a/cli/golem-cli/src/command_handler/api/security_scheme.rs b/cli/golem-cli/src/command_handler/api/security_scheme.rs index 3502d6467c..200d02c173 100644 --- a/cli/golem-cli/src/command_handler/api/security_scheme.rs +++ b/cli/golem-cli/src/command_handler/api/security_scheme.rs @@ -19,7 +19,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_error; use crate::model::environment::EnvironmentResolveMode; -use crate::model::text::http_api_security::{ +use crate::model::http_api::security::{ HttpSecuritySchemeCreateView, HttpSecuritySchemeDeleteView, HttpSecuritySchemeGetView, HttpSecuritySchemeListView, HttpSecuritySchemeUpdateView, }; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 6652f5ab3e..a176d5a37f 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3493,7 +3493,7 @@ fn arb_api_domain_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string(), arb_small_string()).prop_map( |(deleted, domain, id)| { - crate::model::text::http_api_domain::DomainRegistrationDeleteResult { + crate::model::http_api::domain::DomainRegistrationDeleteResult { deleted, domain: golem_common::model::domain_registration::Domain(domain), id: golem_common::model::domain_registration::DomainRegistrationId( @@ -3508,15 +3508,14 @@ fn arb_api_domain_delete_result() -> OutputDocumentStrategy { fn arb_api_domain_register_result() -> OutputDocumentStrategy { serialized_output( arb_domain_registration() - .prop_map(crate::model::text::http_api_domain::DomainRegistrationNewView), + .prop_map(crate::model::http_api::domain::DomainRegistrationNewView), ) } fn arb_api_domain_list_result() -> OutputDocumentStrategy { serialized_output( - proptest::collection::vec(arb_domain_registration(), 0..5).prop_map(|domains| { - crate::model::text::http_api_domain::HttpApiDomainListView { domains } - }), + proptest::collection::vec(arb_domain_registration(), 0..5) + .prop_map(|domains| crate::model::http_api::domain::HttpApiDomainListView { domains }), ) } @@ -3536,14 +3535,14 @@ fn arb_domain_registration() fn arb_api_deployment_get_result() -> OutputDocumentStrategy { serialized_output( arb_http_api_deployment() - .prop_map(crate::model::text::http_api_deployment::HttpApiDeploymentGetView), + .prop_map(crate::model::http_api::deployment::HttpApiDeploymentGetView), ) } fn arb_api_deployment_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_http_api_deployment(), 0..5).prop_map(|deployments| { - crate::model::text::http_api_deployment::HttpApiDeploymentListView { deployments } + crate::model::http_api::deployment::HttpApiDeploymentListView { deployments } }), ) } @@ -3627,35 +3626,34 @@ fn arb_http_api_deployment_agent_options() fn arb_api_security_scheme_create_result() -> OutputDocumentStrategy { serialized_output( arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeCreateView), + .prop_map(crate::model::http_api::security::HttpSecuritySchemeCreateView), ) } fn arb_api_security_scheme_delete_result() -> OutputDocumentStrategy { serialized_output( arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeDeleteView), + .prop_map(crate::model::http_api::security::HttpSecuritySchemeDeleteView), ) } fn arb_api_security_scheme_get_result() -> OutputDocumentStrategy { serialized_output( - arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeGetView), + arb_security_scheme().prop_map(crate::model::http_api::security::HttpSecuritySchemeGetView), ) } fn arb_api_security_scheme_update_result() -> OutputDocumentStrategy { serialized_output( arb_security_scheme() - .prop_map(crate::model::text::http_api_security::HttpSecuritySchemeUpdateView), + .prop_map(crate::model::http_api::security::HttpSecuritySchemeUpdateView), ) } fn arb_api_security_scheme_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_security_scheme(), 0..5).prop_map(|security_schemes| { - crate::model::text::http_api_security::HttpSecuritySchemeListView { security_schemes } + crate::model::http_api::security::HttpSecuritySchemeListView { security_schemes } }), ) } diff --git a/cli/golem-cli/src/model/text/http_api_deployment.rs b/cli/golem-cli/src/model/http_api/deployment.rs similarity index 100% rename from cli/golem-cli/src/model/text/http_api_deployment.rs rename to cli/golem-cli/src/model/http_api/deployment.rs diff --git a/cli/golem-cli/src/model/text/http_api_domain.rs b/cli/golem-cli/src/model/http_api/domain.rs similarity index 100% rename from cli/golem-cli/src/model/text/http_api_domain.rs rename to cli/golem-cli/src/model/http_api/domain.rs diff --git a/cli/golem-cli/src/model/http_api.rs b/cli/golem-cli/src/model/http_api/mod.rs similarity index 96% rename from cli/golem-cli/src/model/http_api.rs rename to cli/golem-cli/src/model/http_api/mod.rs index 632d15f534..1dba98efd6 100644 --- a/cli/golem-cli/src/model/http_api.rs +++ b/cli/golem-cli/src/model/http_api/mod.rs @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod deployment; +pub mod domain; +pub mod security; + use golem_common::model::agent::AgentTypeName; use golem_common::model::http_api_deployment::HttpApiDeploymentAgentOptions; use std::collections::BTreeMap; diff --git a/cli/golem-cli/src/model/text/http_api_security.rs b/cli/golem-cli/src/model/http_api/security.rs similarity index 100% rename from cli/golem-cli/src/model/text/http_api_security.rs rename to cli/golem-cli/src/model/http_api/security.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index c13321ed07..a317b5a9db 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -16,9 +16,6 @@ pub mod action_result; pub mod deployment; pub mod diff; pub mod fmt; -pub mod http_api_deployment; -pub mod http_api_domain; -pub mod http_api_security; pub mod profile; pub mod server; pub mod template; From 2429770c111e414b909f58fca306e20a81394270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:23:16 +0200 Subject: [PATCH 54/70] move profile views into model/config/profile --- cli/golem-cli/src/command_handler/profile/config.rs | 2 +- cli/golem-cli/src/command_handler/profile/mod.rs | 4 ++-- cli/golem-cli/src/model/cli_output/tests.rs | 10 +++++----- cli/golem-cli/src/model/config/mod.rs | 2 ++ cli/golem-cli/src/model/{text => config}/profile.rs | 0 cli/golem-cli/src/model/text/mod.rs | 1 - 6 files changed, 10 insertions(+), 9 deletions(-) rename cli/golem-cli/src/model/{text => config}/profile.rs (100%) diff --git a/cli/golem-cli/src/command_handler/profile/config.rs b/cli/golem-cli/src/command_handler/profile/config.rs index a302d774da..be09bad662 100644 --- a/cli/golem-cli/src/command_handler/profile/config.rs +++ b/cli/golem-cli/src/command_handler/profile/config.rs @@ -20,10 +20,10 @@ use crate::error::NonSuccessfulExit; use crate::log::log_error; use crate::log::logln; use crate::log::log_action; +use crate::model::config::profile::ProfileConfigSetFormatResult; use crate::model::format::Format; use crate::model::help::AvailableProfileNamesHelp; use crate::model::text::fmt::log_text_view; -use crate::model::text::profile::ProfileConfigSetFormatResult; use anyhow::bail; use std::sync::Arc; diff --git a/cli/golem-cli/src/command_handler/profile/mod.rs b/cli/golem-cli/src/command_handler/profile/mod.rs index 9b850a36f4..67383ca075 100644 --- a/cli/golem-cli/src/command_handler/profile/mod.rs +++ b/cli/golem-cli/src/command_handler/profile/mod.rs @@ -24,10 +24,10 @@ use crate::error::NonSuccessfulExit; use crate::log::log_error; use crate::log::{LogColorize, log_action, log_warn_action}; use crate::model::config::ProfileView; -use crate::model::format::Format; -use crate::model::text::profile::{ +use crate::model::config::profile::{ ProfileCreateResult, ProfileDeleteResult, ProfileListView, ProfileSwitchResult, }; +use crate::model::format::Format; use anyhow::bail; use std::collections::BTreeMap; use std::sync::Arc; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index a176d5a37f..05a84719a9 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -4880,7 +4880,7 @@ fn arb_plugin_registration() fn arb_profile_create_result() -> OutputDocumentStrategy { serialized_output((any::(), arb_small_string(), any::()).prop_map( - |(created, profile, set_active)| crate::model::text::profile::ProfileCreateResult { + |(created, profile, set_active)| crate::model::config::profile::ProfileCreateResult { created, profile: crate::config::ProfileName(profile), set_active, @@ -4895,7 +4895,7 @@ fn arb_profile_get_result() -> OutputDocumentStrategy { fn arb_profile_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_profile_view(), 0..5) - .prop_map(|profiles| crate::model::text::profile::ProfileListView { profiles }), + .prop_map(|profiles| crate::model::config::profile::ProfileListView { profiles }), ) } @@ -4933,7 +4933,7 @@ fn arb_profile_view() -> BoxedStrategy { fn arb_profile_switch_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(switched, profile)| { - crate::model::text::profile::ProfileSwitchResult { + crate::model::config::profile::ProfileSwitchResult { switched, profile: crate::config::ProfileName(profile), } @@ -4944,7 +4944,7 @@ fn arb_profile_switch_result() -> OutputDocumentStrategy { fn arb_profile_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, profile)| { - crate::model::text::profile::ProfileDeleteResult { + crate::model::config::profile::ProfileDeleteResult { deleted, profile: crate::config::ProfileName(profile), } @@ -4954,7 +4954,7 @@ fn arb_profile_delete_result() -> OutputDocumentStrategy { fn arb_profile_config_set_format_result() -> OutputDocumentStrategy { serialized_output((any::(), arb_small_string(), arb_format()).prop_map( - |(updated, profile, format)| crate::model::text::profile::ProfileConfigSetFormatResult { + |(updated, profile, format)| crate::model::config::profile::ProfileConfigSetFormatResult { updated, profile: crate::config::ProfileName(profile), format, diff --git a/cli/golem-cli/src/model/config/mod.rs b/cli/golem-cli/src/model/config/mod.rs index e438c07181..7c53b24e07 100644 --- a/cli/golem-cli/src/model/config/mod.rs +++ b/cli/golem-cli/src/model/config/mod.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +pub mod profile; + use crate::config::{AuthenticationConfig, NamedProfile, ProfileConfig, ProfileName}; use serde::{Deserialize, Serialize}; use url::Url; diff --git a/cli/golem-cli/src/model/text/profile.rs b/cli/golem-cli/src/model/config/profile.rs similarity index 100% rename from cli/golem-cli/src/model/text/profile.rs rename to cli/golem-cli/src/model/config/profile.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index a317b5a9db..68e45ec513 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -16,6 +16,5 @@ pub mod action_result; pub mod deployment; pub mod diff; pub mod fmt; -pub mod profile; pub mod server; pub mod template; From 0630143d96f99663087bdbee80a0cc863a326c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:23:59 +0200 Subject: [PATCH 55/70] move server context trait into model/config/server --- cli/golem-cli/src/app/context.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/context.rs | 2 +- cli/golem-cli/src/model/config/mod.rs | 1 + cli/golem-cli/src/model/{text => config}/server.rs | 0 cli/golem-cli/src/model/text/mod.rs | 1 - 6 files changed, 4 insertions(+), 4 deletions(-) rename cli/golem-cli/src/model/{text => config}/server.rs (100%) diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index f97693c5ae..9bb6763daa 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -31,11 +31,11 @@ use crate::model::app::{ }; use crate::model::app_raw; use crate::model::component::format_component_applied_layers; +use crate::model::config::server::ToFormattedServerContext; use crate::model::format::Format; use crate::model::language::GuestLanguage; use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::DecoratedIndent; -use crate::model::text::server::ToFormattedServerContext; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 71fd2e7a05..d081c7aac5 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -48,6 +48,7 @@ use crate::model::app::{ AppBuildStep, ApplicationComponentSelectMode, BuildConfig, CleanMode, DynamicHelpSections, WithSource, }; +use crate::model::config::server::ToFormattedServerContext; use crate::model::config::{collect_unused_leaf_paths, value_at_path}; use crate::model::deploy::{ DeployConfig, DeployError, DeployResult, DeploySummary, EnvironmentSetupPlan, PostDeployError, @@ -60,7 +61,6 @@ use crate::model::language::GuestLanguage; use crate::model::text::deployment::{DeploymentListView, DeploymentNewView}; use crate::model::text::diff::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; -use crate::model::text::server::ToFormattedServerContext; use crate::model::text::template::TemplateListView; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/context.rs b/cli/golem-cli/src/context.rs index 9d08a389d7..47aeafd4ec 100644 --- a/cli/golem-cli/src/context.rs +++ b/cli/golem-cli/src/context.rs @@ -33,12 +33,12 @@ use crate::model::app_raw::{ AppVersionSource, BuiltinServer, CustomServerAuth, DeploymentOptions, Environment, Marker, Server, }; +use crate::model::config::server::ToFormattedServerContext; use crate::model::environment::{EnvironmentReference, SelectedManifestEnvironment}; use crate::model::format::Format; use crate::model::masking::MaskingConfig; use crate::model::plugin::PluginNameAndVersion; use crate::model::repl::ReplLanguage; -use crate::model::text::server::ToFormattedServerContext; use anyhow::{anyhow, bail}; use colored::control::SHOULD_COLORIZE; use golem_client::model::EnvironmentPluginGrantWithDetails; diff --git a/cli/golem-cli/src/model/config/mod.rs b/cli/golem-cli/src/model/config/mod.rs index 7c53b24e07..b761c93cbe 100644 --- a/cli/golem-cli/src/model/config/mod.rs +++ b/cli/golem-cli/src/model/config/mod.rs @@ -13,6 +13,7 @@ // limitations under the License. pub mod profile; +pub mod server; use crate::config::{AuthenticationConfig, NamedProfile, ProfileConfig, ProfileName}; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/text/server.rs b/cli/golem-cli/src/model/config/server.rs similarity index 100% rename from cli/golem-cli/src/model/text/server.rs rename to cli/golem-cli/src/model/config/server.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 68e45ec513..a66f42f375 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -16,5 +16,4 @@ pub mod action_result; pub mod deployment; pub mod diff; pub mod fmt; -pub mod server; pub mod template; From 307666093d25bfedbb90075f44ef5a400573a26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:26:40 +0200 Subject: [PATCH 56/70] move deployment and diff views into model/deploy --- cli/golem-cli/src/app/context.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 4 +- .../src/command_handler/app/template.rs | 2 +- .../src/command_handler/environment.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 11 +- cli/golem-cli/src/model/deploy.rs | 789 +++++++++++++++++- cli/golem-cli/src/model/text/deployment.rs | 104 --- cli/golem-cli/src/model/text/diff.rs | 712 ---------------- cli/golem-cli/src/model/text/mod.rs | 2 - 9 files changed, 794 insertions(+), 834 deletions(-) delete mode 100644 cli/golem-cli/src/model/text/deployment.rs delete mode 100644 cli/golem-cli/src/model/text/diff.rs diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 9bb6763daa..2f423a1ee7 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -32,9 +32,9 @@ use crate::model::app::{ use crate::model::app_raw; use crate::model::component::format_component_applied_layers; use crate::model::config::server::ToFormattedServerContext; +use crate::model::deploy::log_unified_diff_for_path; use crate::model::format::Format; use crate::model::language::GuestLanguage; -use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::DecoratedIndent; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index d081c7aac5..8c1baf2630 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -55,11 +55,11 @@ use crate::model::deploy::{ PostDeployResult, PostDeploySummary, UpdateStagedComponentError, build_environment_setup_plan, preferred_source_language_for_setup, }; +use crate::model::deploy::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; +use crate::model::deploy::{DeploymentListView, DeploymentNewView}; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; -use crate::model::text::deployment::{DeploymentListView, DeploymentNewView}; -use crate::model::text::diff::{DeployPlanView, log_unified_diff, log_unified_diff_for_path}; use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; use crate::model::text::template::TemplateListView; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index eeef685fed..abc57e834c 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -33,9 +33,9 @@ use crate::log::{ LogColorize, LogIndent, log_action, log_anyhow_error, log_error, log_failed_to, log_finished_ok, log_skipping_up_to_date, logln, }; +use crate::model::deploy::log_unified_diff_for_path; use crate::model::help::{AppNewNextStepsHint, AppNewNextStepsMode}; use crate::model::language::GuestLanguage; -use crate::model::text::diff::log_unified_diff_for_path; use crate::model::text::fmt::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{anyhow, bail}; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index 00b4d401ab..1e6342d034 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -22,13 +22,13 @@ use crate::error::service::MapServiceError; use crate::log::{ LogColorize, LogIndent, log_action, log_error, log_skipping_up_to_date, log_warn_action, logln, }; +use crate::model::deploy::log_unified_diff; use crate::model::environment::{EnvironmentListView, EnvironmentSyncDeploymentOptionsResult}; use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, }; use crate::model::help::EnvironmentNameHelp; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::diff::log_unified_diff; use crate::model::text::fmt::log_text_view; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 05a84719a9..937aa86659 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -2,8 +2,8 @@ use crate::model::cli_output::{ CLI_OUTPUT_TYPE_FIELD, StructuredOutput, command_output_type_names, focused_command_output_schema, to_structured_output_value, to_structured_output_value_masked, }; +use crate::model::deploy::DeployPlanView; use crate::model::masking::MaskingConfig; -use crate::model::text::diff::DeployPlanView; use golem_common::model::card::{CardId, PolymorphicCard}; use proptest::prelude::*; use quote::ToTokens; @@ -4492,7 +4492,7 @@ fn arb_deployment_diff() -> BoxedStrategy OutputDocumentStrategy { arb_environment_setup_plan() .prop_map(|output| { - to_structured_output_value(crate::model::text::diff::EnvironmentSetupPlanView(&output)) + to_structured_output_value(crate::model::deploy::EnvironmentSetupPlanView(&output)) .expect("generated environment setup plan should serialize") }) .boxed() @@ -4590,7 +4590,7 @@ fn arb_deployment_create_result() -> OutputDocumentStrategy { arb_current_deployment(), ) .prop_map(|(application_name, environment_name, deployment)| { - crate::model::text::deployment::DeploymentNewView { + crate::model::deploy::DeploymentNewView { application_name: golem_common::model::application::ApplicationName( application_name, ), @@ -4605,9 +4605,8 @@ fn arb_deployment_create_result() -> OutputDocumentStrategy { fn arb_deployment_list_result() -> OutputDocumentStrategy { serialized_output( - proptest::collection::vec(arb_deployment(), 0..5).prop_map(|deployments| { - crate::model::text::deployment::DeploymentListView { deployments } - }), + proptest::collection::vec(arb_deployment(), 0..5) + .prop_map(|deployments| crate::model::deploy::DeploymentListView { deployments }), ) } diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index 221fc5f836..eaa2325495 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -15,6 +15,7 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::command::shared_args::{ForceBuildArg, PostDeployArgs}; use crate::error::service::ServiceError; +use crate::log::{LogColorize, logln}; use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; use crate::model::component::{ @@ -22,24 +23,38 @@ use crate::model::component::{ }; use crate::model::language::GuestLanguage; use crate::model::masking::{ - MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, + Masked, MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, + mask_secret_with_fingerprint, }; -use crate::model::text::fmt::TextOutput; -use golem_client::model::{AgentSecretDto, RetryPolicyDto}; +use crate::model::text::fmt::{ + Column, FieldsBuilder, MessageWithFields, TextOutput, format_id, format_main_id, log_table, + new_table_full_condensed, +}; +use colored::Colorize; +use golem_client::model::{AgentSecretDto, Deployment, RetryPolicyDto}; +use golem_common::base_model::json::NormalizedJsonValue; use golem_common::model::agent::{ AgentConfigSource, HttpEndpointDetails, HttpMethod, HttpMountDetails, PathSegment, }; use golem_common::model::agent_secret::CanonicalAgentSecretPath; +use golem_common::model::application::ApplicationName; use golem_common::model::card::PolymorphicPermissionPattern; use golem_common::model::component::{AgentFilePermissions, ComponentName, ComponentRevision}; -use golem_common::model::deployment::{DeploymentAgentSecretDefault, DeploymentRetryPolicyDefault}; -use golem_common::model::diff::{self, Hashable}; +use golem_common::model::deployment::{ + CurrentDeployment, DeploymentAgentSecretDefault, DeploymentRetryPolicyDefault, +}; +use golem_common::model::diff::{ + self, AgentTypeProvisionConfigDiff, BTreeMapDiffValue, DeploymentDiff, DiffForHashOf, Hashable, +}; +use golem_common::model::environment::EnvironmentName; use golem_common::model::quota::{ResourceDefinition, ResourceDefinitionCreation}; use golem_common::schema::agent::{AgentMethodSchema, AgentTypeSchema}; use golem_common::schema::graph::SchemaGraph; use itertools::Itertools; +use serde::ser::Serializer; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::Path; use thiserror::Error; #[derive(Clone, Debug, Default, Serialize)] @@ -1413,3 +1428,767 @@ fn render_schema_type_for_language( }; render_type_for_language(source_language, &graph, &graph.root, true) } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeploymentNewView { + pub application_name: ApplicationName, + pub environment_name: EnvironmentName, + pub deployment: CurrentDeployment, +} + +impl Masked for DeploymentNewView {} + +impl MessageWithFields for DeploymentNewView { + fn message(&self) -> String { + "Created new deployment".to_owned() + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + + fields + .fmt_field("Application", &self.application_name.0, format_id) + .fmt_field("Environment", &self.environment_name.0, format_id) + .fmt_field( + "Environment ID", + &self.deployment.environment_id, + format_main_id, + ) + .fmt_field( + "Deployment Revision", + &self.deployment.revision, + format_main_id, + ); + + fields.fmt_field_optional( + "Deployment Version", + &self.deployment.version.0, + !self.deployment.version.0.is_empty(), + format_id, + ); + + fields + .fmt_field("Hash", &self.deployment.deployment_hash, format_id) + .field("Deploy Revision", &self.deployment.current_revision); + + fields.build() + } +} + +impl StructuredOutput for DeploymentNewView { + const KIND: &'static str = "deploy.deployment"; +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeploymentListView { + pub deployments: Vec, +} + +impl StructuredOutput for DeploymentListView { + const KIND: &'static str = "deploy.deployments"; +} + +impl TextOutput for DeploymentListView { + fn log(&self) { + let mut table = new_table_full_condensed(vec![ + Column::new("Deployment Revision").fixed_right(), + Column::new("Deployment Version").fixed_right(), + Column::new("Hash"), + ]); + for dep in &self.deployments { + table.add_row(vec![ + dep.revision.get().to_string(), + dep.version.0.clone(), + dep.deployment_hash.to_string(), + ]); + } + log_table(table); + } +} + +const DIFF_COLLAPSE_THRESHOLD: usize = 12; +const DIFF_COLLAPSE_KEEP_HEAD: usize = 3; +const DIFF_COLLAPSE_KEEP_TAIL: usize = 3; +const DIFF_COLLAPSE_DOTS: usize = 3; + +impl TextOutput for DeploymentDiff { + fn log(&self) { + logln(""); + if !self.components.is_empty() { + logln("Component changes:".log_color_help_group().to_string()); + for (component_name, component_diff) in &self.components { + match component_diff { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} component {}", + "create".green(), + component_name.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} component {}", + "delete".red(), + component_name.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(diff) => match diff { + DiffForHashOf::HashDiff { .. } => { + logln(format!( + " - {} component {}", + "update".yellow(), + component_name.log_color_highlight() + )); + } + DiffForHashOf::ValueDiff { diff } => { + logln(format!( + " - {} component {}, changes:", + "update".yellow(), + component_name.log_color_highlight() + )); + if diff.wasm_changed { + logln(" - binary"); + } + if !diff.agent_type_provision_config_changes.is_empty() { + logln(" - provision configs"); + for (agent_type, change) in + &diff.agent_type_provision_config_changes + { + match change { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} agent type {}", + "create".green(), + agent_type.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} agent type {}", + "delete".red(), + agent_type.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(inner) => { + logln(format!( + " - {} agent type {}:", + "update".yellow(), + agent_type.log_color_highlight() + )); + if let DiffForHashOf::ValueDiff { diff } = inner { + log_provision_config_diff(diff); + } + } + } + } + } + } + }, + } + } + logln(""); + } + if !self.http_api_deployments.is_empty() { + logln( + "HTTP API deployment changes:" + .log_color_help_group() + .to_string(), + ); + for (domain, http_api_deployment_diff) in &self.http_api_deployments { + match http_api_deployment_diff { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} HTTP API deployment {}", + "create".green(), + domain.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} HTTP API deployment {}", + "delete".red(), + domain.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(diff) => match diff { + DiffForHashOf::HashDiff { .. } => logln(format!( + " - {} HTTP API deployment {}", + "update".yellow(), + domain.log_color_highlight() + )), + DiffForHashOf::ValueDiff { diff } => { + logln(format!( + " - {} HTTP API deployment {}, changes:", + "update".yellow(), + domain.log_color_highlight() + )); + if diff.webhooks_url_changed { + logln(" - webhooks_url"); + } + if diff.openapi_endpoint_changed { + logln(" - openapi_endpoint"); + } + if !diff.agents_changes.is_empty() { + logln(" - agents"); + for (agent_id, agent_diff) in &diff.agents_changes { + match agent_diff { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} agent {}", + "create".green(), + agent_id.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} agent {}", + "delete".red(), + agent_id.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(diff) => { + logln(format!( + " - {} agent {}, changes:", + "update".yellow(), + agent_id.log_color_highlight() + )); + if diff.security_scheme_changed { + logln(" - security_scheme"); + } + if diff.test_session_header_changed { + logln(" - test_session_header"); + } + } + } + } + } + } + }, + } + } + logln(""); + } + if !self.mcp_deployments.is_empty() { + logln("MCP deployment changes:".log_color_help_group().to_string()); + for (domain, mcp_deployment_diff) in &self.mcp_deployments { + match mcp_deployment_diff { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} MCP deployment {}", + "create".green(), + domain.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} MCP deployment {}", + "delete".red(), + domain.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(diff) => match diff { + DiffForHashOf::HashDiff { .. } => { + logln(format!( + " - {} MCP deployment {}", + "update".yellow(), + domain.log_color_highlight() + )); + } + DiffForHashOf::ValueDiff { diff } => { + logln(format!( + " - {} MCP deployment {}, changes:", + "update".yellow(), + domain.log_color_highlight() + )); + if !diff.agents_changes.is_empty() { + logln(" - agents"); + for (agent_id, agent_diff) in &diff.agents_changes { + match agent_diff { + BTreeMapDiffValue::Create => { + logln(format!( + " - {} agent {}", + "create".green(), + agent_id.log_color_highlight() + )); + } + BTreeMapDiffValue::Delete => { + logln(format!( + " - {} agent {}", + "delete".red(), + agent_id.log_color_highlight() + )); + } + BTreeMapDiffValue::Update(diff) => { + logln(format!( + " - {} agent {}, changes:", + "update".yellow(), + agent_id.log_color_highlight() + )); + if diff.security_scheme_changed { + logln(" - security_scheme"); + } + } + } + } + } + } + }, + } + } + logln(""); + } + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + let _ = config; + self.log(); + Ok(()) + } +} + +impl StructuredOutput for DeploymentDiff { + const KIND: &'static str = "deploy.diff"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + self.masked(config) + .map_err(serde::ser::Error::custom)? + .serialize(serializer) + } +} + +impl Masked for DeploymentDiff { + fn masked(mut self, config: MaskingConfig) -> anyhow::Result { + if config.show_secrets { + return Ok(self); + } + + mask_deployment_diff_secrets(&mut self)?; + Ok(self) + } +} + +fn mask_deployment_diff_secrets(diff: &mut DeploymentDiff) -> anyhow::Result<()> { + for component_change in diff.components.values_mut() { + let BTreeMapDiffValue::Update(component_diff) = component_change else { + continue; + }; + let DiffForHashOf::ValueDiff { + diff: component_diff, + } = component_diff + else { + continue; + }; + + for provision_config_change in component_diff + .agent_type_provision_config_changes + .values_mut() + { + let BTreeMapDiffValue::Update(provision_config_diff) = provision_config_change else { + continue; + }; + let DiffForHashOf::ValueDiff { + diff: provision_config_diff, + } = provision_config_diff + else { + continue; + }; + mask_agent_type_provision_config_diff(provision_config_diff)?; + } + } + + Ok(()) +} + +fn mask_agent_type_provision_config_diff( + diff: &mut AgentTypeProvisionConfigDiff, +) -> anyhow::Result<()> { + for env_change in diff.env_changes.values_mut() { + mask_string_diff_update(env_change)?; + } + + for config_change in diff.config_changes.values_mut() { + mask_normalized_json_diff_update(config_change)?; + } + + Ok(()) +} + +fn mask_string_diff_update(change: &mut BTreeMapDiffValue) -> anyhow::Result<()> { + if let BTreeMapDiffValue::Update(update) = change { + *update = mask_secret_with_fingerprint(&serde_json::to_string(update)?); + } + Ok(()) +} + +fn mask_normalized_json_diff_update( + change: &mut BTreeMapDiffValue, +) -> anyhow::Result<()> { + if let BTreeMapDiffValue::Update(update) = change { + *update = NormalizedJsonValue(serde_json::Value::String(mask_secret_with_fingerprint( + &serde_json::to_string(update)?, + ))); + } + Ok(()) +} + +fn log_provision_config_diff(diff: &AgentTypeProvisionConfigDiff) { + if !diff.env_changes.is_empty() { + logln(" - env"); + } + if !diff.config_changes.is_empty() { + logln(" - agent config"); + } + if !diff.file_changes.is_empty() { + logln(" - files"); + for (path, file_diff) in &diff.file_changes { + match file_diff { + BTreeMapDiffValue::Create => logln(format!( + " - {} {}", + "add".green(), + path.log_color_highlight() + )), + BTreeMapDiffValue::Delete => logln(format!( + " - {} {}", + "remove".red(), + path.log_color_highlight() + )), + BTreeMapDiffValue::Update(inner) => { + if let DiffForHashOf::ValueDiff { diff } = inner { + let mut changes = vec![]; + if diff.content_changed { + changes.push("content"); + } + if diff.permissions_changed { + changes.push("permissions"); + } + logln(format!( + " - {} {} ({})", + "update".yellow(), + path.log_color_highlight(), + changes.join(", ") + )); + } + } + } + } + } + if !diff.plugin_changes.is_empty() { + // TODO: show plugin name/version once grant ID → name mapping is available + logln(format!( + " - plugins ({} change(s))", + diff.plugin_changes.len() + )); + } + if diff.initial_permission_changed { + logln(" - initial permissions"); + } +} + +pub fn log_unified_diff(diff: &str) { + for line in diff.lines() { + log_unified_diff_line(classify_diff_line(line)); + } +} + +pub fn log_unified_diff_for_path(path: &Path, diff: &str) { + if is_compact_diff_path(path) { + log_unified_diff_compact(diff); + } else { + log_unified_diff(diff); + } +} + +pub struct EnvironmentSetupPlanView<'a>(pub &'a EnvironmentSetupPlan); + +impl Serialize for EnvironmentSetupPlanView<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + // EnvironmentSetupPlan.display is built with the active MaskingConfig. + // This view serializes that prepared display and must not be constructed + // from display data that skipped environment setup masking. + self.0.display.serialize(serializer) + } +} + +pub struct DeployPlanView<'a> { + pub deployment_diff: &'a DeploymentDiff, + pub environment_setup: Option<&'a EnvironmentSetupPlan>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct DeployPlanFields<'a> { + deployment_diff: &'a DeploymentDiff, + environment_setup: Option<&'a EnvironmentSetupDisplay>, +} + +impl Serialize for DeployPlanView<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let deployment_diff = self + .deployment_diff + .clone() + .masked(MaskingConfig::hide_secrets()) + .map_err(serde::ser::Error::custom)?; + + DeployPlanFields { + deployment_diff: &deployment_diff, + environment_setup: self.environment_setup.map(|setup| &setup.display), + } + .serialize(serializer) + } +} + +impl TextOutput for DeployPlanView<'_> { + fn log(&self) { + let has_deployment_changes = !self.deployment_diff.components.is_empty() + || !self.deployment_diff.http_api_deployments.is_empty() + || !self.deployment_diff.mcp_deployments.is_empty(); + + if has_deployment_changes { + self.deployment_diff.log(); + } + + if let Some(environment_setup) = self.environment_setup.map(EnvironmentSetupPlanView) + && !environment_setup.0.display.is_empty() + { + environment_setup.log(); + } + } + + fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { + let _ = config; + self.log(); + Ok(()) + } +} + +impl StructuredOutput for DeployPlanView<'_> { + const KIND: &'static str = "deploy.plan"; + + fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result + where + S: Serializer, + { + let deployment_diff = self + .deployment_diff + .clone() + .masked(config) + .map_err(serde::ser::Error::custom)?; + + DeployPlanFields { + deployment_diff: &deployment_diff, + environment_setup: self.environment_setup.map(|setup| &setup.display), + } + .serialize(serializer) + } +} + +impl TextOutput for EnvironmentSetupPlanView<'_> { + fn log(&self) { + let setup = self.0; + + if !setup.display.to_be_applied.is_empty() { + logln( + "Environment setup to apply:" + .log_color_help_group() + .to_string(), + ); + if !setup.display.to_be_applied.secret_values.is_empty() { + for key in setup.display.to_be_applied.secret_values.keys() { + logln(format!( + " - create secret value {}", + key.log_color_highlight() + )); + } + } + if !setup.display.to_be_applied.retry_policies.is_empty() { + for key in setup.display.to_be_applied.retry_policies.keys() { + logln(format!( + " - create retry policy {}", + key.log_color_highlight() + )); + } + } + if !setup.display.to_be_applied.resources.is_empty() { + for key in setup.display.to_be_applied.resources.keys() { + logln(format!(" - create resource {}", key.log_color_highlight())); + } + } + } + + if !setup.display.skipped_already_exists.is_empty() { + if !setup.display.to_be_applied.is_empty() { + logln(""); + } + logln( + "Environment setup skipped because it already exists:" + .log_color_help_group() + .to_string(), + ); + if !setup + .display + .skipped_already_exists + .secret_values + .is_empty() + { + for key in &setup.display.skipped_already_exists.secret_values { + logln(format!(" - secret value {}", key.log_color_highlight())); + } + } + if !setup + .display + .skipped_already_exists + .retry_policies + .is_empty() + { + for key in &setup.display.skipped_already_exists.retry_policies { + logln(format!(" - retry policy {}", key.log_color_highlight())); + } + } + if !setup.display.skipped_already_exists.resources.is_empty() { + for key in &setup.display.skipped_already_exists.resources { + logln(format!(" - resource {}", key.log_color_highlight())); + } + } + } + } +} + +impl StructuredOutput for EnvironmentSetupPlanView<'_> { + const KIND: &'static str = "deploy.environment-setup-plan"; +} + +impl EnvironmentSetupPlanView<'_> { + pub fn has_entries_to_apply(&self) -> bool { + !self.0.display.to_be_applied.is_empty() + } +} + +fn is_compact_diff_path(path: &Path) -> bool { + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("md")) + .unwrap_or(false) +} + +fn log_unified_diff_compact(diff: &str) { + let lines: Vec> = diff.lines().map(classify_diff_line).collect(); + let runs = regroup_diff_lines(&lines); + + for run in runs { + render_diff_run(run); + } +} + +fn regroup_diff_lines<'a>(lines: &'a [DiffLine<'a>]) -> Vec> { + let mut runs = Vec::new(); + + for line in lines { + match line { + DiffLine::Added(_) => push_change_line(&mut runs, ChangeKind::Added, *line), + DiffLine::Removed(_) => push_change_line(&mut runs, ChangeKind::Removed, *line), + _ => push_other_line(&mut runs, *line), + } + } + + runs +} + +fn push_change_line<'a>(runs: &mut Vec>, kind: ChangeKind, line: DiffLine<'a>) { + match runs.last_mut() { + Some(DiffRun::Change { + kind: existing_kind, + lines, + }) if *existing_kind == kind => lines.push(line), + _ => runs.push(DiffRun::Change { + kind, + lines: vec![line], + }), + } +} + +fn push_other_line<'a>(runs: &mut Vec>, line: DiffLine<'a>) { + match runs.last_mut() { + Some(DiffRun::Other(lines)) => lines.push(line), + _ => runs.push(DiffRun::Other(vec![line])), + } +} + +fn render_diff_run(run: DiffRun<'_>) { + match run { + DiffRun::Change { lines, .. } if lines.len() > DIFF_COLLAPSE_THRESHOLD => { + let head_keep = DIFF_COLLAPSE_KEEP_HEAD.min(lines.len()); + let tail_keep = DIFF_COLLAPSE_KEEP_TAIL.min(lines.len() - head_keep); + + for line in lines.iter().take(head_keep) { + log_unified_diff_line(*line); + } + + for _ in 0..DIFF_COLLAPSE_DOTS { + logln(".".dimmed().to_string()); + } + + for line in lines.iter().skip(lines.len() - tail_keep).take(tail_keep) { + log_unified_diff_line(*line); + } + } + DiffRun::Change { lines, .. } | DiffRun::Other(lines) => { + for line in lines { + log_unified_diff_line(line); + } + } + } +} + +fn log_unified_diff_line(line: DiffLine<'_>) { + match line { + DiffLine::Added(raw) => logln(raw.green().bold().to_string()), + DiffLine::Removed(raw) => logln(raw.red().bold().to_string()), + DiffLine::Hunk(raw) => logln(raw.bold().to_string()), + DiffLine::Other(raw) => logln(raw), + } +} + +fn classify_diff_line(line: &str) -> DiffLine<'_> { + if line.starts_with('+') && !line.starts_with("+++") { + DiffLine::Added(line) + } else if line.starts_with('-') && !line.starts_with("---") { + DiffLine::Removed(line) + } else if line.starts_with("@@") { + DiffLine::Hunk(line) + } else { + DiffLine::Other(line) + } +} + +#[derive(Clone, Copy, Eq, PartialEq)] +enum ChangeKind { + Added, + Removed, +} + +#[derive(Clone, Copy)] +enum DiffLine<'a> { + Added(&'a str), + Removed(&'a str), + Hunk(&'a str), + Other(&'a str), +} + +enum DiffRun<'a> { + Change { + kind: ChangeKind, + lines: Vec>, + }, + Other(Vec>), +} diff --git a/cli/golem-cli/src/model/text/deployment.rs b/cli/golem-cli/src/model/text/deployment.rs deleted file mode 100644 index 6a2a4e1622..0000000000 --- a/cli/golem-cli/src/model/text/deployment.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::model::cli_output::StructuredOutput; -use crate::model::masking::Masked; -use crate::model::text::fmt::{ - Column, FieldsBuilder, MessageWithFields, TextOutput, format_id, format_main_id, log_table, - new_table_full_condensed, -}; -use golem_client::model::Deployment; -use golem_common::model::application::ApplicationName; -use golem_common::model::deployment::CurrentDeployment; -use golem_common::model::environment::EnvironmentName; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeploymentNewView { - pub application_name: ApplicationName, - pub environment_name: EnvironmentName, - pub deployment: CurrentDeployment, -} - -impl Masked for DeploymentNewView {} - -impl MessageWithFields for DeploymentNewView { - fn message(&self) -> String { - "Created new deployment".to_owned() - } - - fn fields(&self) -> Vec<(String, String)> { - let mut fields = FieldsBuilder::new(); - - fields - .fmt_field("Application", &self.application_name.0, format_id) - .fmt_field("Environment", &self.environment_name.0, format_id) - .fmt_field( - "Environment ID", - &self.deployment.environment_id, - format_main_id, - ) - .fmt_field( - "Deployment Revision", - &self.deployment.revision, - format_main_id, - ); - - fields.fmt_field_optional( - "Deployment Version", - &self.deployment.version.0, - !self.deployment.version.0.is_empty(), - format_id, - ); - - fields - .fmt_field("Hash", &self.deployment.deployment_hash, format_id) - .field("Deploy Revision", &self.deployment.current_revision); - - fields.build() - } -} - -impl StructuredOutput for DeploymentNewView { - const KIND: &'static str = "deploy.deployment"; -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeploymentListView { - pub deployments: Vec, -} - -impl StructuredOutput for DeploymentListView { - const KIND: &'static str = "deploy.deployments"; -} - -impl TextOutput for DeploymentListView { - fn log(&self) { - let mut table = new_table_full_condensed(vec![ - Column::new("Deployment Revision").fixed_right(), - Column::new("Deployment Version").fixed_right(), - Column::new("Hash"), - ]); - for dep in &self.deployments { - table.add_row(vec![ - dep.revision.get().to_string(), - dep.version.0.clone(), - dep.deployment_hash.to_string(), - ]); - } - log_table(table); - } -} diff --git a/cli/golem-cli/src/model/text/diff.rs b/cli/golem-cli/src/model/text/diff.rs deleted file mode 100644 index 6aec1bb7d5..0000000000 --- a/cli/golem-cli/src/model/text/diff.rs +++ /dev/null @@ -1,712 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use crate::log::{LogColorize, logln}; -use crate::model::cli_output::StructuredOutput; -use crate::model::deploy::{EnvironmentSetupDisplay, EnvironmentSetupPlan}; -use crate::model::masking::{Masked, MaskingConfig, mask_secret_with_fingerprint}; -use crate::model::text::fmt::TextOutput; -use colored::Colorize; -use golem_common::base_model::json::NormalizedJsonValue; -use golem_common::model::diff::{ - AgentTypeProvisionConfigDiff, BTreeMapDiffValue, DeploymentDiff, DiffForHashOf, -}; -use serde::Serialize; -use serde::ser::Serializer; -use std::path::Path; - -const DIFF_COLLAPSE_THRESHOLD: usize = 12; -const DIFF_COLLAPSE_KEEP_HEAD: usize = 3; -const DIFF_COLLAPSE_KEEP_TAIL: usize = 3; -const DIFF_COLLAPSE_DOTS: usize = 3; - -impl TextOutput for DeploymentDiff { - fn log(&self) { - logln(""); - if !self.components.is_empty() { - logln("Component changes:".log_color_help_group().to_string()); - for (component_name, component_diff) in &self.components { - match component_diff { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} component {}", - "create".green(), - component_name.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} component {}", - "delete".red(), - component_name.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(diff) => match diff { - DiffForHashOf::HashDiff { .. } => { - logln(format!( - " - {} component {}", - "update".yellow(), - component_name.log_color_highlight() - )); - } - DiffForHashOf::ValueDiff { diff } => { - logln(format!( - " - {} component {}, changes:", - "update".yellow(), - component_name.log_color_highlight() - )); - if diff.wasm_changed { - logln(" - binary"); - } - if !diff.agent_type_provision_config_changes.is_empty() { - logln(" - provision configs"); - for (agent_type, change) in - &diff.agent_type_provision_config_changes - { - match change { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} agent type {}", - "create".green(), - agent_type.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} agent type {}", - "delete".red(), - agent_type.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(inner) => { - logln(format!( - " - {} agent type {}:", - "update".yellow(), - agent_type.log_color_highlight() - )); - if let DiffForHashOf::ValueDiff { diff } = inner { - log_provision_config_diff(diff); - } - } - } - } - } - } - }, - } - } - logln(""); - } - if !self.http_api_deployments.is_empty() { - logln( - "HTTP API deployment changes:" - .log_color_help_group() - .to_string(), - ); - for (domain, http_api_deployment_diff) in &self.http_api_deployments { - match http_api_deployment_diff { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} HTTP API deployment {}", - "create".green(), - domain.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} HTTP API deployment {}", - "delete".red(), - domain.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(diff) => match diff { - DiffForHashOf::HashDiff { .. } => logln(format!( - " - {} HTTP API deployment {}", - "update".yellow(), - domain.log_color_highlight() - )), - DiffForHashOf::ValueDiff { diff } => { - logln(format!( - " - {} HTTP API deployment {}, changes:", - "update".yellow(), - domain.log_color_highlight() - )); - if diff.webhooks_url_changed { - logln(" - webhooks_url"); - } - if diff.openapi_endpoint_changed { - logln(" - openapi_endpoint"); - } - if !diff.agents_changes.is_empty() { - logln(" - agents"); - for (agent_id, agent_diff) in &diff.agents_changes { - match agent_diff { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} agent {}", - "create".green(), - agent_id.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} agent {}", - "delete".red(), - agent_id.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(diff) => { - logln(format!( - " - {} agent {}, changes:", - "update".yellow(), - agent_id.log_color_highlight() - )); - if diff.security_scheme_changed { - logln(" - security_scheme"); - } - if diff.test_session_header_changed { - logln(" - test_session_header"); - } - } - } - } - } - } - }, - } - } - logln(""); - } - if !self.mcp_deployments.is_empty() { - logln("MCP deployment changes:".log_color_help_group().to_string()); - for (domain, mcp_deployment_diff) in &self.mcp_deployments { - match mcp_deployment_diff { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} MCP deployment {}", - "create".green(), - domain.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} MCP deployment {}", - "delete".red(), - domain.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(diff) => match diff { - DiffForHashOf::HashDiff { .. } => { - logln(format!( - " - {} MCP deployment {}", - "update".yellow(), - domain.log_color_highlight() - )); - } - DiffForHashOf::ValueDiff { diff } => { - logln(format!( - " - {} MCP deployment {}, changes:", - "update".yellow(), - domain.log_color_highlight() - )); - if !diff.agents_changes.is_empty() { - logln(" - agents"); - for (agent_id, agent_diff) in &diff.agents_changes { - match agent_diff { - BTreeMapDiffValue::Create => { - logln(format!( - " - {} agent {}", - "create".green(), - agent_id.log_color_highlight() - )); - } - BTreeMapDiffValue::Delete => { - logln(format!( - " - {} agent {}", - "delete".red(), - agent_id.log_color_highlight() - )); - } - BTreeMapDiffValue::Update(diff) => { - logln(format!( - " - {} agent {}, changes:", - "update".yellow(), - agent_id.log_color_highlight() - )); - if diff.security_scheme_changed { - logln(" - security_scheme"); - } - } - } - } - } - } - }, - } - } - logln(""); - } - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - let _ = config; - self.log(); - Ok(()) - } -} - -impl StructuredOutput for DeploymentDiff { - const KIND: &'static str = "deploy.diff"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } -} - -impl Masked for DeploymentDiff { - fn masked(mut self, config: MaskingConfig) -> anyhow::Result { - if config.show_secrets { - return Ok(self); - } - - mask_deployment_diff_secrets(&mut self)?; - Ok(self) - } -} - -fn mask_deployment_diff_secrets(diff: &mut DeploymentDiff) -> anyhow::Result<()> { - for component_change in diff.components.values_mut() { - let BTreeMapDiffValue::Update(component_diff) = component_change else { - continue; - }; - let DiffForHashOf::ValueDiff { - diff: component_diff, - } = component_diff - else { - continue; - }; - - for provision_config_change in component_diff - .agent_type_provision_config_changes - .values_mut() - { - let BTreeMapDiffValue::Update(provision_config_diff) = provision_config_change else { - continue; - }; - let DiffForHashOf::ValueDiff { - diff: provision_config_diff, - } = provision_config_diff - else { - continue; - }; - mask_agent_type_provision_config_diff(provision_config_diff)?; - } - } - - Ok(()) -} - -fn mask_agent_type_provision_config_diff( - diff: &mut AgentTypeProvisionConfigDiff, -) -> anyhow::Result<()> { - for env_change in diff.env_changes.values_mut() { - mask_string_diff_update(env_change)?; - } - - for config_change in diff.config_changes.values_mut() { - mask_normalized_json_diff_update(config_change)?; - } - - Ok(()) -} - -fn mask_string_diff_update(change: &mut BTreeMapDiffValue) -> anyhow::Result<()> { - if let BTreeMapDiffValue::Update(update) = change { - *update = mask_secret_with_fingerprint(&serde_json::to_string(update)?); - } - Ok(()) -} - -fn mask_normalized_json_diff_update( - change: &mut BTreeMapDiffValue, -) -> anyhow::Result<()> { - if let BTreeMapDiffValue::Update(update) = change { - *update = NormalizedJsonValue(serde_json::Value::String(mask_secret_with_fingerprint( - &serde_json::to_string(update)?, - ))); - } - Ok(()) -} - -fn log_provision_config_diff(diff: &AgentTypeProvisionConfigDiff) { - if !diff.env_changes.is_empty() { - logln(" - env"); - } - if !diff.config_changes.is_empty() { - logln(" - agent config"); - } - if !diff.file_changes.is_empty() { - logln(" - files"); - for (path, file_diff) in &diff.file_changes { - match file_diff { - BTreeMapDiffValue::Create => logln(format!( - " - {} {}", - "add".green(), - path.log_color_highlight() - )), - BTreeMapDiffValue::Delete => logln(format!( - " - {} {}", - "remove".red(), - path.log_color_highlight() - )), - BTreeMapDiffValue::Update(inner) => { - if let DiffForHashOf::ValueDiff { diff } = inner { - let mut changes = vec![]; - if diff.content_changed { - changes.push("content"); - } - if diff.permissions_changed { - changes.push("permissions"); - } - logln(format!( - " - {} {} ({})", - "update".yellow(), - path.log_color_highlight(), - changes.join(", ") - )); - } - } - } - } - } - if !diff.plugin_changes.is_empty() { - // TODO: show plugin name/version once grant ID → name mapping is available - logln(format!( - " - plugins ({} change(s))", - diff.plugin_changes.len() - )); - } - if diff.initial_permission_changed { - logln(" - initial permissions"); - } -} - -pub fn log_unified_diff(diff: &str) { - for line in diff.lines() { - log_unified_diff_line(classify_diff_line(line)); - } -} - -pub fn log_unified_diff_for_path(path: &Path, diff: &str) { - if is_compact_diff_path(path) { - log_unified_diff_compact(diff); - } else { - log_unified_diff(diff); - } -} - -pub struct EnvironmentSetupPlanView<'a>(pub &'a EnvironmentSetupPlan); - -impl Serialize for EnvironmentSetupPlanView<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - // EnvironmentSetupPlan.display is built with the active MaskingConfig. - // This view serializes that prepared display and must not be constructed - // from display data that skipped environment setup masking. - self.0.display.serialize(serializer) - } -} - -pub struct DeployPlanView<'a> { - pub deployment_diff: &'a DeploymentDiff, - pub environment_setup: Option<&'a EnvironmentSetupPlan>, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct DeployPlanFields<'a> { - deployment_diff: &'a DeploymentDiff, - environment_setup: Option<&'a EnvironmentSetupDisplay>, -} - -impl Serialize for DeployPlanView<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - let deployment_diff = self - .deployment_diff - .clone() - .masked(MaskingConfig::hide_secrets()) - .map_err(serde::ser::Error::custom)?; - - DeployPlanFields { - deployment_diff: &deployment_diff, - environment_setup: self.environment_setup.map(|setup| &setup.display), - } - .serialize(serializer) - } -} - -impl TextOutput for DeployPlanView<'_> { - fn log(&self) { - let has_deployment_changes = !self.deployment_diff.components.is_empty() - || !self.deployment_diff.http_api_deployments.is_empty() - || !self.deployment_diff.mcp_deployments.is_empty(); - - if has_deployment_changes { - self.deployment_diff.log(); - } - - if let Some(environment_setup) = self.environment_setup.map(EnvironmentSetupPlanView) - && !environment_setup.0.display.is_empty() - { - environment_setup.log(); - } - } - - fn log_masked(self, config: MaskingConfig) -> anyhow::Result<()> { - let _ = config; - self.log(); - Ok(()) - } -} - -impl StructuredOutput for DeployPlanView<'_> { - const KIND: &'static str = "deploy.plan"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - let deployment_diff = self - .deployment_diff - .clone() - .masked(config) - .map_err(serde::ser::Error::custom)?; - - DeployPlanFields { - deployment_diff: &deployment_diff, - environment_setup: self.environment_setup.map(|setup| &setup.display), - } - .serialize(serializer) - } -} - -impl TextOutput for EnvironmentSetupPlanView<'_> { - fn log(&self) { - let setup = self.0; - - if !setup.display.to_be_applied.is_empty() { - logln( - "Environment setup to apply:" - .log_color_help_group() - .to_string(), - ); - if !setup.display.to_be_applied.secret_values.is_empty() { - for key in setup.display.to_be_applied.secret_values.keys() { - logln(format!( - " - create secret value {}", - key.log_color_highlight() - )); - } - } - if !setup.display.to_be_applied.retry_policies.is_empty() { - for key in setup.display.to_be_applied.retry_policies.keys() { - logln(format!( - " - create retry policy {}", - key.log_color_highlight() - )); - } - } - if !setup.display.to_be_applied.resources.is_empty() { - for key in setup.display.to_be_applied.resources.keys() { - logln(format!(" - create resource {}", key.log_color_highlight())); - } - } - } - - if !setup.display.skipped_already_exists.is_empty() { - if !setup.display.to_be_applied.is_empty() { - logln(""); - } - logln( - "Environment setup skipped because it already exists:" - .log_color_help_group() - .to_string(), - ); - if !setup - .display - .skipped_already_exists - .secret_values - .is_empty() - { - for key in &setup.display.skipped_already_exists.secret_values { - logln(format!(" - secret value {}", key.log_color_highlight())); - } - } - if !setup - .display - .skipped_already_exists - .retry_policies - .is_empty() - { - for key in &setup.display.skipped_already_exists.retry_policies { - logln(format!(" - retry policy {}", key.log_color_highlight())); - } - } - if !setup.display.skipped_already_exists.resources.is_empty() { - for key in &setup.display.skipped_already_exists.resources { - logln(format!(" - resource {}", key.log_color_highlight())); - } - } - } - } -} - -impl StructuredOutput for EnvironmentSetupPlanView<'_> { - const KIND: &'static str = "deploy.environment-setup-plan"; -} - -impl EnvironmentSetupPlanView<'_> { - pub fn has_entries_to_apply(&self) -> bool { - !self.0.display.to_be_applied.is_empty() - } -} - -fn is_compact_diff_path(path: &Path) -> bool { - path.extension() - .and_then(|ext| ext.to_str()) - .map(|ext| ext.eq_ignore_ascii_case("md")) - .unwrap_or(false) -} - -fn log_unified_diff_compact(diff: &str) { - let lines: Vec> = diff.lines().map(classify_diff_line).collect(); - let runs = regroup_diff_lines(&lines); - - for run in runs { - render_diff_run(run); - } -} - -fn regroup_diff_lines<'a>(lines: &'a [DiffLine<'a>]) -> Vec> { - let mut runs = Vec::new(); - - for line in lines { - match line { - DiffLine::Added(_) => push_change_line(&mut runs, ChangeKind::Added, *line), - DiffLine::Removed(_) => push_change_line(&mut runs, ChangeKind::Removed, *line), - _ => push_other_line(&mut runs, *line), - } - } - - runs -} - -fn push_change_line<'a>(runs: &mut Vec>, kind: ChangeKind, line: DiffLine<'a>) { - match runs.last_mut() { - Some(DiffRun::Change { - kind: existing_kind, - lines, - }) if *existing_kind == kind => lines.push(line), - _ => runs.push(DiffRun::Change { - kind, - lines: vec![line], - }), - } -} - -fn push_other_line<'a>(runs: &mut Vec>, line: DiffLine<'a>) { - match runs.last_mut() { - Some(DiffRun::Other(lines)) => lines.push(line), - _ => runs.push(DiffRun::Other(vec![line])), - } -} - -fn render_diff_run(run: DiffRun<'_>) { - match run { - DiffRun::Change { lines, .. } if lines.len() > DIFF_COLLAPSE_THRESHOLD => { - let head_keep = DIFF_COLLAPSE_KEEP_HEAD.min(lines.len()); - let tail_keep = DIFF_COLLAPSE_KEEP_TAIL.min(lines.len() - head_keep); - - for line in lines.iter().take(head_keep) { - log_unified_diff_line(*line); - } - - for _ in 0..DIFF_COLLAPSE_DOTS { - logln(".".dimmed().to_string()); - } - - for line in lines.iter().skip(lines.len() - tail_keep).take(tail_keep) { - log_unified_diff_line(*line); - } - } - DiffRun::Change { lines, .. } | DiffRun::Other(lines) => { - for line in lines { - log_unified_diff_line(line); - } - } - } -} - -fn log_unified_diff_line(line: DiffLine<'_>) { - match line { - DiffLine::Added(raw) => logln(raw.green().bold().to_string()), - DiffLine::Removed(raw) => logln(raw.red().bold().to_string()), - DiffLine::Hunk(raw) => logln(raw.bold().to_string()), - DiffLine::Other(raw) => logln(raw), - } -} - -fn classify_diff_line(line: &str) -> DiffLine<'_> { - if line.starts_with('+') && !line.starts_with("+++") { - DiffLine::Added(line) - } else if line.starts_with('-') && !line.starts_with("---") { - DiffLine::Removed(line) - } else if line.starts_with("@@") { - DiffLine::Hunk(line) - } else { - DiffLine::Other(line) - } -} - -#[derive(Clone, Copy, Eq, PartialEq)] -enum ChangeKind { - Added, - Removed, -} - -#[derive(Clone, Copy)] -enum DiffLine<'a> { - Added(&'a str), - Removed(&'a str), - Hunk(&'a str), - Other(&'a str), -} - -enum DiffRun<'a> { - Change { - kind: ChangeKind, - lines: Vec>, - }, - Other(Vec>), -} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index a66f42f375..88562e0851 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -13,7 +13,5 @@ // limitations under the License. pub mod action_result; -pub mod deployment; -pub mod diff; pub mod fmt; pub mod template; From ecb662c57148c5f304d809b05a59052c72645e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:27:41 +0200 Subject: [PATCH 57/70] move template list view into app/template --- .../src/{model/text/template.rs => app/template/list_view.rs} | 0 cli/golem-cli/src/app/template/mod.rs | 2 ++ cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/model/cli_output/tests.rs | 2 +- cli/golem-cli/src/model/text/mod.rs | 1 - 5 files changed, 4 insertions(+), 3 deletions(-) rename cli/golem-cli/src/{model/text/template.rs => app/template/list_view.rs} (100%) diff --git a/cli/golem-cli/src/model/text/template.rs b/cli/golem-cli/src/app/template/list_view.rs similarity index 100% rename from cli/golem-cli/src/model/text/template.rs rename to cli/golem-cli/src/app/template/list_view.rs diff --git a/cli/golem-cli/src/app/template/mod.rs b/cli/golem-cli/src/app/template/mod.rs index a51b0c9f37..14bb2db3d9 100644 --- a/cli/golem-cli/src/app/template/mod.rs +++ b/cli/golem-cli/src/app/template/mod.rs @@ -14,6 +14,7 @@ mod description; mod generator; +mod list_view; mod metadata; mod plan; mod repo; @@ -23,6 +24,7 @@ mod template; pub use description::TemplateDescription; pub use generator::InMemoryFs; +pub use list_view::TemplateListView; pub use metadata::AppTemplateMetadata; pub use plan::{ MultiComponentLayoutUpgradePlan, MultiComponentLayoutUpgradePlanStep, SafeTemplatePlan, diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 8c1baf2630..724aa5768b 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -19,6 +19,7 @@ use crate::app::context::BuildContext; use crate::app::error::CustomCommandError; use crate::app::template::AppTemplateName; use crate::app::template::TemplateDescription; +use crate::app::template::TemplateListView; use crate::command::builtin_exec_subcommands; use crate::command::exec::ExecSubcommand; use crate::command::shared_args::{ @@ -61,7 +62,6 @@ use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdent use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; -use crate::model::text::template::TemplateListView; use anyhow::{anyhow, bail}; use colored::Colorize; use futures_util::{StreamExt, TryStreamExt, stream}; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 937aa86659..b9ec095a7e 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3738,7 +3738,7 @@ fn arb_new_app_result() -> OutputDocumentStrategy { fn arb_template_list_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_template_description(), 0..5) - .prop_map(|templates| crate::model::text::template::TemplateListView { templates }), + .prop_map(|templates| crate::app::template::TemplateListView { templates }), ) } diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 88562e0851..89a8e0c530 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -14,4 +14,3 @@ pub mod action_result; pub mod fmt; -pub mod template; From 712ce41603c25e6f7126faafe649d56621de8bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:31:11 +0200 Subject: [PATCH 58/70] split app lifecycle result views into their entity modules --- cli/golem-cli/src/app/build/gen_bridge.rs | 16 ++++ cli/golem-cli/src/command_handler/app/mod.rs | 10 +- .../src/command_handler/app/template.rs | 2 +- cli/golem-cli/src/command_handler/bridge.rs | 6 +- cli/golem-cli/src/model/app.rs | 45 ++++++++- cli/golem-cli/src/model/cli_output/tests.rs | 30 +++--- cli/golem-cli/src/model/deploy.rs | 17 +++- cli/golem-cli/src/model/text/action_result.rs | 95 ------------------- cli/golem-cli/src/model/text/mod.rs | 1 - 9 files changed, 96 insertions(+), 126 deletions(-) delete mode 100644 cli/golem-cli/src/model/text/action_result.rs diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index e24737afa2..bcd7fd85c4 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -20,12 +20,15 @@ use crate::model::app::{ BridgeSdkTarget, BridgeSdkTargetKind, BridgeSdkTargetSubject, ComponentDependency, CustomBridgeSdkTarget, }; +use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::{ReplAgentMetadata, ReplMetadata}; +use crate::model::text::fmt::{NoTextOutput, TextOutput}; use anyhow::bail; use camino::Utf8PathBuf; use golem_common::model::component::ComponentName; use itertools::Itertools; +use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; #[derive(Debug, Default)] @@ -977,6 +980,19 @@ pub(crate) fn validate_supported_bridge_targets(targets: &[BridgeSdkTarget]) -> Ok(()) } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenerateBridgeResult { + pub generated: bool, +} + +impl NoTextOutput for GenerateBridgeResult {} +impl TextOutput for GenerateBridgeResult {} + +impl StructuredOutput for GenerateBridgeResult { + const KIND: &'static str = "generate-bridge"; +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index 724aa5768b..b706fcc5e3 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -163,7 +163,7 @@ impl AppCommandHandler { if outcome.is_ok() { self.ctx .log_handler() - .log_output(crate::model::text::action_result::BuildResult { built: true })?; + .log_output(crate::model::app::BuildResult { built: true })?; } outcome } @@ -181,7 +181,7 @@ impl AppCommandHandler { if outcome.is_ok() { self.ctx .log_handler() - .log_output(crate::model::text::action_result::CleanResult { cleaned: true })?; + .log_output(crate::model::app::CleanResult { cleaned: true })?; } outcome } @@ -365,9 +365,9 @@ impl AppCommandHandler { }, }; if outcome.is_ok() { - self.ctx.log_handler().log_output( - crate::model::text::action_result::DeployResultView { deployed: true }, - )?; + self.ctx + .log_handler() + .log_output(crate::model::deploy::DeployResultView { deployed: true })?; } outcome } diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index abc57e834c..b4f6630e87 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -185,7 +185,7 @@ impl TemplateHandler { self.ctx .log_handler() - .log_output(crate::model::text::action_result::NewAppResult { + .log_output(crate::model::app::NewAppResult { created: true, application_name: selections.application_name.to_string(), application_dir: context.application_path.clone(), diff --git a/cli/golem-cli/src/command_handler/bridge.rs b/cli/golem-cli/src/command_handler/bridge.rs index c64bac8039..6d0e8f68a7 100644 --- a/cli/golem-cli/src/command_handler/bridge.rs +++ b/cli/golem-cli/src/command_handler/bridge.rs @@ -50,9 +50,9 @@ impl BridgeCommandHandler { ) .await?; - self.ctx.log_handler().log_output( - crate::model::text::action_result::GenerateBridgeResult { generated: true }, - )?; + self.ctx + .log_handler() + .log_output(crate::app::build::gen_bridge::GenerateBridgeResult { generated: true })?; Ok(()) } diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 871422cff0..96e130ba7b 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -27,9 +27,11 @@ use crate::model::cascade::property::map::{MapMergeMode, MapProperty}; use crate::model::cascade::property::optional::OptionalProperty; use crate::model::cascade::property::vec::{VecMergeMode, VecProperty}; use crate::model::cascade::store::Store; +use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; use crate::model::template::Template; +use crate::model::text::fmt::{NoTextOutput, TextOutput}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{Context, anyhow}; use golem_common::model::agent::AgentTypeName; @@ -48,7 +50,7 @@ use heck::{ }; use indexmap::IndexMap; use itertools::Itertools; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Serialize, Serializer}; use serde_json::Value as JsonValue; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Formatter; @@ -4113,6 +4115,47 @@ mod app_builder { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CleanResult { + pub cleaned: bool, +} + +impl NoTextOutput for CleanResult {} +impl TextOutput for CleanResult {} + +impl StructuredOutput for CleanResult { + const KIND: &'static str = "clean"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildResult { + pub built: bool, +} + +impl NoTextOutput for BuildResult {} +impl TextOutput for BuildResult {} + +impl StructuredOutput for BuildResult { + const KIND: &'static str = "build"; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NewAppResult { + pub created: bool, + pub application_name: String, + pub application_dir: PathBuf, +} + +impl NoTextOutput for NewAppResult {} +impl TextOutput for NewAppResult {} + +impl StructuredOutput for NewAppResult { + const KIND: &'static str = "new"; +} + #[cfg(test)] mod test { use crate::bridge_gen::{BridgeMode, bridge_client_directory_name}; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index b9ec095a7e..34181338b6 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -2058,29 +2058,25 @@ fn sample_public_oplog_entries() -> Vec OutputDocumentStrategy { - serialized_output( - any::().prop_map(|built| crate::model::text::action_result::BuildResult { built }), - ) + serialized_output(any::().prop_map(|built| crate::model::app::BuildResult { built })) } fn arb_clean_result() -> OutputDocumentStrategy { - serialized_output( - any::() - .prop_map(|cleaned| crate::model::text::action_result::CleanResult { cleaned }), - ) + serialized_output(any::().prop_map(|cleaned| crate::model::app::CleanResult { cleaned })) } fn arb_deploy_result() -> OutputDocumentStrategy { serialized_output( - any::() - .prop_map(|deployed| crate::model::text::action_result::DeployResultView { deployed }), + any::().prop_map(|deployed| crate::model::deploy::DeployResultView { deployed }), ) } fn arb_generate_bridge_result() -> OutputDocumentStrategy { - serialized_output(any::().prop_map(|generated| { - crate::model::text::action_result::GenerateBridgeResult { generated } - })) + serialized_output( + any::().prop_map( + |generated| crate::app::build::gen_bridge::GenerateBridgeResult { generated }, + ), + ) } fn arb_agent_type_get_result() -> OutputDocumentStrategy { @@ -3724,12 +3720,10 @@ fn arb_security_scheme_provider() -> BoxedStrategy OutputDocumentStrategy { serialized_output( (any::(), arb_small_string(), arb_small_string()).prop_map( - |(created, application_name, application_dir)| { - crate::model::text::action_result::NewAppResult { - created, - application_name, - application_dir: PathBuf::from(application_dir), - } + |(created, application_name, application_dir)| crate::model::app::NewAppResult { + created, + application_name, + application_dir: PathBuf::from(application_dir), }, ), ) diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index eaa2325495..0237365c79 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -27,8 +27,8 @@ use crate::model::masking::{ mask_secret_with_fingerprint, }; use crate::model::text::fmt::{ - Column, FieldsBuilder, MessageWithFields, TextOutput, format_id, format_main_id, log_table, - new_table_full_condensed, + Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, + log_table, new_table_full_condensed, }; use colored::Colorize; use golem_client::model::{AgentSecretDto, Deployment, RetryPolicyDto}; @@ -2192,3 +2192,16 @@ enum DiffRun<'a> { }, Other(Vec>), } + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeployResultView { + pub deployed: bool, +} + +impl NoTextOutput for DeployResultView {} +impl TextOutput for DeployResultView {} + +impl StructuredOutput for DeployResultView { + const KIND: &'static str = "deploy"; +} diff --git a/cli/golem-cli/src/model/text/action_result.rs b/cli/golem-cli/src/model/text/action_result.rs deleted file mode 100644 index 5a4b177ba7..0000000000 --- a/cli/golem-cli/src/model/text/action_result.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Lightweight structured result views for commands whose human-readable -//! output is mostly progress text printed during the run. -//! -//! Each view implements `NoTextOutput`: when `--format text` is used -//! (the default), the user has already seen the progress lines on stdout -//! and adding another rendering of the same information would just be -//! noise. When `--format json/yaml/toon` is used, the progress text is routed -//! to stderr (see `Context::new`) and these structured payloads are -//! emitted on stdout so that automation can rely on a stable schema. - -use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::{NoTextOutput, TextOutput}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CleanResult { - pub cleaned: bool, -} - -impl NoTextOutput for CleanResult {} -impl TextOutput for CleanResult {} - -impl StructuredOutput for CleanResult { - const KIND: &'static str = "clean"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BuildResult { - pub built: bool, -} - -impl NoTextOutput for BuildResult {} -impl TextOutput for BuildResult {} - -impl StructuredOutput for BuildResult { - const KIND: &'static str = "build"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct NewAppResult { - pub created: bool, - pub application_name: String, - pub application_dir: PathBuf, -} - -impl NoTextOutput for NewAppResult {} -impl TextOutput for NewAppResult {} - -impl StructuredOutput for NewAppResult { - const KIND: &'static str = "new"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DeployResultView { - pub deployed: bool, -} - -impl NoTextOutput for DeployResultView {} -impl TextOutput for DeployResultView {} - -impl StructuredOutput for DeployResultView { - const KIND: &'static str = "deploy"; -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct GenerateBridgeResult { - pub generated: bool, -} - -impl NoTextOutput for GenerateBridgeResult {} -impl TextOutput for GenerateBridgeResult {} - -impl StructuredOutput for GenerateBridgeResult { - const KIND: &'static str = "generate-bridge"; -} diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs index 89a8e0c530..3b4a85294d 100644 --- a/cli/golem-cli/src/model/text/mod.rs +++ b/cli/golem-cli/src/model/text/mod.rs @@ -12,5 +12,4 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod action_result; pub mod fmt; From a049826c0122694f9eb3a9da3c880a45f730a3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:32:29 +0200 Subject: [PATCH 59/70] rename Template trait to TemplateRender and module to template_render --- cli/golem-cli/src/model/app.rs | 2 +- cli/golem-cli/src/model/mod.rs | 2 +- .../model/{template.rs => template_render.rs} | 34 +++++++++---------- 3 files changed, 19 insertions(+), 19 deletions(-) rename cli/golem-cli/src/model/{template.rs => template_render.rs} (85%) diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 96e130ba7b..458733d594 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -30,7 +30,7 @@ use crate::model::cascade::store::Store; use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; -use crate::model::template::Template; +use crate::model::template_render::TemplateRender; use crate::model::text::fmt::{NoTextOutput, TextOutput}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{Context, anyhow}; diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index b6169f32e9..0410ac7062 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -37,6 +37,6 @@ pub mod repl; pub mod resource_definition; pub mod retry_policy; pub mod secret; -pub mod template; +pub mod template_render; pub mod text; pub mod token; diff --git a/cli/golem-cli/src/model/template.rs b/cli/golem-cli/src/model/template_render.rs similarity index 85% rename from cli/golem-cli/src/model/template.rs rename to cli/golem-cli/src/model/template_render.rs index 517a03d56f..c6baa07e23 100644 --- a/cli/golem-cli/src/model/template.rs +++ b/cli/golem-cli/src/model/template_render.rs @@ -4,7 +4,7 @@ use minijinja::{Environment, Error}; use serde::Serialize; use std::collections::HashMap; -pub trait Template +pub trait TemplateRender where C: Serialize, Self: Sized, @@ -22,13 +22,13 @@ where } } -impl Template for String { +impl TemplateRender for String { fn render(&self, env: &Environment, ctx: &C) -> Result { env.render_str(self, ctx) } } -impl> Template for Option { +impl> TemplateRender for Option { fn render(&self, env: &Environment, ctx: &C) -> Result { match self { Some(template) => Ok(Some(template.render(env, ctx)?)), @@ -37,13 +37,13 @@ impl> Template for Option { } } -impl> Template for Vec { +impl> TemplateRender for Vec { fn render(&self, env: &Environment, ctx: &C) -> Result { self.iter().map(|elem| elem.render(env, ctx)).collect() } } -impl> Template for HashMap { +impl> TemplateRender for HashMap { fn render(&self, env: &Environment, ctx: &C) -> Result { let mut rendered = HashMap::::with_capacity(self.len()); for (key, template) in self { @@ -53,7 +53,7 @@ impl> Template for HashMap { } } -impl> Template for IndexMap { +impl> TemplateRender for IndexMap { fn render(&self, env: &Environment, ctx: &C) -> Result { let mut rendered = IndexMap::::with_capacity(self.len()); for (key, template) in self { @@ -63,7 +63,7 @@ impl> Template for IndexMap { } } -impl Template for app_raw::BuildCommand { +impl TemplateRender for app_raw::BuildCommand { fn render(&self, env: &Environment, ctx: &C) -> Result { match self { app_raw::BuildCommand::External(external) => { @@ -87,7 +87,7 @@ impl Template for app_raw::BuildCommand { } } -impl Template for app_raw::ComponentDependencies { +impl TemplateRender for app_raw::ComponentDependencies { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::ComponentDependencies { agents: self.agents.render(env, ctx)?, @@ -96,7 +96,7 @@ impl Template for app_raw::ComponentDependencies { } } -impl Template for app_raw::ComponentDependencyReference { +impl TemplateRender for app_raw::ComponentDependencyReference { fn render(&self, env: &Environment, ctx: &C) -> Result { match self { app_raw::ComponentDependencyReference::Shortcut(shortcut) => Ok( @@ -109,7 +109,7 @@ impl Template for app_raw::ComponentDependencyReference { } } -impl Template for app_raw::ComponentDependencyReferenceStruct { +impl TemplateRender for app_raw::ComponentDependencyReferenceStruct { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::ComponentDependencyReferenceStruct { component: self.component.render(env, ctx)?, @@ -118,7 +118,7 @@ impl Template for app_raw::ComponentDependencyReferenceStruct { } } -impl Template for app_raw::ExternalCommand { +impl TemplateRender for app_raw::ExternalCommand { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::ExternalCommand { command: self.command.render(env, ctx)?, @@ -132,7 +132,7 @@ impl Template for app_raw::ExternalCommand { } } -impl Template for app_raw::GenerateQuickJSCrate { +impl TemplateRender for app_raw::GenerateQuickJSCrate { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::GenerateQuickJSCrate { generate_quickjs_crate: self.generate_quickjs_crate.render(env, ctx)?, @@ -151,7 +151,7 @@ impl Template for app_raw::GenerateQuickJSCrate { } } -impl Template for app_raw::GenerateQuickJSDTS { +impl TemplateRender for app_raw::GenerateQuickJSDTS { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::GenerateQuickJSDTS { generate_quickjs_dts: self.generate_quickjs_dts.render(env, ctx)?, @@ -161,7 +161,7 @@ impl Template for app_raw::GenerateQuickJSDTS { } } -impl Template for app_raw::InjectToPrebuiltQuickJs { +impl TemplateRender for app_raw::InjectToPrebuiltQuickJs { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::InjectToPrebuiltQuickJs { inject_to_prebuilt_quickjs: self.inject_to_prebuilt_quickjs.render(env, ctx)?, @@ -171,7 +171,7 @@ impl Template for app_raw::InjectToPrebuiltQuickJs { } } -impl Template for app_raw::PreinitializeJs { +impl TemplateRender for app_raw::PreinitializeJs { fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(app_raw::PreinitializeJs { preinitialize_js: self.preinitialize_js.render(env, ctx)?, @@ -180,7 +180,7 @@ impl Template for app_raw::PreinitializeJs { } } -impl Template for serde_json::Value { +impl TemplateRender for serde_json::Value { #[allow(clippy::only_used_in_recursion)] fn render(&self, env: &Environment, ctx: &C) -> Result { Ok(match self { @@ -207,7 +207,7 @@ impl Template for serde_json::Value { } } -impl Template for serde_json::Map { +impl TemplateRender for serde_json::Map { fn render(&self, env: &Environment, ctx: &C) -> Result { let mut rendered = serde_json::Map::::with_capacity(self.len()); for (key, template) in self { From 825776458b2d8674eb285d5a1cc0ac64def1a0bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Fri, 31 Jul 2026 20:34:58 +0200 Subject: [PATCH 60/70] update integration test refs to relocated view modules --- cli/golem-cli/tests/app/build_and_deploy_all.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/golem-cli/tests/app/build_and_deploy_all.rs b/cli/golem-cli/tests/app/build_and_deploy_all.rs index 6a8a720498..699c0aa1fc 100644 --- a/cli/golem-cli/tests/app/build_and_deploy_all.rs +++ b/cli/golem-cli/tests/app/build_and_deploy_all.rs @@ -1,12 +1,12 @@ use crate::Tracing; use crate::app::{TestContext, cmd, flag}; +use golem_cli::app::template::TemplateListView; use golem_cli::bridge_gen::BridgeMode; use golem_cli::fs; +use golem_cli::model::agent::AgentTypeListView; use golem_cli::model::app::BridgeSdkTargetKind; use golem_cli::model::language::GuestLanguage; -use golem_cli::model::text::agent::AgentTypeListView; -use golem_cli::model::text::template::TemplateListView; use strum::IntoEnumIterator; use test_r::{inherit_test_dep, test}; From ab6cbb03ee5c51306252560add663acfc96e6458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 4 Aug 2026 15:39:01 +0200 Subject: [PATCH 61/70] collapse text/fmt into a single text.rs module --- cli/golem-cli/src/app/build/gen_bridge.rs | 2 +- cli/golem-cli/src/app/context.rs | 2 +- cli/golem-cli/src/app/template/list_view.rs | 2 +- cli/golem-cli/src/command_handler/agent/mod.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/command_handler/app/template.rs | 2 +- .../src/command_handler/component/mod.rs | 2 +- cli/golem-cli/src/command_handler/environment.rs | 2 +- cli/golem-cli/src/command_handler/log.rs | 2 +- .../src/command_handler/partial_match.rs | 2 +- .../src/command_handler/profile/config.rs | 4 ++-- cli/golem-cli/src/error.rs | 2 +- cli/golem-cli/src/model/account.rs | 2 +- cli/golem-cli/src/model/agent/action_result.rs | 2 +- cli/golem-cli/src/model/agent/extraction.rs | 2 +- cli/golem-cli/src/model/agent/files.rs | 2 +- cli/golem-cli/src/model/agent/mod.rs | 2 +- cli/golem-cli/src/model/agent/oplog.rs | 2 +- cli/golem-cli/src/model/agent/stream.rs | 2 +- cli/golem-cli/src/model/app.rs | 2 +- cli/golem-cli/src/model/card.rs | 2 +- cli/golem-cli/src/model/component.rs | 2 +- cli/golem-cli/src/model/config/profile.rs | 2 +- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/environment.rs | 2 +- cli/golem-cli/src/model/help.rs | 2 +- cli/golem-cli/src/model/http_api/deployment.rs | 2 +- cli/golem-cli/src/model/http_api/domain.rs | 2 +- cli/golem-cli/src/model/http_api/security.rs | 2 +- cli/golem-cli/src/model/invoke_result_view.rs | 2 +- cli/golem-cli/src/model/plugin.rs | 2 +- cli/golem-cli/src/model/resource_definition.rs | 2 +- cli/golem-cli/src/model/retry_policy.rs | 2 +- cli/golem-cli/src/model/secret.rs | 2 +- cli/golem-cli/src/model/{text/fmt.rs => text.rs} | 0 cli/golem-cli/src/model/text/mod.rs | 15 --------------- cli/golem-cli/src/model/token.rs | 2 +- 37 files changed, 36 insertions(+), 51 deletions(-) rename cli/golem-cli/src/model/{text/fmt.rs => text.rs} (100%) delete mode 100644 cli/golem-cli/src/model/text/mod.rs diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index bcd7fd85c4..5c2af7addc 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -23,7 +23,7 @@ use crate::model::app::{ use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::{ReplAgentMetadata, ReplMetadata}; -use crate::model::text::fmt::{NoTextOutput, TextOutput}; +use crate::model::text::{NoTextOutput, TextOutput}; use anyhow::bail; use camino::Utf8PathBuf; use golem_common::model::component::ComponentName; diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 2f423a1ee7..57ad0ceadd 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -35,7 +35,7 @@ use crate::model::config::server::ToFormattedServerContext; use crate::model::deploy::log_unified_diff_for_path; use crate::model::format::Format; use crate::model::language::GuestLanguage; -use crate::model::text::fmt::DecoratedIndent; +use crate::model::text::DecoratedIndent; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/app/template/list_view.rs b/cli/golem-cli/src/app/template/list_view.rs index dde1088e5b..fe36b8ca4e 100644 --- a/cli/golem-cli/src/app/template/list_view.rs +++ b/cli/golem-cli/src/app/template/list_view.rs @@ -15,7 +15,7 @@ use crate::app::template::TemplateDescription; use crate::log::current_indent_width; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::*; +use crate::model::text::*; use itertools::Itertools; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 975540c56a..cc5668032a 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -43,7 +43,7 @@ use crate::model::help::{ ParameterErrorTableView, }; use crate::model::invoke_result_view::InvokeResultView; -use crate::model::text::fmt::{log_fuzzy_match, log_text_view}; +use crate::model::text::{log_fuzzy_match, log_text_view}; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index b706fcc5e3..a3d0a8b0a6 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -61,7 +61,7 @@ use crate::model::deploy::{DeploymentListView, DeploymentNewView}; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; -use crate::model::text::fmt::{log_fuzzy_matches, log_text_view}; +use crate::model::text::{log_fuzzy_matches, log_text_view}; use anyhow::{anyhow, bail}; use colored::Colorize; use futures_util::{StreamExt, TryStreamExt, stream}; diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index b4f6630e87..d0479de281 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -36,7 +36,7 @@ use crate::log::{ use crate::model::deploy::log_unified_diff_for_path; use crate::model::help::{AppNewNextStepsHint, AppNewNextStepsMode}; use crate::model::language::GuestLanguage; -use crate::model::text::fmt::log_text_view; +use crate::model::text::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 09e785b1a5..6a8a72dd66 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -48,7 +48,7 @@ use crate::model::environment::{ use crate::model::help::ComponentNameHelp; use crate::model::language::GuestLanguage; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::fmt::log_text_view; +use crate::model::text::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; use futures_util::future::OptionFuture; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index 1e6342d034..682bb5ce81 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -29,7 +29,7 @@ use crate::model::environment::{ }; use crate::model::help::EnvironmentNameHelp; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::fmt::log_text_view; +use crate::model::text::log_text_view; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; use golem_client::model::{EnvironmentCreation, EnvironmentPluginGrantWithDetails}; diff --git a/cli/golem-cli/src/command_handler/log.rs b/cli/golem-cli/src/command_handler/log.rs index 5704cc03dd..cab30af414 100644 --- a/cli/golem-cli/src/command_handler/log.rs +++ b/cli/golem-cli/src/command_handler/log.rs @@ -16,7 +16,7 @@ use crate::context::Context; use crate::model::cli_output::{StructuredOutput, to_structured_output_value_masked}; use crate::model::format::Format; use crate::model::masking::MaskingConfig; -use crate::model::text::fmt::{ +use crate::model::text::{ DecoratedIndent, TextOutput, TruncatableTextOutput, to_colored_json, to_colored_yaml, truncate_rendered, }; diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index 7b431f3b82..e92f1e9c98 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -29,7 +29,7 @@ use crate::model::help::{ AgentNameHelp, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, AvailableProfileNamesHelp, EnvironmentNameHelp, }; -use crate::model::text::fmt::{DecoratedIndent, log_text_view}; +use crate::model::text::{DecoratedIndent, log_text_view}; use colored::Colorize; use indoc::indoc; use std::sync::Arc; diff --git a/cli/golem-cli/src/command_handler/profile/config.rs b/cli/golem-cli/src/command_handler/profile/config.rs index be09bad662..67674ebd9e 100644 --- a/cli/golem-cli/src/command_handler/profile/config.rs +++ b/cli/golem-cli/src/command_handler/profile/config.rs @@ -17,13 +17,13 @@ use crate::command_handler::Handlers; use crate::config::{Config, ProfileName}; use crate::context::Context; use crate::error::NonSuccessfulExit; +use crate::log::log_action; use crate::log::log_error; use crate::log::logln; -use crate::log::log_action; use crate::model::config::profile::ProfileConfigSetFormatResult; use crate::model::format::Format; use crate::model::help::AvailableProfileNamesHelp; -use crate::model::text::fmt::log_text_view; +use crate::model::text::log_text_view; use anyhow::bail; use std::sync::Arc; diff --git a/cli/golem-cli/src/error.rs b/cli/golem-cli/src/error.rs index 649aa6de6e..322da62280 100644 --- a/cli/golem-cli/src/error.rs +++ b/cli/golem-cli/src/error.rs @@ -71,7 +71,7 @@ impl Error for ContextInitHintError {} pub mod service { use crate::log::LogColorize; - use crate::model::text::fmt::{format_error, format_stderr}; + use crate::model::text::{format_error, format_stderr}; use bytes::Bytes; use colored::Colorize; use golem_common::base_model::api; diff --git a/cli/golem-cli/src/model/account.rs b/cli/golem-cli/src/model/account.rs index dce9ef256a..fb2fed65e0 100644 --- a/cli/golem-cli/src/model/account.rs +++ b/cli/golem-cli/src/model/account.rs @@ -15,7 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::grant::{format_grants, grant_count}; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_client::model::{Account, PermissionShare}; use golem_common::model::account::AccountId; use golem_common::model::permission_share::PermissionShareId; diff --git a/cli/golem-cli/src/model/agent/action_result.rs b/cli/golem-cli/src/model/agent/action_result.rs index fa024aea04..6fc4a710c6 100644 --- a/cli/golem-cli/src/model/agent/action_result.rs +++ b/cli/golem-cli/src/model/agent/action_result.rs @@ -24,7 +24,7 @@ use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::{NoTextOutput, TextOutput}; +use crate::model::text::{NoTextOutput, TextOutput}; use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; diff --git a/cli/golem-cli/src/model/agent/extraction.rs b/cli/golem-cli/src/model/agent/extraction.rs index 6a0aa9ea72..eebdbb1c55 100644 --- a/cli/golem-cli/src/model/agent/extraction.rs +++ b/cli/golem-cli/src/model/agent/extraction.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::text::fmt::format_stderr; +use crate::model::text::format_stderr; use anyhow::anyhow; use golem_common::model::agent::extraction::ExtractedComponentMetadata; use itertools::Itertools; diff --git a/cli/golem-cli/src/model/agent/files.rs b/cli/golem-cli/src/model/agent/files.rs index 3436695935..611f963bff 100644 --- a/cli/golem-cli/src/model/agent/files.rs +++ b/cli/golem-cli/src/model/agent/files.rs @@ -14,7 +14,7 @@ use crate::log::logln; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::*; +use crate::model::text::*; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index 395f959b4d..59b1a65bfc 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -27,7 +27,7 @@ use crate::model::environment::{ EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, }; use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; -use crate::model::text::fmt::*; +use crate::model::text::*; use chrono::DateTime; use clap::ValueEnum; use colored::Colorize; diff --git a/cli/golem-cli/src/model/agent/oplog.rs b/cli/golem-cli/src/model/agent/oplog.rs index 9c01923452..2c519e8ab8 100644 --- a/cli/golem-cli/src/model/agent/oplog.rs +++ b/cli/golem-cli/src/model/agent/oplog.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::SourceLanguage; use crate::log::logln; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::*; +use crate::model::text::*; use base64::Engine; use base64::prelude::BASE64_STANDARD; use golem_common::model::Timestamp; diff --git a/cli/golem-cli/src/model/agent/stream.rs b/cli/golem-cli/src/model/agent/stream.rs index 44b345d159..a804ecf5c2 100644 --- a/cli/golem-cli/src/model/agent/stream.rs +++ b/cli/golem-cli/src/model/agent/stream.rs @@ -14,7 +14,7 @@ use crate::model::agent::AgentLogStreamOptions; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::format_stderr; +use crate::model::text::format_stderr; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 458733d594..c1bd9418b2 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -31,7 +31,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; use crate::model::template_render::TemplateRender; -use crate::model::text::fmt::{NoTextOutput, TextOutput}; +use crate::model::text::{NoTextOutput, TextOutput}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{Context, anyhow}; use golem_common::model::agent::AgentTypeName; diff --git a/cli/golem-cli/src/model/card.rs b/cli/golem-cli/src/model/card.rs index 648d152c7a..54ede44f16 100644 --- a/cli/golem-cli/src/model/card.rs +++ b/cli/golem-cli/src/model/card.rs @@ -15,7 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::grant::format_grants; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_client::model::{CardManagedBy, StoredCard}; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index 7c917e17f8..a61a949e9e 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -24,7 +24,7 @@ use crate::model::masking::{ Masked, MaskingConfig, is_sensitive_key, mask_secret, mask_sensitive_map, mask_typed_agent_config_entries, }; -use crate::model::text::fmt::*; +use crate::model::text::*; use chrono::{DateTime, Utc}; use colored::Colorize; use colored::control::SHOULD_COLORIZE; diff --git a/cli/golem-cli/src/model/config/profile.rs b/cli/golem-cli/src/model/config/profile.rs index d109b417cf..4c2e84d804 100644 --- a/cli/golem-cli/src/model/config/profile.rs +++ b/cli/golem-cli/src/model/config/profile.rs @@ -19,7 +19,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::config::ProfileView; use crate::model::format::Format; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use colored::Colorize; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index 0237365c79..aa8ee96ddd 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -26,7 +26,7 @@ use crate::model::masking::{ Masked, MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, }; -use crate::model::text::fmt::{ +use crate::model::text::{ Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/environment.rs b/cli/golem-cli/src/model/environment.rs index c4d612fe9b..7ff0da3782 100644 --- a/cli/golem-cli/src/model/environment.rs +++ b/cli/golem-cli/src/model/environment.rs @@ -17,7 +17,7 @@ use crate::log::log_warn; use crate::log::{LogColorize, logln}; use crate::model::app_raw::Environment; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::*; +use crate::model::text::*; use anyhow::bail; use golem_common::model::account::AccountId; use golem_common::model::application::{ApplicationId, ApplicationName}; diff --git a/cli/golem-cli/src/model/help.rs b/cli/golem-cli/src/model/help.rs index 25eabd8fde..3acd8e1acc 100644 --- a/cli/golem-cli/src/model/help.rs +++ b/cli/golem-cli/src/model/help.rs @@ -17,7 +17,7 @@ use crate::config::{Config, ProfileName}; use crate::log::{LogColorize, LogIndent, logln}; use crate::model::component::show_exported_agent_constructors; use crate::model::masking::Masked; -use crate::model::text::fmt::{ +use crate::model::text::{ Column, FieldsBuilder, MessageWithFields, MessageWithFieldsIndentMode, TextOutput, format_export, log_table, new_table_full, }; diff --git a/cli/golem-cli/src/model/http_api/deployment.rs b/cli/golem-cli/src/model/http_api/deployment.rs index cfef53922a..fbe6fb7258 100644 --- a/cli/golem-cli/src/model/http_api/deployment.rs +++ b/cli/golem-cli/src/model/http_api/deployment.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::{ +use crate::model::text::{ Column, FieldsBuilder, MessageWithFields, TextOutput, format_main_id, format_message_highlight, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/http_api/domain.rs b/cli/golem-cli/src/model/http_api/domain.rs index bbc6f41207..fa4a838619 100644 --- a/cli/golem-cli/src/model/http_api/domain.rs +++ b/cli/golem-cli/src/model/http_api/domain.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_common::model::domain_registration::{Domain, DomainRegistration, DomainRegistrationId}; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/http_api/security.rs b/cli/golem-cli/src/model/http_api/security.rs index bf8401a49c..6377c10f1b 100644 --- a/cli/golem-cli/src/model/http_api/security.rs +++ b/cli/golem-cli/src/model/http_api/security.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_client::model::SecuritySchemeDto; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/invoke_result_view.rs b/cli/golem-cli/src/model/invoke_result_view.rs index f92b772370..98a96b2d63 100644 --- a/cli/golem-cli/src/model/invoke_result_view.rs +++ b/cli/golem-cli/src/model/invoke_result_view.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::{SourceLanguage, render_typed_schema_value}; use crate::log::{log_error, logln}; use crate::model::cli_output::StructuredOutput; -use crate::model::text::fmt::{TextOutput, format_message_highlight, format_warn}; +use crate::model::text::{TextOutput, format_message_highlight, format_warn}; use anyhow::anyhow; use golem_client::model::AgentInvocationResult; use golem_common::model::IdempotencyKey; diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index 77626c093b..15552a165d 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::{ +use crate::model::text::{ Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, format_message_highlight, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/resource_definition.rs b/cli/golem-cli/src/model/resource_definition.rs index 717077fea4..fa02e4d9a7 100644 --- a/cli/golem-cli/src/model/resource_definition.rs +++ b/cli/golem-cli/src/model/resource_definition.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_common::model::quota::ResourceDefinition; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/retry_policy.rs b/cli/golem-cli/src/model/retry_policy.rs index 64faf2561b..e7ccd133d4 100644 --- a/cli/golem-cli/src/model/retry_policy.rs +++ b/cli/golem-cli/src/model/retry_policy.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use golem_common::model::retry_policy::RetryPolicyDto; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/secret.rs b/cli/golem-cli/src/model/secret.rs index 69f191513b..8cc31a1bf3 100644 --- a/cli/golem-cli/src/model/secret.rs +++ b/cli/golem-cli/src/model/secret.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::model::cli_output::StructuredOutput; use crate::model::masking::{Masked, MaskingConfig, mask_json_secret_value}; -use crate::model::text::fmt::*; +use crate::model::text::*; use comfy_table::Cell; use golem_common::model::agent_secret::AgentSecretDto; diff --git a/cli/golem-cli/src/model/text/fmt.rs b/cli/golem-cli/src/model/text.rs similarity index 100% rename from cli/golem-cli/src/model/text/fmt.rs rename to cli/golem-cli/src/model/text.rs diff --git a/cli/golem-cli/src/model/text/mod.rs b/cli/golem-cli/src/model/text/mod.rs deleted file mode 100644 index 3b4a85294d..0000000000 --- a/cli/golem-cli/src/model/text/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2024-2026 Golem Cloud -// -// Licensed under the Golem Source License v1.1 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://license.golem.cloud/LICENSE -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -pub mod fmt; diff --git a/cli/golem-cli/src/model/token.rs b/cli/golem-cli/src/model/token.rs index 981cd3de69..f28f428a32 100644 --- a/cli/golem-cli/src/model/token.rs +++ b/cli/golem-cli/src/model/token.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::fmt::*; +use crate::model::text::*; use colored::Colorize; use golem_client::model::{Token, TokenWithSecret}; From 42988e89b8a9b9b78d50d78029d0729d22d6f950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 4 Aug 2026 16:03:40 +0200 Subject: [PATCH 62/70] rename model/text to model/text_format --- cli/golem-cli/src/app/build/gen_bridge.rs | 2 +- cli/golem-cli/src/app/context.rs | 2 +- cli/golem-cli/src/app/template/list_view.rs | 2 +- cli/golem-cli/src/command_handler/agent/mod.rs | 2 +- cli/golem-cli/src/command_handler/app/mod.rs | 2 +- cli/golem-cli/src/command_handler/app/template.rs | 2 +- cli/golem-cli/src/command_handler/component/mod.rs | 2 +- cli/golem-cli/src/command_handler/environment.rs | 2 +- cli/golem-cli/src/command_handler/log.rs | 2 +- cli/golem-cli/src/command_handler/partial_match.rs | 2 +- cli/golem-cli/src/command_handler/profile/config.rs | 2 +- cli/golem-cli/src/error.rs | 2 +- cli/golem-cli/src/model/account.rs | 2 +- cli/golem-cli/src/model/agent/action_result.rs | 2 +- cli/golem-cli/src/model/agent/extraction.rs | 2 +- cli/golem-cli/src/model/agent/files.rs | 2 +- cli/golem-cli/src/model/agent/mod.rs | 2 +- cli/golem-cli/src/model/agent/oplog.rs | 2 +- cli/golem-cli/src/model/agent/stream.rs | 2 +- cli/golem-cli/src/model/app.rs | 2 +- cli/golem-cli/src/model/card.rs | 2 +- cli/golem-cli/src/model/component.rs | 2 +- cli/golem-cli/src/model/config/profile.rs | 2 +- cli/golem-cli/src/model/deploy.rs | 2 +- cli/golem-cli/src/model/environment.rs | 2 +- cli/golem-cli/src/model/help.rs | 2 +- cli/golem-cli/src/model/http_api/deployment.rs | 2 +- cli/golem-cli/src/model/http_api/domain.rs | 2 +- cli/golem-cli/src/model/http_api/security.rs | 2 +- cli/golem-cli/src/model/invoke_result_view.rs | 2 +- cli/golem-cli/src/model/mod.rs | 2 +- cli/golem-cli/src/model/plugin.rs | 2 +- cli/golem-cli/src/model/resource_definition.rs | 2 +- cli/golem-cli/src/model/retry_policy.rs | 2 +- cli/golem-cli/src/model/secret.rs | 2 +- cli/golem-cli/src/model/{text.rs => text_format.rs} | 0 cli/golem-cli/src/model/token.rs | 2 +- 37 files changed, 36 insertions(+), 36 deletions(-) rename cli/golem-cli/src/model/{text.rs => text_format.rs} (100%) diff --git a/cli/golem-cli/src/app/build/gen_bridge.rs b/cli/golem-cli/src/app/build/gen_bridge.rs index 5c2af7addc..ad1dcef9b9 100644 --- a/cli/golem-cli/src/app/build/gen_bridge.rs +++ b/cli/golem-cli/src/app/build/gen_bridge.rs @@ -23,7 +23,7 @@ use crate::model::app::{ use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::{ReplAgentMetadata, ReplMetadata}; -use crate::model::text::{NoTextOutput, TextOutput}; +use crate::model::text_format::{NoTextOutput, TextOutput}; use anyhow::bail; use camino::Utf8PathBuf; use golem_common::model::component::ComponentName; diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index 57ad0ceadd..055db85c60 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -35,7 +35,7 @@ use crate::model::config::server::ToFormattedServerContext; use crate::model::deploy::log_unified_diff_for_path; use crate::model::format::Format; use crate::model::language::GuestLanguage; -use crate::model::text::DecoratedIndent; +use crate::model::text_format::DecoratedIndent; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/app/template/list_view.rs b/cli/golem-cli/src/app/template/list_view.rs index fe36b8ca4e..c24fafebad 100644 --- a/cli/golem-cli/src/app/template/list_view.rs +++ b/cli/golem-cli/src/app/template/list_view.rs @@ -15,7 +15,7 @@ use crate::app::template::TemplateDescription; use crate::log::current_indent_width; use crate::model::cli_output::StructuredOutput; -use crate::model::text::*; +use crate::model::text_format::*; use itertools::Itertools; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index cc5668032a..3ed58055ec 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -43,7 +43,7 @@ use crate::model::help::{ ParameterErrorTableView, }; use crate::model::invoke_result_view::InvokeResultView; -use crate::model::text::{log_fuzzy_match, log_text_view}; +use crate::model::text_format::{log_fuzzy_match, log_text_view}; use anyhow::{Context as AnyhowContext, anyhow, bail}; use chrono::{DateTime, Utc}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/app/mod.rs b/cli/golem-cli/src/command_handler/app/mod.rs index a3d0a8b0a6..881c27ec94 100644 --- a/cli/golem-cli/src/command_handler/app/mod.rs +++ b/cli/golem-cli/src/command_handler/app/mod.rs @@ -61,7 +61,7 @@ use crate::model::deploy::{DeploymentListView, DeploymentNewView}; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; use crate::model::help::AvailableComponentNamesHelp; use crate::model::language::GuestLanguage; -use crate::model::text::{log_fuzzy_matches, log_text_view}; +use crate::model::text_format::{log_fuzzy_matches, log_text_view}; use anyhow::{anyhow, bail}; use colored::Colorize; use futures_util::{StreamExt, TryStreamExt, stream}; diff --git a/cli/golem-cli/src/command_handler/app/template.rs b/cli/golem-cli/src/command_handler/app/template.rs index d0479de281..63267eaac5 100644 --- a/cli/golem-cli/src/command_handler/app/template.rs +++ b/cli/golem-cli/src/command_handler/app/template.rs @@ -36,7 +36,7 @@ use crate::log::{ use crate::model::deploy::log_unified_diff_for_path; use crate::model::help::{AppNewNextStepsHint, AppNewNextStepsMode}; use crate::model::language::GuestLanguage; -use crate::model::text::log_text_view; +use crate::model::text_format::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{anyhow, bail}; use colored::Colorize; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 6a8a72dd66..f0195695c0 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -48,7 +48,7 @@ use crate::model::environment::{ use crate::model::help::ComponentNameHelp; use crate::model::language::GuestLanguage; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::log_text_view; +use crate::model::text_format::log_text_view; use crate::validation::ValidationBuilder; use anyhow::{Context as AnyhowContext, anyhow, bail}; use futures_util::future::OptionFuture; diff --git a/cli/golem-cli/src/command_handler/environment.rs b/cli/golem-cli/src/command_handler/environment.rs index 682bb5ce81..a12c4d449e 100644 --- a/cli/golem-cli/src/command_handler/environment.rs +++ b/cli/golem-cli/src/command_handler/environment.rs @@ -29,7 +29,7 @@ use crate::model::environment::{ }; use crate::model::help::EnvironmentNameHelp; use crate::model::plugin::PluginNameAndVersion; -use crate::model::text::log_text_view; +use crate::model::text_format::log_text_view; use anyhow::{anyhow, bail}; use golem_client::api::{EnvironmentClient, MeClient}; use golem_client::model::{EnvironmentCreation, EnvironmentPluginGrantWithDetails}; diff --git a/cli/golem-cli/src/command_handler/log.rs b/cli/golem-cli/src/command_handler/log.rs index cab30af414..d14bf1507d 100644 --- a/cli/golem-cli/src/command_handler/log.rs +++ b/cli/golem-cli/src/command_handler/log.rs @@ -16,7 +16,7 @@ use crate::context::Context; use crate::model::cli_output::{StructuredOutput, to_structured_output_value_masked}; use crate::model::format::Format; use crate::model::masking::MaskingConfig; -use crate::model::text::{ +use crate::model::text_format::{ DecoratedIndent, TextOutput, TruncatableTextOutput, to_colored_json, to_colored_yaml, truncate_rendered, }; diff --git a/cli/golem-cli/src/command_handler/partial_match.rs b/cli/golem-cli/src/command_handler/partial_match.rs index e92f1e9c98..16a6a33760 100644 --- a/cli/golem-cli/src/command_handler/partial_match.rs +++ b/cli/golem-cli/src/command_handler/partial_match.rs @@ -29,7 +29,7 @@ use crate::model::help::{ AgentNameHelp, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, AvailableProfileNamesHelp, EnvironmentNameHelp, }; -use crate::model::text::{DecoratedIndent, log_text_view}; +use crate::model::text_format::{DecoratedIndent, log_text_view}; use colored::Colorize; use indoc::indoc; use std::sync::Arc; diff --git a/cli/golem-cli/src/command_handler/profile/config.rs b/cli/golem-cli/src/command_handler/profile/config.rs index 67674ebd9e..4026fbd5dd 100644 --- a/cli/golem-cli/src/command_handler/profile/config.rs +++ b/cli/golem-cli/src/command_handler/profile/config.rs @@ -23,7 +23,7 @@ use crate::log::logln; use crate::model::config::profile::ProfileConfigSetFormatResult; use crate::model::format::Format; use crate::model::help::AvailableProfileNamesHelp; -use crate::model::text::log_text_view; +use crate::model::text_format::log_text_view; use anyhow::bail; use std::sync::Arc; diff --git a/cli/golem-cli/src/error.rs b/cli/golem-cli/src/error.rs index 322da62280..6d380d6a5d 100644 --- a/cli/golem-cli/src/error.rs +++ b/cli/golem-cli/src/error.rs @@ -71,7 +71,7 @@ impl Error for ContextInitHintError {} pub mod service { use crate::log::LogColorize; - use crate::model::text::{format_error, format_stderr}; + use crate::model::text_format::{format_error, format_stderr}; use bytes::Bytes; use colored::Colorize; use golem_common::base_model::api; diff --git a/cli/golem-cli/src/model/account.rs b/cli/golem-cli/src/model/account.rs index fb2fed65e0..7b70dad7f3 100644 --- a/cli/golem-cli/src/model/account.rs +++ b/cli/golem-cli/src/model/account.rs @@ -15,7 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::grant::{format_grants, grant_count}; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_client::model::{Account, PermissionShare}; use golem_common::model::account::AccountId; use golem_common::model::permission_share::PermissionShareId; diff --git a/cli/golem-cli/src/model/agent/action_result.rs b/cli/golem-cli/src/model/agent/action_result.rs index 6fc4a710c6..6511f7b9da 100644 --- a/cli/golem-cli/src/model/agent/action_result.rs +++ b/cli/golem-cli/src/model/agent/action_result.rs @@ -24,7 +24,7 @@ use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; -use crate::model::text::{NoTextOutput, TextOutput}; +use crate::model::text_format::{NoTextOutput, TextOutput}; use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; diff --git a/cli/golem-cli/src/model/agent/extraction.rs b/cli/golem-cli/src/model/agent/extraction.rs index eebdbb1c55..f527f9436c 100644 --- a/cli/golem-cli/src/model/agent/extraction.rs +++ b/cli/golem-cli/src/model/agent/extraction.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::model::text::format_stderr; +use crate::model::text_format::format_stderr; use anyhow::anyhow; use golem_common::model::agent::extraction::ExtractedComponentMetadata; use itertools::Itertools; diff --git a/cli/golem-cli/src/model/agent/files.rs b/cli/golem-cli/src/model/agent/files.rs index 611f963bff..c47d7e2666 100644 --- a/cli/golem-cli/src/model/agent/files.rs +++ b/cli/golem-cli/src/model/agent/files.rs @@ -14,7 +14,7 @@ use crate::log::logln; use crate::model::cli_output::StructuredOutput; -use crate::model::text::*; +use crate::model::text_format::*; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/cli/golem-cli/src/model/agent/mod.rs b/cli/golem-cli/src/model/agent/mod.rs index 59b1a65bfc..4fc36f229d 100644 --- a/cli/golem-cli/src/model/agent/mod.rs +++ b/cli/golem-cli/src/model/agent/mod.rs @@ -27,7 +27,7 @@ use crate::model::environment::{ EnvironmentReference, ResolvedEnvironmentIdentity, ResolvedEnvironmentIdentitySource, }; use crate::model::masking::{Masked, MaskingConfig, mask_agent_config_entries, mask_sensitive_map}; -use crate::model::text::*; +use crate::model::text_format::*; use chrono::DateTime; use clap::ValueEnum; use colored::Colorize; diff --git a/cli/golem-cli/src/model/agent/oplog.rs b/cli/golem-cli/src/model/agent/oplog.rs index 2c519e8ab8..459d29eefd 100644 --- a/cli/golem-cli/src/model/agent/oplog.rs +++ b/cli/golem-cli/src/model/agent/oplog.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::SourceLanguage; use crate::log::logln; use crate::model::cli_output::StructuredOutput; -use crate::model::text::*; +use crate::model::text_format::*; use base64::Engine; use base64::prelude::BASE64_STANDARD; use golem_common::model::Timestamp; diff --git a/cli/golem-cli/src/model/agent/stream.rs b/cli/golem-cli/src/model/agent/stream.rs index a804ecf5c2..b935a5b0bd 100644 --- a/cli/golem-cli/src/model/agent/stream.rs +++ b/cli/golem-cli/src/model/agent/stream.rs @@ -14,7 +14,7 @@ use crate::model::agent::AgentLogStreamOptions; use crate::model::cli_output::StructuredOutput; -use crate::model::text::format_stderr; +use crate::model::text_format::format_stderr; use golem_common::model::{IdempotencyKey, LogLevel, Timestamp}; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index c1bd9418b2..4fd858d6c7 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -31,7 +31,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::language::GuestLanguage; use crate::model::repl::ReplLanguage; use crate::model::template_render::TemplateRender; -use crate::model::text::{NoTextOutput, TextOutput}; +use crate::model::text_format::{NoTextOutput, TextOutput}; use crate::validation::{ValidatedResult, ValidationBuilder}; use anyhow::{Context, anyhow}; use golem_common::model::agent::AgentTypeName; diff --git a/cli/golem-cli/src/model/card.rs b/cli/golem-cli/src/model/card.rs index 54ede44f16..ad40c467fd 100644 --- a/cli/golem-cli/src/model/card.rs +++ b/cli/golem-cli/src/model/card.rs @@ -15,7 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::grant::format_grants; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_client::model::{CardManagedBy, StoredCard}; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/cli/golem-cli/src/model/component.rs b/cli/golem-cli/src/model/component.rs index a61a949e9e..849018bd5e 100644 --- a/cli/golem-cli/src/model/component.rs +++ b/cli/golem-cli/src/model/component.rs @@ -24,7 +24,7 @@ use crate::model::masking::{ Masked, MaskingConfig, is_sensitive_key, mask_secret, mask_sensitive_map, mask_typed_agent_config_entries, }; -use crate::model::text::*; +use crate::model::text_format::*; use chrono::{DateTime, Utc}; use colored::Colorize; use colored::control::SHOULD_COLORIZE; diff --git a/cli/golem-cli/src/model/config/profile.rs b/cli/golem-cli/src/model/config/profile.rs index 4c2e84d804..0290189955 100644 --- a/cli/golem-cli/src/model/config/profile.rs +++ b/cli/golem-cli/src/model/config/profile.rs @@ -19,7 +19,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::config::ProfileView; use crate::model::format::Format; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use colored::Colorize; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index aa8ee96ddd..d7ac461767 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -26,7 +26,7 @@ use crate::model::masking::{ Masked, MaskingConfig, is_sensitive_key, mask_json_secret_for_deploy_diff, mask_secret_with_fingerprint, }; -use crate::model::text::{ +use crate::model::text_format::{ Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/environment.rs b/cli/golem-cli/src/model/environment.rs index 7ff0da3782..7518d1b8d0 100644 --- a/cli/golem-cli/src/model/environment.rs +++ b/cli/golem-cli/src/model/environment.rs @@ -17,7 +17,7 @@ use crate::log::log_warn; use crate::log::{LogColorize, logln}; use crate::model::app_raw::Environment; use crate::model::cli_output::StructuredOutput; -use crate::model::text::*; +use crate::model::text_format::*; use anyhow::bail; use golem_common::model::account::AccountId; use golem_common::model::application::{ApplicationId, ApplicationName}; diff --git a/cli/golem-cli/src/model/help.rs b/cli/golem-cli/src/model/help.rs index 3acd8e1acc..3f716e2d00 100644 --- a/cli/golem-cli/src/model/help.rs +++ b/cli/golem-cli/src/model/help.rs @@ -17,7 +17,7 @@ use crate::config::{Config, ProfileName}; use crate::log::{LogColorize, LogIndent, logln}; use crate::model::component::show_exported_agent_constructors; use crate::model::masking::Masked; -use crate::model::text::{ +use crate::model::text_format::{ Column, FieldsBuilder, MessageWithFields, MessageWithFieldsIndentMode, TextOutput, format_export, log_table, new_table_full, }; diff --git a/cli/golem-cli/src/model/http_api/deployment.rs b/cli/golem-cli/src/model/http_api/deployment.rs index fbe6fb7258..9d4c8676ba 100644 --- a/cli/golem-cli/src/model/http_api/deployment.rs +++ b/cli/golem-cli/src/model/http_api/deployment.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::{ +use crate::model::text_format::{ Column, FieldsBuilder, MessageWithFields, TextOutput, format_main_id, format_message_highlight, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/http_api/domain.rs b/cli/golem-cli/src/model/http_api/domain.rs index fa4a838619..815ea0bff1 100644 --- a/cli/golem-cli/src/model/http_api/domain.rs +++ b/cli/golem-cli/src/model/http_api/domain.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_common::model::domain_registration::{Domain, DomainRegistration, DomainRegistrationId}; use serde::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/http_api/security.rs b/cli/golem-cli/src/model/http_api/security.rs index 6377c10f1b..8be74cd9c2 100644 --- a/cli/golem-cli/src/model/http_api/security.rs +++ b/cli/golem-cli/src/model/http_api/security.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_client::model::SecuritySchemeDto; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/invoke_result_view.rs b/cli/golem-cli/src/model/invoke_result_view.rs index 98a96b2d63..bc86bf0611 100644 --- a/cli/golem-cli/src/model/invoke_result_view.rs +++ b/cli/golem-cli/src/model/invoke_result_view.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::{SourceLanguage, render_typed_schema_value}; use crate::log::{log_error, logln}; use crate::model::cli_output::StructuredOutput; -use crate::model::text::{TextOutput, format_message_highlight, format_warn}; +use crate::model::text_format::{TextOutput, format_message_highlight, format_warn}; use anyhow::anyhow; use golem_client::model::AgentInvocationResult; use golem_common::model::IdempotencyKey; diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index 0410ac7062..a57f298299 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -38,5 +38,5 @@ pub mod resource_definition; pub mod retry_policy; pub mod secret; pub mod template_render; -pub mod text; +pub mod text_format; pub mod token; diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index 15552a165d..2f6b28bbb5 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::{ +use crate::model::text_format::{ Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, format_message_highlight, log_table, new_table_full_condensed, }; diff --git a/cli/golem-cli/src/model/resource_definition.rs b/cli/golem-cli/src/model/resource_definition.rs index fa02e4d9a7..b75de1c6c8 100644 --- a/cli/golem-cli/src/model/resource_definition.rs +++ b/cli/golem-cli/src/model/resource_definition.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_common::model::quota::ResourceDefinition; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/retry_policy.rs b/cli/golem-cli/src/model/retry_policy.rs index e7ccd133d4..13db08c73e 100644 --- a/cli/golem-cli/src/model/retry_policy.rs +++ b/cli/golem-cli/src/model/retry_policy.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use golem_common::model::retry_policy::RetryPolicyDto; use serde_derive::{Deserialize, Serialize}; diff --git a/cli/golem-cli/src/model/secret.rs b/cli/golem-cli/src/model/secret.rs index 8cc31a1bf3..be03063827 100644 --- a/cli/golem-cli/src/model/secret.rs +++ b/cli/golem-cli/src/model/secret.rs @@ -15,7 +15,7 @@ use crate::agent_id_display::{SourceLanguage, render_type_for_language}; use crate::model::cli_output::StructuredOutput; use crate::model::masking::{Masked, MaskingConfig, mask_json_secret_value}; -use crate::model::text::*; +use crate::model::text_format::*; use comfy_table::Cell; use golem_common::model::agent_secret::AgentSecretDto; diff --git a/cli/golem-cli/src/model/text.rs b/cli/golem-cli/src/model/text_format.rs similarity index 100% rename from cli/golem-cli/src/model/text.rs rename to cli/golem-cli/src/model/text_format.rs diff --git a/cli/golem-cli/src/model/token.rs b/cli/golem-cli/src/model/token.rs index f28f428a32..5150a0c33b 100644 --- a/cli/golem-cli/src/model/token.rs +++ b/cli/golem-cli/src/model/token.rs @@ -14,7 +14,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; -use crate::model::text::*; +use crate::model::text_format::*; use colored::Colorize; use golem_client::model::{Token, TokenWithSecret}; From c97b7476728318d5e5eb1fbb604b043674a76f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 4 Aug 2026 16:21:08 +0200 Subject: [PATCH 63/70] restore license header on template_render.rs --- cli/golem-cli/src/model/template_render.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cli/golem-cli/src/model/template_render.rs b/cli/golem-cli/src/model/template_render.rs index c6baa07e23..60825f5383 100644 --- a/cli/golem-cli/src/model/template_render.rs +++ b/cli/golem-cli/src/model/template_render.rs @@ -1,3 +1,17 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use crate::model::app_raw; use indexmap::IndexMap; use minijinja::{Environment, Error}; From 80bb87d7599468580fe1345b7f824bffef16f68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Tue, 4 Aug 2026 17:01:16 +0200 Subject: [PATCH 64/70] rename *Result table views to *View --- .../command-output-schema/command-output.schema.json | 4 ++-- cli/golem-cli/src/command_handler/agent/mod.rs | 10 +++++----- cli/golem-cli/src/command_handler/card.rs | 4 ++-- cli/golem-cli/src/command_handler/component/mod.rs | 5 ++--- cli/golem-cli/src/model/card.rs | 6 +++--- cli/golem-cli/src/model/cli_output/tests.rs | 11 ++++------- cli/golem-cli/src/model/deploy.rs | 10 +++++----- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 97a158e653..cca23bb360 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -6712,7 +6712,7 @@ }, { "type": "card.revoke", - "rustType": "CardRevokeResult" + "rustType": "CardRevokeView" }, { "type": "agent-type.get", @@ -6792,7 +6792,7 @@ }, { "type": "agent.update", - "rustType": "TryUpdateAllWorkersResult" + "rustType": "TryUpdateAllWorkersView" }, { "type": "api-token.delete", diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index 3ed58055ec..ec03b1bae8 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -37,7 +37,7 @@ use crate::model::agent::files::{AgentFilesView, FileNodeView}; use crate::model::agent::oplog::AgentOplogEntryView; use crate::model::agent::{AgentCreateView, AgentGetView, format_agent_id_match, format_timestamp}; use crate::model::component::ComponentNameMatchKind; -use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersResult}; +use crate::model::deploy::{AgentUpdateMeta, TryUpdateAllWorkersView}; use crate::model::help::{ AgentNameHelp, ArgumentError, AvailableAgentConstructorsHelp, AvailableFunctionNamesHelp, ParameterErrorTableView, @@ -1287,7 +1287,7 @@ impl AgentCommandHandler { version: component.metadata.root_package_version().clone(), }; - let mut update_results = TryUpdateAllWorkersResult::default(); + let mut update_results = TryUpdateAllWorkersView::default(); update_results.agents.push(meta); match self .update_agent( @@ -1786,7 +1786,7 @@ impl AgentCommandHandler { target_revision: ComponentRevision, await_update: bool, disable_wakeup: bool, - ) -> anyhow::Result { + ) -> anyhow::Result { let agent_filters = [ // only consider durable agents AgentFilter::new_mode(FilterComparator::Equal, AgentMode::Durable).to_string(), @@ -1806,7 +1806,7 @@ impl AgentCommandHandler { .await?; if agents_to_update.is_empty() { - return Ok(TryUpdateAllWorkersResult::default()); + return Ok(TryUpdateAllWorkersView::default()); } log_action( @@ -1825,7 +1825,7 @@ impl AgentCommandHandler { .component_handler() .component_version_at(component_id, target_revision) .await; - let mut update_results = TryUpdateAllWorkersResult::default(); + let mut update_results = TryUpdateAllWorkersView::default(); for agent in &agents_to_update { let result = self .update_agent( diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 856d6d5182..99f740fccc 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -20,7 +20,7 @@ use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; use crate::log::log_warn_action; use crate::model::agent::RawAgentId; -use crate::model::card::{CardGetView, CardListView, CardRevokeResult}; +use crate::model::card::{CardGetView, CardListView, CardRevokeView}; use anyhow::bail; use golem_client::api::{CardClient, WorkerClient}; use golem_common::model::account::AccountId; @@ -195,7 +195,7 @@ impl CardCommandHandler { log_warn_action("Revoked", "card"); - self.ctx.log_handler().log_output(CardRevokeResult { + self.ctx.log_handler().log_output(CardRevokeView { revoked_card_ids: response.revoked_card_ids, })?; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index f0195695c0..144dcc3ed4 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -39,8 +39,7 @@ use crate::model::component::{ use crate::model::component::{ComponentGetView, ComponentListView, ComponentManifestTraceView}; use crate::model::config::{collect_unused_leaf_paths, value_at_path}; use crate::model::deploy::{ - DeployConfig, TryUpdateAllWorkersResult, UpdateStagedComponentError, - UpdateStagedComponentResult, + DeployConfig, TryUpdateAllWorkersView, UpdateStagedComponentError, UpdateStagedComponentResult, }; use crate::model::environment::{ EnvironmentReference, EnvironmentResolveMode, ResolvedEnvironmentIdentity, @@ -360,7 +359,7 @@ impl ComponentCommandHandler { log_action("Updating", format!("existing agents using {update} mode")); let _indent = LogIndent::new(); - let mut update_results = TryUpdateAllWorkersResult::default(); + let mut update_results = TryUpdateAllWorkersView::default(); for component in components { let result = self .ctx diff --git a/cli/golem-cli/src/model/card.rs b/cli/golem-cli/src/model/card.rs index ad40c467fd..06a701d4a5 100644 --- a/cli/golem-cli/src/model/card.rs +++ b/cli/golem-cli/src/model/card.rs @@ -79,11 +79,11 @@ impl StructuredOutput for CardListView { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CardRevokeResult { +pub struct CardRevokeView { pub revoked_card_ids: Vec, } -impl TextOutput for CardRevokeResult { +impl TextOutput for CardRevokeView { fn log(&self) { let mut table = new_table_full_condensed(vec![Column::new("Revoked card ID")]); @@ -95,7 +95,7 @@ impl TextOutput for CardRevokeResult { } } -impl StructuredOutput for CardRevokeResult { +impl StructuredOutput for CardRevokeView { const KIND: &'static str = "card.revoke"; } diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 34181338b6..910048acdd 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -83,7 +83,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ ), registry_entry!("CardGetView", "card.get", arb_card_get_result), registry_entry!("CardListView", "card.list", arb_card_list_result), - registry_entry!("CardRevokeResult", "card.revoke", arb_card_revoke_result), + registry_entry!("CardRevokeView", "card.revoke", arb_card_revoke_result), registry_entry!("AgentTypeView", "agent-type.get", arb_agent_type_get_result), registry_entry!( "AgentTypeListView", @@ -140,7 +140,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ ), registry_entry!("AgentStreamEvent", "agent.stream", arb_agent_stream_event), registry_entry!( - "TryUpdateAllWorkersResult", + "TryUpdateAllWorkersView", "agent.update", arb_agent_update_result ), @@ -2748,10 +2748,7 @@ fn arb_agent_update_result() -> OutputDocumentStrategy { proptest::collection::btree_map(arb_small_string(), arb_small_string(), 0..3), ) .prop_map( - |(agents, errors)| crate::model::deploy::TryUpdateAllWorkersResult { - agents, - errors, - }, + |(agents, errors)| crate::model::deploy::TryUpdateAllWorkersView { agents, errors }, ), ) } @@ -3225,7 +3222,7 @@ fn arb_card_list_result() -> OutputDocumentStrategy { fn arb_card_revoke_result() -> OutputDocumentStrategy { serialized_output( proptest::collection::vec(arb_uuid(), 0..5) - .prop_map(|revoked_card_ids| crate::model::card::CardRevokeResult { revoked_card_ids }), + .prop_map(|revoked_card_ids| crate::model::card::CardRevokeView { revoked_card_ids }), ) } diff --git a/cli/golem-cli/src/model/deploy.rs b/cli/golem-cli/src/model/deploy.rs index d7ac461767..3edf2c162a 100644 --- a/cli/golem-cli/src/model/deploy.rs +++ b/cli/golem-cli/src/model/deploy.rs @@ -1304,24 +1304,24 @@ fn mask_sensitive_key_value_for_deploy_diff( #[derive(Clone, Default, PartialEq, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TryUpdateAllWorkersResult { +pub struct TryUpdateAllWorkersView { pub agents: Vec, /// Per-agent update errors, keyed by the (environment-unique) agent id. pub errors: BTreeMap, } -impl TryUpdateAllWorkersResult { - pub fn extend(&mut self, other: TryUpdateAllWorkersResult) { +impl TryUpdateAllWorkersView { + pub fn extend(&mut self, other: TryUpdateAllWorkersView) { self.agents.extend(other.agents); self.errors.extend(other.errors); } } -impl StructuredOutput for TryUpdateAllWorkersResult { +impl StructuredOutput for TryUpdateAllWorkersView { const KIND: &'static str = "agent.update"; } -impl TextOutput for TryUpdateAllWorkersResult { +impl TextOutput for TryUpdateAllWorkersView { fn log(&self) { // NOP } From c63fd913af082cc8fae9b1b9d9c5a8ae2104160c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 5 Aug 2026 14:17:19 +0200 Subject: [PATCH 65/70] move mcp deployment model to model/mcp --- .../src/command_handler/api/deployment.rs | 10 +++--- .../src/command_handler/app/deploy_diff.rs | 3 +- cli/golem-cli/src/model/app.rs | 8 ++--- cli/golem-cli/src/model/http_api/mod.rs | 18 ---------- cli/golem-cli/src/model/mcp.rs | 34 +++++++++++++++++++ cli/golem-cli/src/model/mod.rs | 1 + 6 files changed, 45 insertions(+), 29 deletions(-) create mode 100644 cli/golem-cli/src/model/mcp.rs diff --git a/cli/golem-cli/src/command_handler/api/deployment.rs b/cli/golem-cli/src/command_handler/api/deployment.rs index a967da0eb4..7fa6bd04e7 100644 --- a/cli/golem-cli/src/command_handler/api/deployment.rs +++ b/cli/golem-cli/src/command_handler/api/deployment.rs @@ -18,8 +18,9 @@ use crate::context::Context; use crate::error::service::{MapServiceError, ServiceError}; use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; use crate::model::environment::{EnvironmentResolveMode, ResolvedEnvironmentIdentity}; +use crate::model::http_api::HttpApiDeploymentDeployProperties; use crate::model::http_api::deployment::{HttpApiDeploymentGetView, HttpApiDeploymentListView}; -use crate::model::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProperties}; +use crate::model::mcp::McpDeploymentDeployProperties; use anyhow::{anyhow, bail}; use golem_client::api::{ApiDeploymentClient, McpDeploymentClient}; use golem_common::cache::SimpleCache; @@ -164,8 +165,7 @@ impl ApiDeploymentCommandHandler { pub async fn deployable_manifest_mcp_deployments( &self, environment_name: &EnvironmentName, - ) -> anyhow::Result> - { + ) -> anyhow::Result> { let app_ctx = self.ctx.app_context_lock().await; let app_ctx = app_ctx.some_or_err()?; Ok(app_ctx @@ -174,9 +174,7 @@ impl ApiDeploymentCommandHandler { .map( |deployments: &BTreeMap< golem_common::model::domain_registration::Domain, - crate::model::app::WithSource< - crate::model::http_api::McpDeploymentDeployProperties, - >, + crate::model::app::WithSource, >| { deployments .iter() diff --git a/cli/golem-cli/src/command_handler/app/deploy_diff.rs b/cli/golem-cli/src/command_handler/app/deploy_diff.rs index 4a3d473ee8..b30661f330 100644 --- a/cli/golem-cli/src/command_handler/app/deploy_diff.rs +++ b/cli/golem-cli/src/command_handler/app/deploy_diff.rs @@ -17,8 +17,9 @@ use crate::model::deploy::{ DeploymentDisplay, DeploymentDisplayContext, DeploymentDisplayMode, EnvironmentSetupPlan, }; use crate::model::environment::ResolvedEnvironmentIdentity; -use crate::model::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProperties}; +use crate::model::http_api::HttpApiDeploymentDeployProperties; use crate::model::masking::MaskingConfig; +use crate::model::mcp::McpDeploymentDeployProperties; use anyhow::bail; use golem_client::model::{DeploymentPlan, DeploymentSummary}; use golem_common::model::component::{ComponentDto, ComponentName}; diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 4fd858d6c7..a5c14f5d3b 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use super::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProperties}; +use super::http_api::HttpApiDeploymentDeployProperties; +use super::mcp::McpDeploymentDeployProperties; use crate::bridge_gen::{ BridgeMode, bridge_client_directory_name, tool_bridge_client_directory_name, }; @@ -2651,9 +2652,8 @@ mod app_builder { }; use crate::model::app_raw; use crate::model::cascade::store::Store; - use crate::model::http_api::{ - HttpApiDeploymentDeployProperties, McpDeploymentAgentOptions, McpDeploymentDeployProperties, - }; + use crate::model::http_api::HttpApiDeploymentDeployProperties; + use crate::model::mcp::{McpDeploymentAgentOptions, McpDeploymentDeployProperties}; use crate::validation::{ValidatedResult, ValidationBuilder}; use crate::{fs, fuzzy}; use colored::Colorize; diff --git a/cli/golem-cli/src/model/http_api/mod.rs b/cli/golem-cli/src/model/http_api/mod.rs index 1dba98efd6..7b97279c0e 100644 --- a/cli/golem-cli/src/model/http_api/mod.rs +++ b/cli/golem-cli/src/model/http_api/mod.rs @@ -26,21 +26,3 @@ pub struct HttpApiDeploymentDeployProperties { pub openapi_prefix: String, pub agents: BTreeMap, } - -#[derive(Clone, Debug)] -pub struct McpDeploymentDeployProperties { - pub agents: BTreeMap, -} - -#[derive(Clone, Debug)] -pub struct McpDeploymentAgentOptions { - pub security_scheme: Option, -} - -impl McpDeploymentAgentOptions { - pub fn to_diffable(&self) -> golem_common::model::diff::McpDeploymentAgentOptions { - golem_common::model::diff::McpDeploymentAgentOptions { - security_scheme: self.security_scheme.clone(), - } - } -} diff --git a/cli/golem-cli/src/model/mcp.rs b/cli/golem-cli/src/model/mcp.rs new file mode 100644 index 0000000000..d370105334 --- /dev/null +++ b/cli/golem-cli/src/model/mcp.rs @@ -0,0 +1,34 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use golem_common::model::agent::AgentTypeName; +use std::collections::BTreeMap; + +#[derive(Clone, Debug)] +pub struct McpDeploymentDeployProperties { + pub agents: BTreeMap, +} + +#[derive(Clone, Debug)] +pub struct McpDeploymentAgentOptions { + pub security_scheme: Option, +} + +impl McpDeploymentAgentOptions { + pub fn to_diffable(&self) -> golem_common::model::diff::McpDeploymentAgentOptions { + golem_common::model::diff::McpDeploymentAgentOptions { + security_scheme: self.security_scheme.clone(), + } + } +} diff --git a/cli/golem-cli/src/model/mod.rs b/cli/golem-cli/src/model/mod.rs index a57f298299..bae54ffea5 100644 --- a/cli/golem-cli/src/model/mod.rs +++ b/cli/golem-cli/src/model/mod.rs @@ -32,6 +32,7 @@ pub mod input; pub mod invoke_result_view; pub mod language; pub mod masking; +pub mod mcp; pub mod plugin; pub mod repl; pub mod resource_definition; From 414de51107a397fffb06ab2f7d3d14b5d7496187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 5 Aug 2026 14:51:30 +0200 Subject: [PATCH 66/70] render deletion confirmations as views (style-1 deletes) --- .../command-output.schema.json | 12 ++--- cli/golem-cli/src/command_handler/account.rs | 13 ++--- .../src/command_handler/agent/mod.rs | 9 +--- .../src/command_handler/api/domain.rs | 9 +--- .../src/command_handler/api_token.rs | 10 +--- cli/golem-cli/src/command_handler/plugin.rs | 15 ++---- cli/golem-cli/src/model/account.rs | 48 +++++++++++++++---- .../src/model/agent/action_result.rs | 22 +++++++-- cli/golem-cli/src/model/cli_output/tests.rs | 36 +++++++------- cli/golem-cli/src/model/http_api/domain.rs | 24 ++++++++-- cli/golem-cli/src/model/plugin.rs | 27 +++++++++-- cli/golem-cli/src/model/token.rs | 22 +++++++-- 12 files changed, 153 insertions(+), 94 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index cca23bb360..9cd4f491ae 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -6668,7 +6668,7 @@ "x-golem-cli-output-types": [ { "type": "account.delete", - "rustType": "AccountDeleteResult" + "rustType": "AccountDeleteView" }, { "type": "account.get", @@ -6680,7 +6680,7 @@ }, { "type": "account.permission-share.delete", - "rustType": "PermissionShareDeleteResult" + "rustType": "PermissionShareDeleteView" }, { "type": "account.permission-share.get", @@ -6728,7 +6728,7 @@ }, { "type": "agent.delete", - "rustType": "AgentDeleteResult" + "rustType": "AgentDeleteView" }, { "type": "agent.delete-all", @@ -6796,7 +6796,7 @@ }, { "type": "api-token.delete", - "rustType": "TokenDeleteResult" + "rustType": "TokenDeleteView" }, { "type": "api-token.list", @@ -6816,7 +6816,7 @@ }, { "type": "api.domain.delete", - "rustType": "DomainRegistrationDeleteResult" + "rustType": "DomainRegistrationDeleteView" }, { "type": "api.domain.list", @@ -6924,7 +6924,7 @@ }, { "type": "plugin.unregister", - "rustType": "PluginUnregisterResult" + "rustType": "PluginUnregisterView" }, { "type": "profile.config.set-format", diff --git a/cli/golem-cli/src/command_handler/account.rs b/cli/golem-cli/src/command_handler/account.rs index 3c119941f1..77cda36619 100644 --- a/cli/golem-cli/src/command_handler/account.rs +++ b/cli/golem-cli/src/command_handler/account.rs @@ -19,10 +19,9 @@ use crate::command_handler::Handlers; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; -use crate::log::log_warn_action; use crate::model::account::{ - AccountDeleteResult, AccountGetView, AccountNewView, AccountUpdateView, - PermissionShareDeleteResult, PermissionShareGetView, PermissionShareListView, + AccountDeleteView, AccountGetView, AccountNewView, AccountUpdateView, + PermissionShareDeleteView, PermissionShareGetView, PermissionShareListView, PermissionShareNewView, PermissionShareUpdateView, }; use anyhow::bail; @@ -185,9 +184,7 @@ impl AccountCommandHandler { .await .map_service_error()?; - log_warn_action("Deleted", "account"); - - self.ctx.log_handler().log_output(AccountDeleteResult { + self.ctx.log_handler().log_output(AccountDeleteView { deleted: true, account_id: account.id, })?; @@ -337,11 +334,9 @@ impl AccountCommandHandler { .await .map_service_error()?; - log_warn_action("Deleted", "permission share"); - self.ctx .log_handler() - .log_output(PermissionShareDeleteResult { + .log_output(PermissionShareDeleteView { deleted: true, permission_share_id, })?; diff --git a/cli/golem-cli/src/command_handler/agent/mod.rs b/cli/golem-cli/src/command_handler/agent/mod.rs index ec03b1bae8..bf6f1cbc6f 100644 --- a/cli/golem-cli/src/command_handler/agent/mod.rs +++ b/cli/golem-cli/src/command_handler/agent/mod.rs @@ -30,7 +30,7 @@ use crate::log::{ log_warn_action, logln, }; use crate::model::agent::action_result::{ - AgentCancelInvocationResult, AgentDeleteResult, AgentFileContentsResult, AgentInterruptResult, + AgentCancelInvocationResult, AgentDeleteView, AgentFileContentsResult, AgentInterruptResult, AgentPluginToggleResult, AgentResumeResult, AgentRevertResult, AgentSimulateCrashResult, }; use crate::model::agent::files::{AgentFilesView, FileNodeView}; @@ -1374,12 +1374,7 @@ impl AgentCommandHandler { self.delete(component.id.0, &agent_id.0).await?; - log_action( - "Deleted", - format!("agent {}", format_agent_id_match(&agent_id_match)), - ); - - self.ctx.log_handler().log_output(AgentDeleteResult { + self.ctx.log_handler().log_output(AgentDeleteView { deleted: true, agent_id: agent_id.0.clone(), })?; diff --git a/cli/golem-cli/src/command_handler/api/domain.rs b/cli/golem-cli/src/command_handler/api/domain.rs index 2a6d2394f3..56c2777352 100644 --- a/cli/golem-cli/src/command_handler/api/domain.rs +++ b/cli/golem-cli/src/command_handler/api/domain.rs @@ -16,7 +16,7 @@ use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; use crate::model::http_api::domain::{ - DomainRegistrationDeleteResult, DomainRegistrationNewView, HttpApiDomainListView, + DomainRegistrationDeleteView, DomainRegistrationNewView, HttpApiDomainListView, }; use crate::command::api::domain::ApiDomainSubcommand; @@ -159,14 +159,9 @@ impl ApiDomainCommandHandler { environment.text_format() ), ); - log_action( - "Deleted", - format!("domain registration {}", domain.0.log_color_highlight()), - ); - self.ctx .log_handler() - .log_output(DomainRegistrationDeleteResult { + .log_output(DomainRegistrationDeleteView { deleted: true, domain, id: domain_to_delete.id, diff --git a/cli/golem-cli/src/command_handler/api_token.rs b/cli/golem-cli/src/command_handler/api_token.rs index 076691916e..1df16e7397 100644 --- a/cli/golem-cli/src/command_handler/api_token.rs +++ b/cli/golem-cli/src/command_handler/api_token.rs @@ -16,8 +16,7 @@ use crate::command::api_token::ApiTokenSubcommand; use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; -use crate::log::{LogColorize, log_warn_action}; -use crate::model::token::{TokenDeleteResult, TokenListView, TokenNewView}; +use crate::model::token::{TokenDeleteView, TokenListView, TokenNewView}; use chrono::{DateTime, Utc}; use golem_client::api::{AccountClient, TokenClient}; use golem_client::model::TokenCreation; @@ -80,12 +79,7 @@ impl ApiTokenCommandHandler { .await .map_service_error()?; - log_warn_action( - "Deleted", - format!("token {}", token_id.0.to_string().log_color_highlight()), - ); - - self.ctx.log_handler().log_output(TokenDeleteResult { + self.ctx.log_handler().log_output(TokenDeleteView { deleted: true, token_id, })?; diff --git a/cli/golem-cli/src/command_handler/plugin.rs b/cli/golem-cli/src/command_handler/plugin.rs index 10f032082c..a0bc89cf89 100644 --- a/cli/golem-cli/src/command_handler/plugin.rs +++ b/cli/golem-cli/src/command_handler/plugin.rs @@ -16,12 +16,12 @@ use crate::command::plugin::PluginSubcommand; use crate::command_handler::Handlers; use crate::context::Context; use crate::error::service::MapServiceError; -use crate::log::{LogColorize, LogIndent, log_action, log_warn_action}; +use crate::log::{LogColorize, LogIndent, log_action}; use crate::model::environment::EnvironmentResolveMode; use crate::model::input::PathBufOrStdin; use crate::model::plugin::{ PluginListEntry, PluginListView, PluginRegistrationGetView, PluginRegistrationRegisterView, - PluginSource, PluginUnregisterResult, + PluginSource, PluginUnregisterView, }; use crate::model::plugin::{PluginManifest, PluginTypeSpecificManifest}; use anyhow::{Context as AnyhowContext, anyhow}; @@ -189,16 +189,7 @@ impl PluginCommandHandler { .await .map_service_error()?; - log_warn_action( - "Unregistered", - format!( - "plugin: {}/{}", - result.name.log_color_highlight(), - result.version.log_color_highlight() - ), - ); - - self.ctx.log_handler().log_output(PluginUnregisterResult { + self.ctx.log_handler().log_output(PluginUnregisterView { unregistered: true, plugin_id: id, name: result.name, diff --git a/cli/golem-cli/src/model/account.rs b/cli/golem-cli/src/model/account.rs index 7b70dad7f3..51a60ca4ca 100644 --- a/cli/golem-cli/src/model/account.rs +++ b/cli/golem-cli/src/model/account.rs @@ -97,15 +97,29 @@ impl StructuredOutput for AccountUpdateView { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AccountDeleteResult { +pub struct AccountDeleteView { pub deleted: bool, pub account_id: AccountId, } -impl NoTextOutput for AccountDeleteResult {} -impl TextOutput for AccountDeleteResult {} +impl Masked for AccountDeleteView {} -impl StructuredOutput for AccountDeleteResult { +impl MessageWithFields for AccountDeleteView { + fn message(&self) -> String { + format!( + "Deleted account {}", + format_message_highlight(&self.account_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields.fmt_field("Account ID", &self.account_id, format_main_id); + fields.build() + } +} + +impl StructuredOutput for AccountDeleteView { const KIND: &'static str = "account.delete"; } @@ -192,15 +206,33 @@ impl StructuredOutput for PermissionShareUpdateView { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PermissionShareDeleteResult { +pub struct PermissionShareDeleteView { pub deleted: bool, pub permission_share_id: PermissionShareId, } -impl NoTextOutput for PermissionShareDeleteResult {} -impl TextOutput for PermissionShareDeleteResult {} +impl Masked for PermissionShareDeleteView {} + +impl MessageWithFields for PermissionShareDeleteView { + fn message(&self) -> String { + format!( + "Deleted permission share {}", + format_message_highlight(&self.permission_share_id) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields.fmt_field( + "Permission share ID", + &self.permission_share_id, + format_main_id, + ); + fields.build() + } +} -impl StructuredOutput for PermissionShareDeleteResult { +impl StructuredOutput for PermissionShareDeleteView { const KIND: &'static str = "account.permission-share.delete"; } diff --git a/cli/golem-cli/src/model/agent/action_result.rs b/cli/golem-cli/src/model/agent/action_result.rs index 6511f7b9da..a99d717879 100644 --- a/cli/golem-cli/src/model/agent/action_result.rs +++ b/cli/golem-cli/src/model/agent/action_result.rs @@ -24,22 +24,34 @@ use crate::model::agent::RawAgentId; use crate::model::cli_output::StructuredOutput; -use crate::model::text_format::{NoTextOutput, TextOutput}; +use crate::model::masking::Masked; +use crate::model::text_format::*; use golem_common::model::component::{ComponentName, ComponentRevision}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentDeleteResult { +pub struct AgentDeleteView { pub deleted: bool, pub agent_id: String, } -impl NoTextOutput for AgentDeleteResult {} -impl TextOutput for AgentDeleteResult {} +impl Masked for AgentDeleteView {} -impl StructuredOutput for AgentDeleteResult { +impl MessageWithFields for AgentDeleteView { + fn message(&self) -> String { + format!("Deleted agent {}", format_message_highlight(&self.agent_id)) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields.fmt_field("Agent ID", &self.agent_id, format_main_id); + fields.build() + } +} + +impl StructuredOutput for AgentDeleteView { const KIND: &'static str = "agent.delete"; } diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 910048acdd..430058d6ff 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -45,14 +45,14 @@ macro_rules! registry_entry { static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ registry_entry!( - "AccountDeleteResult", + "AccountDeleteView", "account.delete", arb_account_delete_result ), registry_entry!("AccountGetView", "account.get", arb_account_get_result), registry_entry!("AccountNewView", "account.new", arb_account_new_result), registry_entry!( - "PermissionShareDeleteResult", + "PermissionShareDeleteView", "account.permission-share.delete", arb_permission_share_delete_result ), @@ -95,7 +95,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ "agent.cancel-invocation", arb_agent_cancel_invocation_result ), - registry_entry!("AgentDeleteResult", "agent.delete", arb_agent_delete_result), + registry_entry!("AgentDeleteView", "agent.delete", arb_agent_delete_result), registry_entry!( "AgentDeleteAllResult", "agent.delete-all", @@ -145,7 +145,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ arb_agent_update_result ), registry_entry!( - "TokenDeleteResult", + "TokenDeleteView", "api-token.delete", arb_token_delete_result ), @@ -162,7 +162,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ arb_api_deployment_list_result ), registry_entry!( - "DomainRegistrationDeleteResult", + "DomainRegistrationDeleteView", "api.domain.delete", arb_api_domain_delete_result ), @@ -265,7 +265,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ arb_plugin_register_result ), registry_entry!( - "PluginUnregisterResult", + "PluginUnregisterView", "plugin.unregister", arb_plugin_unregister_result ), @@ -2998,7 +2998,7 @@ fn arb_agent_resource_description() -> BoxedStrategy OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, agent)| { - crate::model::agent::action_result::AgentDeleteResult { + crate::model::agent::action_result::AgentDeleteView { deleted, agent_id: agent, } @@ -3063,7 +3063,7 @@ fn arb_agent_simulate_crash_result() -> OutputDocumentStrategy { fn arb_account_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, account_id)| { - crate::model::account::AccountDeleteResult { + crate::model::account::AccountDeleteView { deleted, account_id: golem_common::model::account::AccountId( uuid::Uuid::parse_str(&account_id).expect("generated UUID should parse"), @@ -3123,7 +3123,7 @@ fn arb_account_role() -> BoxedStrategy { fn arb_permission_share_delete_result() -> OutputDocumentStrategy { serialized_output((any::(), arb_small_string()).prop_map( - |(deleted, permission_share_id)| crate::model::account::PermissionShareDeleteResult { + |(deleted, permission_share_id)| crate::model::account::PermissionShareDeleteView { deleted, permission_share_id: golem_common::model::permission_share::PermissionShareId( uuid::Uuid::parse_str(&permission_share_id).expect("generated UUID should parse"), @@ -3428,7 +3428,7 @@ fn arb_agent_plugin_toggle_result() -> OutputDocumentStrategy { fn arb_token_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, token_id)| { - crate::model::token::TokenDeleteResult { + crate::model::token::TokenDeleteView { deleted, token_id: golem_common::model::auth::TokenId( uuid::Uuid::parse_str(&token_id).expect("generated UUID should parse"), @@ -3485,14 +3485,12 @@ fn arb_token_with_secret() -> BoxedStrategy OutputDocumentStrategy { serialized_output( (any::(), arb_small_string(), arb_small_string()).prop_map( - |(deleted, domain, id)| { - crate::model::http_api::domain::DomainRegistrationDeleteResult { - deleted, - domain: golem_common::model::domain_registration::Domain(domain), - id: golem_common::model::domain_registration::DomainRegistrationId( - uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), - ), - } + |(deleted, domain, id)| crate::model::http_api::domain::DomainRegistrationDeleteView { + deleted, + domain: golem_common::model::domain_registration::Domain(domain), + id: golem_common::model::domain_registration::DomainRegistrationId( + uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), + ), }, ), ) @@ -4775,7 +4773,7 @@ fn arb_plugin_unregister_result() -> OutputDocumentStrategy { arb_small_string(), ) .prop_map(|(unregistered, plugin_id, name, version)| { - crate::model::plugin::PluginUnregisterResult { + crate::model::plugin::PluginUnregisterView { unregistered, plugin_id, name, diff --git a/cli/golem-cli/src/model/http_api/domain.rs b/cli/golem-cli/src/model/http_api/domain.rs index 815ea0bff1..3268874b06 100644 --- a/cli/golem-cli/src/model/http_api/domain.rs +++ b/cli/golem-cli/src/model/http_api/domain.rs @@ -50,16 +50,32 @@ impl StructuredOutput for DomainRegistrationNewView { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DomainRegistrationDeleteResult { +pub struct DomainRegistrationDeleteView { pub deleted: bool, pub domain: Domain, pub id: DomainRegistrationId, } -impl NoTextOutput for DomainRegistrationDeleteResult {} -impl TextOutput for DomainRegistrationDeleteResult {} +impl Masked for DomainRegistrationDeleteView {} -impl StructuredOutput for DomainRegistrationDeleteResult { +impl MessageWithFields for DomainRegistrationDeleteView { + fn message(&self) -> String { + format!( + "Deleted API domain registration {}", + format_message_highlight(&self.domain.0) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Domain name", &self.domain.0, format_main_id) + .fmt_field("ID", &self.id, format_main_id); + fields.build() + } +} + +impl StructuredOutput for DomainRegistrationDeleteView { const KIND: &'static str = "api.domain.delete"; } diff --git a/cli/golem-cli/src/model/plugin.rs b/cli/golem-cli/src/model/plugin.rs index 2f6b28bbb5..9a9e2f7962 100644 --- a/cli/golem-cli/src/model/plugin.rs +++ b/cli/golem-cli/src/model/plugin.rs @@ -15,7 +15,7 @@ use crate::model::cli_output::StructuredOutput; use crate::model::masking::Masked; use crate::model::text_format::{ - Column, FieldsBuilder, MessageWithFields, NoTextOutput, TextOutput, format_id, format_main_id, + Column, FieldsBuilder, MessageWithFields, TextOutput, format_id, format_main_id, format_message_highlight, log_table, new_table_full_condensed, }; use golem_common::model::component::ComponentRevision; @@ -183,17 +183,34 @@ impl StructuredOutput for PluginRegistrationGetView { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct PluginUnregisterResult { +pub struct PluginUnregisterView { pub unregistered: bool, pub plugin_id: Uuid, pub name: String, pub version: String, } -impl NoTextOutput for PluginUnregisterResult {} -impl TextOutput for PluginUnregisterResult {} +impl Masked for PluginUnregisterView {} -impl StructuredOutput for PluginUnregisterResult { +impl MessageWithFields for PluginUnregisterView { + fn message(&self) -> String { + format!( + "Unregistered plugin {}", + format_message_highlight(&self.name) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Name", &self.name, format_main_id) + .fmt_field("Version", &self.version, format_main_id) + .fmt_field("Plugin ID", &self.plugin_id, format_id); + fields.build() + } +} + +impl StructuredOutput for PluginUnregisterView { const KIND: &'static str = "plugin.unregister"; } diff --git a/cli/golem-cli/src/model/token.rs b/cli/golem-cli/src/model/token.rs index 5150a0c33b..3fabf6e89e 100644 --- a/cli/golem-cli/src/model/token.rs +++ b/cli/golem-cli/src/model/token.rs @@ -86,14 +86,28 @@ impl StructuredOutput for TokenListView { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct TokenDeleteResult { +pub struct TokenDeleteView { pub deleted: bool, pub token_id: TokenId, } -impl NoTextOutput for TokenDeleteResult {} -impl TextOutput for TokenDeleteResult {} +impl Masked for TokenDeleteView {} -impl StructuredOutput for TokenDeleteResult { +impl MessageWithFields for TokenDeleteView { + fn message(&self) -> String { + format!( + "Deleted token {}", + format_message_highlight(&self.token_id.0) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields.fmt_field("Token ID", &self.token_id.0, format_main_id); + fields.build() + } +} + +impl StructuredOutput for TokenDeleteView { const KIND: &'static str = "api-token.delete"; } From 337e5b16fa1ab7ab915d59540d0ec0c55074f3b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 5 Aug 2026 19:44:06 +0200 Subject: [PATCH 67/70] trim deletion views to id-like fields, never secrets (rich-4) --- .../command-output.schema.json | 75 +++++-------------- .../command_handler/api/security_scheme.rs | 2 +- .../command_handler/resource_definition.rs | 2 +- .../src/command_handler/retry_policy.rs | 2 +- cli/golem-cli/src/command_handler/secret.rs | 4 +- cli/golem-cli/src/model/cli_output/tests.rs | 25 +++---- cli/golem-cli/src/model/http_api/security.rs | 28 ++++++- .../src/model/resource_definition.rs | 31 +++++++- cli/golem-cli/src/model/retry_policy.rs | 31 +++++++- cli/golem-cli/src/model/secret.rs | 42 +++++++---- 10 files changed, 142 insertions(+), 100 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 9cd4f491ae..5aacc57708 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -2058,46 +2058,27 @@ "x-golem-command": "api security-scheme delete", "required": [ "$type", + "deleted", "id", - "revision", "name", - "environmentId", - "providerType", - "clientId", - "redirectUrl", - "scopes" + "revision" ], "properties": { "$type": { "const": "api.security-scheme.delete" }, + "deleted": { + "type": "boolean" + }, "id": { "type": "string" }, - "revision": { - "type": "integer", - "minimum": 0 - }, "name": { "type": "string" }, - "environmentId": { - "type": "string" - }, - "providerType": { - "$ref": "#/definitions/SecuritySchemeProvider" - }, - "clientId": { - "type": "string" - }, - "redirectUrl": { - "type": "string" - }, - "scopes": { - "type": "array", - "items": { - "type": "string" - } + "revision": { + "type": "integer", + "minimum": 0 } }, "additionalProperties": false @@ -5393,27 +5374,21 @@ "x-golem-command": "resource delete", "required": [ "$type", + "deleted", "id", "revision", "environmentId", - "name", - "limit", - "enforcementAction", - "unit", - "units" + "name" ], "properties": { "$type": { "const": "resource.delete" }, + "deleted": { "type": "boolean" }, "id": { "type": "string" }, "revision": { "type": "integer", "minimum": 0 }, "environmentId": { "type": "string" }, - "name": { "type": "string" }, - "limit": { "$ref": "#/definitions/ResourceLimit" }, - "enforcementAction": { "$ref": "#/definitions/EnforcementAction" }, - "unit": { "type": "string" }, - "units": { "type": "string" } + "name": { "type": "string" } }, "additionalProperties": false }, @@ -5591,25 +5566,21 @@ "x-golem-command": "retry-policy delete", "required": [ "$type", + "deleted", "id", "environmentId", "name", - "revision", - "priority", - "predicate", - "policy" + "revision" ], "properties": { "$type": { "const": "retry-policy.delete" }, + "deleted": { "type": "boolean" }, "id": { "type": "string" }, "environmentId": { "type": "string" }, "name": { "type": "string" }, - "revision": { "type": "integer", "minimum": 0 }, - "priority": { "type": "integer", "minimum": 0 }, - "predicate": { "$ref": "#/definitions/ApiPredicate" }, - "policy": { "$ref": "#/definitions/ApiRetryPolicy" } + "revision": { "type": "integer", "minimum": 0 } }, "additionalProperties": false }, @@ -6520,27 +6491,21 @@ "x-golem-command": "secret delete", "required": [ "$type", + "deleted", "id", "environmentId", "path", - "revision", - "secretType" + "revision" ], "properties": { "$type": { "const": "secret.delete" }, + "deleted": { "type": "boolean" }, "id": { "type": "string" }, "environmentId": { "type": "string" }, "path": { "type": "array", "items": { "type": "string" } }, - "revision": { "type": "integer", "minimum": 0 }, - "secretType": { "$ref": "#/definitions/SchemaGraph" }, - "secretValue": { - "oneOf": [ - { "$ref": "#/definitions/SchemaValue" }, - { "type": "null" } - ] - } + "revision": { "type": "integer", "minimum": 0 } }, "additionalProperties": false }, diff --git a/cli/golem-cli/src/command_handler/api/security_scheme.rs b/cli/golem-cli/src/command_handler/api/security_scheme.rs index 200d02c173..3c2656a977 100644 --- a/cli/golem-cli/src/command_handler/api/security_scheme.rs +++ b/cli/golem-cli/src/command_handler/api/security_scheme.rs @@ -266,7 +266,7 @@ impl ApiSecuritySchemeCommandHandler { self.ctx .log_handler() - .log_output(HttpSecuritySchemeDeleteView(result))?; + .log_output(HttpSecuritySchemeDeleteView::from(result))?; Ok(()) } diff --git a/cli/golem-cli/src/command_handler/resource_definition.rs b/cli/golem-cli/src/command_handler/resource_definition.rs index 67b5f51013..bd8f1750f6 100644 --- a/cli/golem-cli/src/command_handler/resource_definition.rs +++ b/cli/golem-cli/src/command_handler/resource_definition.rs @@ -227,7 +227,7 @@ impl ResourceDefinitionCommandHandler { self.ctx .log_handler() - .log_output(ResourceDefinitionDeleteView(resource))?; + .log_output(ResourceDefinitionDeleteView::from(resource))?; Ok(()) } diff --git a/cli/golem-cli/src/command_handler/retry_policy.rs b/cli/golem-cli/src/command_handler/retry_policy.rs index 59b68e1c16..5c32e37c90 100644 --- a/cli/golem-cli/src/command_handler/retry_policy.rs +++ b/cli/golem-cli/src/command_handler/retry_policy.rs @@ -232,7 +232,7 @@ impl RetryPolicyCommandHandler { self.ctx .log_handler() - .log_output(RetryPolicyDeleteView(result))?; + .log_output(RetryPolicyDeleteView::from(result))?; Ok(()) } diff --git a/cli/golem-cli/src/command_handler/secret.rs b/cli/golem-cli/src/command_handler/secret.rs index 1bb83a988c..220627c2fd 100644 --- a/cli/golem-cli/src/command_handler/secret.rs +++ b/cli/golem-cli/src/command_handler/secret.rs @@ -22,7 +22,7 @@ use crate::log::log_error; use crate::model::environment::EnvironmentResolveMode; use crate::model::language::GuestLanguage; use crate::model::secret::{ - SecretCreateView, SecretDeleteView, SecretGetView, SecretListView, SecretUpdateView, + SecretCreateView, SecretDeleteView, SecretGetView, SecretListView, SecretUpdateView, SecretView, }; use anyhow::bail; use golem_client::api::AgentSecretsClient; @@ -237,7 +237,7 @@ impl SecretCommandHandler { self.ctx .log_handler() - .log_output(SecretDeleteView(result.into()))?; + .log_output(SecretDeleteView::from(SecretView::from(result)))?; Ok(()) } diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 430058d6ff..826251f569 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -1037,11 +1037,6 @@ fn cli_output_schema_validates_schema_native_secret_outputs() { MaskingConfig::hide_secrets(), ) .expect("secret.create should serialize"), - to_structured_output_value_masked( - crate::model::secret::SecretDeleteView(secret.clone().into()), - MaskingConfig::hide_secrets(), - ) - .expect("secret.delete should serialize"), to_structured_output_value_masked( crate::model::secret::SecretGetView(secret.clone().into()), MaskingConfig::hide_secrets(), @@ -3622,10 +3617,9 @@ fn arb_api_security_scheme_create_result() -> OutputDocumentStrategy { } fn arb_api_security_scheme_delete_result() -> OutputDocumentStrategy { - serialized_output( - arb_security_scheme() - .prop_map(crate::model::http_api::security::HttpSecuritySchemeDeleteView), - ) + serialized_output(arb_security_scheme().prop_map(|scheme| { + crate::model::http_api::security::HttpSecuritySchemeDeleteView::from(scheme) + })) } fn arb_api_security_scheme_get_result() -> OutputDocumentStrategy { @@ -4958,10 +4952,9 @@ fn arb_resource_create_result() -> OutputDocumentStrategy { } fn arb_resource_delete_result() -> OutputDocumentStrategy { - serialized_output( - arb_resource_definition() - .prop_map(crate::model::resource_definition::ResourceDefinitionDeleteView), - ) + serialized_output(arb_resource_definition().prop_map(|resource| { + crate::model::resource_definition::ResourceDefinitionDeleteView::from(resource) + })) } fn arb_resource_get_result() -> OutputDocumentStrategy { @@ -5331,7 +5324,7 @@ fn arb_retry_policy_create_result() -> OutputDocumentStrategy { fn arb_retry_policy_delete_result() -> OutputDocumentStrategy { serialized_output( - arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyDeleteView), + arb_retry_policy().prop_map(crate::model::retry_policy::RetryPolicyDeleteView::from), ) } @@ -5402,7 +5395,9 @@ fn arb_secret_delete_result() -> OutputDocumentStrategy { arb_secret() .prop_map(|secret| { to_structured_output_value_masked( - crate::model::secret::SecretDeleteView(secret.into()), + crate::model::secret::SecretDeleteView::from( + crate::model::secret::SecretView::from(secret), + ), MaskingConfig::hide_secrets(), ) .expect("generated secret delete should serialize") diff --git a/cli/golem-cli/src/model/http_api/security.rs b/cli/golem-cli/src/model/http_api/security.rs index 8be74cd9c2..463a5ad654 100644 --- a/cli/golem-cli/src/model/http_api/security.rs +++ b/cli/golem-cli/src/model/http_api/security.rs @@ -86,7 +86,24 @@ impl StructuredOutput for HttpSecuritySchemeUpdateView { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct HttpSecuritySchemeDeleteView(pub SecuritySchemeDto); +#[serde(rename_all = "camelCase")] +pub struct HttpSecuritySchemeDeleteView { + pub deleted: bool, + pub id: golem_common::model::security_scheme::SecuritySchemeId, + pub name: golem_common::model::security_scheme::SecuritySchemeName, + pub revision: golem_common::model::security_scheme::SecuritySchemeRevision, +} + +impl From for HttpSecuritySchemeDeleteView { + fn from(scheme: SecuritySchemeDto) -> Self { + Self { + deleted: true, + id: scheme.id, + name: scheme.name, + revision: scheme.revision, + } + } +} impl Masked for HttpSecuritySchemeDeleteView {} @@ -94,12 +111,17 @@ impl MessageWithFields for HttpSecuritySchemeDeleteView { fn message(&self) -> String { format!( "Deleted HTTP API Security scheme {}", - format_message_highlight(&self.0.name), + format_message_highlight(&self.name) ) } fn fields(&self) -> Vec<(String, String)> { - security_scheme_view_fields(&self.0) + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Name", &self.name.0, format_main_id) + .fmt_field("ID", &self.id, format_id) + .fmt_field("Revision", &self.revision.get(), format_id); + fields.build() } } diff --git a/cli/golem-cli/src/model/resource_definition.rs b/cli/golem-cli/src/model/resource_definition.rs index b75de1c6c8..43250a202c 100644 --- a/cli/golem-cli/src/model/resource_definition.rs +++ b/cli/golem-cli/src/model/resource_definition.rs @@ -63,7 +63,26 @@ impl StructuredOutput for ResourceDefinitionUpdateView { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResourceDefinitionDeleteView(pub ResourceDefinition); +#[serde(rename_all = "camelCase")] +pub struct ResourceDefinitionDeleteView { + pub deleted: bool, + pub id: golem_common::model::quota::ResourceDefinitionId, + pub environment_id: golem_common::model::environment::EnvironmentId, + pub name: golem_common::model::quota::ResourceName, + pub revision: golem_common::model::quota::ResourceDefinitionRevision, +} + +impl From for ResourceDefinitionDeleteView { + fn from(r: ResourceDefinition) -> Self { + Self { + deleted: true, + id: r.id, + environment_id: r.environment_id, + name: r.name, + revision: r.revision, + } + } +} impl Masked for ResourceDefinitionDeleteView {} @@ -71,12 +90,18 @@ impl MessageWithFields for ResourceDefinitionDeleteView { fn message(&self) -> String { format!( "Deleted resource definition {}", - format_message_highlight(&self.0.name.0), + format_message_highlight(&self.name.0) ) } fn fields(&self) -> Vec<(String, String)> { - resource_definition_fields(&self.0) + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Environment ID", &self.environment_id.0, format_main_id) + .fmt_field("Name", &self.name.0, format_main_id) + .fmt_field("ID", &self.id, format_id) + .fmt_field("Revision", &self.revision.get(), format_id); + fields.build() } } diff --git a/cli/golem-cli/src/model/retry_policy.rs b/cli/golem-cli/src/model/retry_policy.rs index 13db08c73e..033c6bcba4 100644 --- a/cli/golem-cli/src/model/retry_policy.rs +++ b/cli/golem-cli/src/model/retry_policy.rs @@ -82,7 +82,26 @@ impl StructuredOutput for RetryPolicyUpdateView { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RetryPolicyDeleteView(pub RetryPolicyDto); +#[serde(rename_all = "camelCase")] +pub struct RetryPolicyDeleteView { + pub deleted: bool, + pub id: golem_common::model::retry_policy::RetryPolicyId, + pub environment_id: golem_common::model::environment::EnvironmentId, + pub name: String, + pub revision: golem_common::model::retry_policy::RetryPolicyRevision, +} + +impl From for RetryPolicyDeleteView { + fn from(dto: RetryPolicyDto) -> Self { + Self { + deleted: true, + id: dto.id, + environment_id: dto.environment_id, + name: dto.name, + revision: dto.revision, + } + } +} impl Masked for RetryPolicyDeleteView {} @@ -90,12 +109,18 @@ impl MessageWithFields for RetryPolicyDeleteView { fn message(&self) -> String { format!( "Deleted retry policy {}", - format_message_highlight(&self.0.name), + format_message_highlight(&self.name) ) } fn fields(&self) -> Vec<(String, String)> { - retry_policy_view_fields(&self.0) + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Environment ID", &self.environment_id.0, format_main_id) + .fmt_field("Name", &self.name, format_main_id) + .fmt_field("ID", &self.id, format_id) + .fmt_field("Revision", &self.revision.get(), format_id); + fields.build() } } diff --git a/cli/golem-cli/src/model/secret.rs b/cli/golem-cli/src/model/secret.rs index be03063827..a699c9895a 100644 --- a/cli/golem-cli/src/model/secret.rs +++ b/cli/golem-cli/src/model/secret.rs @@ -162,36 +162,46 @@ impl StructuredOutput for SecretUpdateView { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -#[serde(transparent)] -pub struct SecretDeleteView(pub SecretView); +pub struct SecretDeleteView { + pub deleted: bool, + pub id: golem_common::model::agent_secret::AgentSecretId, + pub environment_id: golem_common::model::environment::EnvironmentId, + pub path: golem_common::model::agent_secret::CanonicalAgentSecretPath, + pub revision: golem_common::model::agent_secret::AgentSecretRevision, +} -impl Masked for SecretDeleteView { - fn masked(self, config: MaskingConfig) -> anyhow::Result { - Ok(Self(self.0.masked(config)?)) +impl From for SecretDeleteView { + fn from(view: SecretView) -> Self { + Self { + deleted: true, + id: view.id, + environment_id: view.environment_id, + path: view.path, + revision: view.revision, + } } } +impl Masked for SecretDeleteView {} + impl MessageWithFields for SecretDeleteView { fn message(&self) -> String { - format!("Deleted secret {}", format_message_highlight(&self.0.id),) + format!("Deleted secret {}", format_message_highlight(&self.id)) } fn fields(&self) -> Vec<(String, String)> { - secret_view_fields(&self.0) + let mut fields = FieldsBuilder::new(); + fields + .fmt_field("Environment ID", &self.environment_id.0, format_main_id) + .fmt_field("Path", &self.path, format_main_id) + .fmt_field("ID", &self.id, format_id) + .fmt_field("Revision", &self.revision.get(), format_id); + fields.build() } } impl StructuredOutput for SecretDeleteView { const KIND: &'static str = "secret.delete"; - - fn serialize_masked(self, serializer: S, config: MaskingConfig) -> Result - where - S: Serializer, - { - self.masked(config) - .map_err(serde::ser::Error::custom)? - .serialize(serializer) - } } fn secret_view_fields(view: &SecretView) -> Vec<(String, String)> { From 4793a6c2542566fb155602433495362d8910559b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 5 Aug 2026 19:48:16 +0200 Subject: [PATCH 68/70] render bulk deletion outputs as tables (agent-delete-all, card-revoke) --- .../command-output.schema.json | 2 +- cli/golem-cli/src/command_handler/card.rs | 3 --- .../src/command_handler/component/mod.rs | 4 ++-- .../src/model/agent/action_result.rs | 23 +++++++++++++++---- cli/golem-cli/src/model/card.rs | 2 ++ cli/golem-cli/src/model/cli_output/tests.rs | 4 ++-- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index 5aacc57708..dacf7c4b4f 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -6697,7 +6697,7 @@ }, { "type": "agent.delete-all", - "rustType": "AgentDeleteAllResult" + "rustType": "AgentDeleteAllView" }, { "type": "agent.file-contents", diff --git a/cli/golem-cli/src/command_handler/card.rs b/cli/golem-cli/src/command_handler/card.rs index 99f740fccc..2a196cce85 100644 --- a/cli/golem-cli/src/command_handler/card.rs +++ b/cli/golem-cli/src/command_handler/card.rs @@ -18,7 +18,6 @@ use crate::command_handler::agent::AgentCommandHandler; use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::error::service::MapServiceError; -use crate::log::log_warn_action; use crate::model::agent::RawAgentId; use crate::model::card::{CardGetView, CardListView, CardRevokeView}; use anyhow::bail; @@ -193,8 +192,6 @@ impl CardCommandHandler { .await .map_service_error()?; - log_warn_action("Revoked", "card"); - self.ctx.log_handler().log_output(CardRevokeView { revoked_card_ids: response.revoked_card_ids, })?; diff --git a/cli/golem-cli/src/command_handler/component/mod.rs b/cli/golem-cli/src/command_handler/component/mod.rs index 144dcc3ed4..0dbaa780ec 100644 --- a/cli/golem-cli/src/command_handler/component/mod.rs +++ b/cli/golem-cli/src/command_handler/component/mod.rs @@ -26,7 +26,7 @@ use crate::error::service::MapServiceError; use crate::log::{LogColorize, LogIndent, log_action, log_error, log_warn_action, logln}; use crate::model::agent::AgentUpdateMode; use crate::model::agent::action_result::{ - AgentDeleteAllResult, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, + AgentDeleteAllView, AgentDeletionMeta, AgentRedeployResult, AgentRedeploymentMeta, }; use crate::model::app::BuildConfig; use crate::model::app::{ApplicationComponentSelectMode, DynamicHelpSections}; @@ -459,7 +459,7 @@ impl ComponentCommandHandler { first_round = false; } - self.ctx.log_handler().log_output(AgentDeleteAllResult { + self.ctx.log_handler().log_output(AgentDeleteAllView { deleted: true, agents, })?; diff --git a/cli/golem-cli/src/model/agent/action_result.rs b/cli/golem-cli/src/model/agent/action_result.rs index a99d717879..7ba3d2c4ce 100644 --- a/cli/golem-cli/src/model/agent/action_result.rs +++ b/cli/golem-cli/src/model/agent/action_result.rs @@ -176,15 +176,30 @@ pub struct AgentRedeploymentMeta { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct AgentDeleteAllResult { +pub struct AgentDeleteAllView { pub deleted: bool, pub agents: Vec, } -impl NoTextOutput for AgentDeleteAllResult {} -impl TextOutput for AgentDeleteAllResult {} +impl TextOutput for AgentDeleteAllView { + fn log(&self) { + logln(format!("Deleted {} agent(s)", self.agents.len())); -impl StructuredOutput for AgentDeleteAllResult { + let mut table = + new_table_full_condensed(vec![Column::new("Component"), Column::new("Agent ID")]); + + for agent in &self.agents { + table.add_row(vec![ + agent.component_name.to_string(), + agent.agent_id.to_string(), + ]); + } + + log_table(table); + } +} + +impl StructuredOutput for AgentDeleteAllView { const KIND: &'static str = "agent.delete-all"; } diff --git a/cli/golem-cli/src/model/card.rs b/cli/golem-cli/src/model/card.rs index 06a701d4a5..e4b0f51e46 100644 --- a/cli/golem-cli/src/model/card.rs +++ b/cli/golem-cli/src/model/card.rs @@ -85,6 +85,8 @@ pub struct CardRevokeView { impl TextOutput for CardRevokeView { fn log(&self) { + logln(format!("Revoked {} card(s)", self.revoked_card_ids.len())); + let mut table = new_table_full_condensed(vec![Column::new("Revoked card ID")]); for card_id in &self.revoked_card_ids { diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 826251f569..30054d76e5 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -97,7 +97,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ ), registry_entry!("AgentDeleteView", "agent.delete", arb_agent_delete_result), registry_entry!( - "AgentDeleteAllResult", + "AgentDeleteAllView", "agent.delete-all", arb_agent_delete_all_result ), @@ -3363,7 +3363,7 @@ fn arb_agent_delete_all_result() -> OutputDocumentStrategy { proptest::collection::vec(arb_agent_deletion_meta(), 0..5), ) .prop_map(|(deleted, agents)| { - crate::model::agent::action_result::AgentDeleteAllResult { deleted, agents } + crate::model::agent::action_result::AgentDeleteAllView { deleted, agents } }), ) } From 80396a22b236cca298d0e2b83433c4cfa41f2cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Wed, 5 Aug 2026 19:52:12 +0200 Subject: [PATCH 69/70] render profile delete confirmation as view --- .../command-output.schema.json | 2 +- .../src/command_handler/profile/mod.rs | 11 +++------- cli/golem-cli/src/model/cli_output/tests.rs | 4 ++-- cli/golem-cli/src/model/config/profile.rs | 22 +++++++++++++++---- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index dacf7c4b4f..ce0132c076 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -6897,7 +6897,7 @@ }, { "type": "profile.delete", - "rustType": "ProfileDeleteResult" + "rustType": "ProfileDeleteView" }, { "type": "profile.get", diff --git a/cli/golem-cli/src/command_handler/profile/mod.rs b/cli/golem-cli/src/command_handler/profile/mod.rs index 67383ca075..6814221006 100644 --- a/cli/golem-cli/src/command_handler/profile/mod.rs +++ b/cli/golem-cli/src/command_handler/profile/mod.rs @@ -22,10 +22,10 @@ use crate::config::{ use crate::context::Context; use crate::error::NonSuccessfulExit; use crate::log::log_error; -use crate::log::{LogColorize, log_action, log_warn_action}; +use crate::log::{LogColorize, log_action}; use crate::model::config::ProfileView; use crate::model::config::profile::{ - ProfileCreateResult, ProfileDeleteResult, ProfileListView, ProfileSwitchResult, + ProfileCreateResult, ProfileDeleteView, ProfileListView, ProfileSwitchResult, }; use crate::model::format::Format; use anyhow::bail; @@ -255,12 +255,7 @@ impl ProfileCommandHandler { Config::delete_profile(&profile_name, self.ctx.config_dir())?; - log_warn_action( - "Deleted", - format!("profile {}", profile_name.0.log_color_highlight()), - ); - - self.ctx.log_handler().log_output(ProfileDeleteResult { + self.ctx.log_handler().log_output(ProfileDeleteView { deleted: true, profile: profile_name, })?; diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index 30054d76e5..e538ee0876 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -275,7 +275,7 @@ static STRUCTURED_OUTPUT_TEST_REGISTRY: &[StructuredOutputTestEntry] = &[ arb_profile_config_set_format_result ), registry_entry!( - "ProfileDeleteResult", + "ProfileDeleteView", "profile.delete", arb_profile_delete_result ), @@ -4926,7 +4926,7 @@ fn arb_profile_switch_result() -> OutputDocumentStrategy { fn arb_profile_delete_result() -> OutputDocumentStrategy { serialized_output( (any::(), arb_small_string()).prop_map(|(deleted, profile)| { - crate::model::config::profile::ProfileDeleteResult { + crate::model::config::profile::ProfileDeleteView { deleted, profile: crate::config::ProfileName(profile), } diff --git a/cli/golem-cli/src/model/config/profile.rs b/cli/golem-cli/src/model/config/profile.rs index 0290189955..df70c9e2d0 100644 --- a/cli/golem-cli/src/model/config/profile.rs +++ b/cli/golem-cli/src/model/config/profile.rs @@ -124,15 +124,29 @@ impl StructuredOutput for ProfileSwitchResult { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct ProfileDeleteResult { +pub struct ProfileDeleteView { pub deleted: bool, pub profile: ProfileName, } -impl NoTextOutput for ProfileDeleteResult {} -impl TextOutput for ProfileDeleteResult {} +impl Masked for ProfileDeleteView {} -impl StructuredOutput for ProfileDeleteResult { +impl MessageWithFields for ProfileDeleteView { + fn message(&self) -> String { + format!( + "Deleted profile {}", + format_message_highlight(&self.profile) + ) + } + + fn fields(&self) -> Vec<(String, String)> { + let mut fields = FieldsBuilder::new(); + fields.fmt_field("Profile", &self.profile, format_main_id); + fields.build() + } +} + +impl StructuredOutput for ProfileDeleteView { const KIND: &'static str = "profile.delete"; } From ce7d75293c8b25a701e893d06cf9a8eedf2f791a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81r=C3=B3?= Date: Thu, 6 Aug 2026 14:18:58 +0200 Subject: [PATCH 70/70] keep environment id in security-scheme and domain delete views --- .../command-output.schema.json | 10 ++++++- .../src/command_handler/api/domain.rs | 1 + cli/golem-cli/src/model/cli_output/tests.rs | 28 +++++++++++++------ cli/golem-cli/src/model/http_api/domain.rs | 2 ++ cli/golem-cli/src/model/http_api/security.rs | 3 ++ 5 files changed, 34 insertions(+), 10 deletions(-) diff --git a/cli/golem-cli/command-output-schema/command-output.schema.json b/cli/golem-cli/command-output-schema/command-output.schema.json index ce0132c076..6f51258797 100644 --- a/cli/golem-cli/command-output-schema/command-output.schema.json +++ b/cli/golem-cli/command-output-schema/command-output.schema.json @@ -1817,7 +1817,8 @@ "$type", "deleted", "domain", - "id" + "id", + "environmentId" ], "properties": { "$type": { @@ -1831,6 +1832,9 @@ }, "id": { "type": "string" + }, + "environmentId": { + "type": "string" } }, "additionalProperties": false @@ -2061,6 +2065,7 @@ "deleted", "id", "name", + "environmentId", "revision" ], "properties": { @@ -2076,6 +2081,9 @@ "name": { "type": "string" }, + "environmentId": { + "type": "string" + }, "revision": { "type": "integer", "minimum": 0 diff --git a/cli/golem-cli/src/command_handler/api/domain.rs b/cli/golem-cli/src/command_handler/api/domain.rs index 56c2777352..ece2d1dd27 100644 --- a/cli/golem-cli/src/command_handler/api/domain.rs +++ b/cli/golem-cli/src/command_handler/api/domain.rs @@ -165,6 +165,7 @@ impl ApiDomainCommandHandler { deleted: true, domain, id: domain_to_delete.id, + environment_id: domain_to_delete.environment_id, })?; Ok(()) diff --git a/cli/golem-cli/src/model/cli_output/tests.rs b/cli/golem-cli/src/model/cli_output/tests.rs index e538ee0876..a01aebc0f6 100644 --- a/cli/golem-cli/src/model/cli_output/tests.rs +++ b/cli/golem-cli/src/model/cli_output/tests.rs @@ -3479,15 +3479,25 @@ fn arb_token_with_secret() -> BoxedStrategy OutputDocumentStrategy { serialized_output( - (any::(), arb_small_string(), arb_small_string()).prop_map( - |(deleted, domain, id)| crate::model::http_api::domain::DomainRegistrationDeleteView { - deleted, - domain: golem_common::model::domain_registration::Domain(domain), - id: golem_common::model::domain_registration::DomainRegistrationId( - uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), - ), - }, - ), + ( + any::(), + arb_small_string(), + arb_small_string(), + arb_small_string(), + ) + .prop_map(|(deleted, domain, id, environment_id)| { + crate::model::http_api::domain::DomainRegistrationDeleteView { + deleted, + domain: golem_common::model::domain_registration::Domain(domain), + id: golem_common::model::domain_registration::DomainRegistrationId( + uuid::Uuid::parse_str(&id).expect("generated UUID should parse"), + ), + environment_id: golem_common::model::environment::EnvironmentId( + uuid::Uuid::parse_str(&environment_id) + .expect("generated UUID should parse"), + ), + } + }), ) } diff --git a/cli/golem-cli/src/model/http_api/domain.rs b/cli/golem-cli/src/model/http_api/domain.rs index 3268874b06..010116f532 100644 --- a/cli/golem-cli/src/model/http_api/domain.rs +++ b/cli/golem-cli/src/model/http_api/domain.rs @@ -54,6 +54,7 @@ pub struct DomainRegistrationDeleteView { pub deleted: bool, pub domain: Domain, pub id: DomainRegistrationId, + pub environment_id: golem_common::model::environment::EnvironmentId, } impl Masked for DomainRegistrationDeleteView {} @@ -70,6 +71,7 @@ impl MessageWithFields for DomainRegistrationDeleteView { let mut fields = FieldsBuilder::new(); fields .fmt_field("Domain name", &self.domain.0, format_main_id) + .fmt_field("Environment ID", &self.environment_id.0, format_main_id) .fmt_field("ID", &self.id, format_main_id); fields.build() } diff --git a/cli/golem-cli/src/model/http_api/security.rs b/cli/golem-cli/src/model/http_api/security.rs index 463a5ad654..341dcd479d 100644 --- a/cli/golem-cli/src/model/http_api/security.rs +++ b/cli/golem-cli/src/model/http_api/security.rs @@ -91,6 +91,7 @@ pub struct HttpSecuritySchemeDeleteView { pub deleted: bool, pub id: golem_common::model::security_scheme::SecuritySchemeId, pub name: golem_common::model::security_scheme::SecuritySchemeName, + pub environment_id: golem_common::model::environment::EnvironmentId, pub revision: golem_common::model::security_scheme::SecuritySchemeRevision, } @@ -100,6 +101,7 @@ impl From for HttpSecuritySchemeDeleteView { deleted: true, id: scheme.id, name: scheme.name, + environment_id: scheme.environment_id, revision: scheme.revision, } } @@ -118,6 +120,7 @@ impl MessageWithFields for HttpSecuritySchemeDeleteView { fn fields(&self) -> Vec<(String, String)> { let mut fields = FieldsBuilder::new(); fields + .fmt_field("Environment ID", &self.environment_id.0, format_main_id) .fmt_field("Name", &self.name.0, format_main_id) .fmt_field("ID", &self.id, format_id) .fmt_field("Revision", &self.revision.get(), format_id);