From 6c2c06e804de85face7aa4f58550524c0a99fa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Da=CC=81vid=20Istva=CC=81n=20Bi=CC=81ro=CC=81?= Date: Thu, 25 Jun 2026 15:23:25 +0200 Subject: [PATCH 1/5] introduce localServer and subdomain for deployments. --- cli/golem-cli/src/app/context.rs | 177 +- cli/golem-cli/src/app/manifest_upgrade.rs | 2 +- cli/golem-cli/src/command.rs | 10 +- cli/golem-cli/src/config.rs | 19 +- cli/golem-cli/src/context.rs | 83 +- cli/golem-cli/src/model/app.rs | 672 +++++- cli/golem-cli/src/model/app_raw.rs | 138 +- cli/golem-cli/src/model/text/server.rs | 6 +- cli/golem-cli/src/versions.rs | 2 +- .../templates/moonbit/default/golem.yaml | 2 +- .../moonbit/human-in-the-loop/golem.yaml | 2 +- .../templates/moonbit/json/golem.yaml | 2 +- .../templates/moonbit/snapshotting/golem.yaml | 2 +- .../templates/rust/default/golem.yaml | 2 +- .../rust/human-in-the-loop/golem.yaml | 2 +- cli/golem-cli/templates/rust/json/golem.yaml | 2 +- .../templates/rust/llm-session/golem.yaml | 2 +- .../llm-websearch-summary-example/golem.yaml | 2 +- .../templates/rust/snapshotting/golem.yaml | 2 +- .../templates/scala/default/golem.yaml | 2 +- .../scala/human-in-the-loop/golem.yaml | 2 +- cli/golem-cli/templates/scala/json/golem.yaml | 2 +- .../templates/scala/snapshotting/golem.yaml | 2 +- cli/golem-cli/templates/ts/default/golem.yaml | 2 +- .../templates/ts/human-in-the-loop/golem.yaml | 2 +- cli/golem-cli/templates/ts/json/golem.yaml | 2 +- .../templates/ts/snapshotting/golem.yaml | 2 +- cli/golem/src/command_handler.rs | 144 +- .../app/golem/1.6.0-dev.3/golem.schema.json | 1903 +++++++++++++++++ docs/src/content/next/app-manifest.mdx | 36 +- docs/src/content/next/cli/app-manifest.mdx | 6 +- docs/src/content/next/develop/webhooks.mdx | 2 +- .../common/golem-configure-api-domain.mdx | 53 +- .../common/golem-configure-mcp-server.mdx | 39 +- .../common/golem-edit-manifest.mdx | 20 +- .../common/golem-integration-test-setup.mdx | 31 +- .../common/golem-local-dev-server.mdx | 25 + .../golem-profiles-and-environments.mdx | 12 +- .../moonbit/golem-add-http-auth-moonbit.mdx | 6 +- .../golem-add-http-endpoint-moonbit.mdx | 4 +- .../moonbit/golem-add-webhook-moonbit.mdx | 4 +- .../rust/golem-add-http-auth-rust.mdx | 6 +- .../rust/golem-add-http-endpoint-rust.mdx | 4 +- .../rust/golem-add-webhook-rust.mdx | 4 +- .../scala/golem-add-http-auth-scala.mdx | 6 +- .../scala/golem-add-http-endpoint-scala.mdx | 4 +- .../scala/golem-add-webhook-scala.mdx | 4 +- .../ts/golem-add-http-auth-ts.mdx | 6 +- .../ts/golem-add-http-endpoint-ts.mdx | 4 +- .../how-to-guides/ts/golem-add-webhook-ts.mdx | 4 +- .../next/invoke/making-custom-apis.mdx | 2 +- docs/src/content/next/invoke/mcp.mdx | 2 +- docs/src/content/next/quickstart.mdx | 6 +- .../golem-configure-api-domain/SKILL.md | 53 +- .../golem-configure-mcp-server/SKILL.md | 39 +- .../common/golem-edit-manifest/SKILL.md | 20 +- .../golem-integration-test-setup/SKILL.md | 31 +- .../common/golem-local-dev-server/SKILL.md | 25 + .../golem-profiles-and-environments/SKILL.md | 12 +- .../golem-add-http-auth-moonbit/SKILL.md | 6 +- .../golem-add-http-endpoint-moonbit/SKILL.md | 4 +- .../golem-add-webhook-moonbit/SKILL.md | 4 +- .../rust/golem-add-http-auth-rust/SKILL.md | 6 +- .../golem-add-http-endpoint-rust/SKILL.md | 4 +- .../rust/golem-add-webhook-rust/SKILL.md | 4 +- .../scala/golem-add-http-auth-scala/SKILL.md | 6 +- .../golem-add-http-endpoint-scala/SKILL.md | 4 +- .../scala/golem-add-webhook-scala/SKILL.md | 4 +- .../skills/ts/golem-add-http-auth-ts/SKILL.md | 6 +- .../ts/golem-add-http-endpoint-ts/SKILL.md | 4 +- .../skills/ts/golem-add-webhook-ts/SKILL.md | 4 +- .../scenarios/configure-api-domain.yaml | 5 +- .../scenarios/configure-mcp-server.yaml | 5 +- .../harness/scenarios/local-dev-server.yaml | 59 + 74 files changed, 3471 insertions(+), 316 deletions(-) create mode 100644 cli/schema.golem.cloud/app/golem/1.6.0-dev.3/golem.schema.json diff --git a/cli/golem-cli/src/app/context.rs b/cli/golem-cli/src/app/context.rs index f2885df18d..2a1d1bf5b2 100644 --- a/cli/golem-cli/src/app/context.rs +++ b/cli/golem-cli/src/app/context.rs @@ -25,8 +25,8 @@ use crate::fs; use crate::log::{LogColorize, LogIndent, LogOutput, Output, log_action, logln}; use crate::model::app::{ AppBuildStep, Application, ApplicationComponentSelectMode, ApplicationConfig, - ApplicationNameAndEnvironments, ApplicationSourceMode, BuildConfig, CleanMode, - ComponentPresetSelector, CustomBridgeSdkTarget, DynamicHelpSections, LoadedRawApps, WithSource, + ApplicationPreload, ApplicationSourceMode, BuildConfig, CleanMode, ComponentPresetSelector, + CustomBridgeSdkTarget, DynamicHelpSections, LoadedRawApps, ResolvedLocalServer, WithSource, includes_from_yaml_file, }; use crate::model::format::Format; @@ -152,7 +152,8 @@ impl ToolsWithEnsuredCommonDeps { pub struct ApplicationPreloadResult { pub source_mode: ApplicationSourceMode, pub loaded_with_warnings: bool, - pub application_name_and_environments: Option, + pub application_preload: Option, + pub resolved_local_server: Option, pub used_language_templates: HashSet, } @@ -262,7 +263,8 @@ impl ApplicationContext { None => Ok(ApplicationPreloadResult { source_mode: ApplicationSourceMode::None, loaded_with_warnings: false, - application_name_and_environments: None, + application_preload: None, + resolved_local_server: None, used_language_templates: HashSet::new(), }), } @@ -273,12 +275,14 @@ impl ApplicationContext { config: ApplicationConfig, application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, file_download_client: reqwest::Client, ) -> anyhow::Result> { let Some(app_and_calling_working_dir) = load_app( application_name, environments, + local_server, component_presets, source_mode, ) else { @@ -626,6 +630,28 @@ impl ApplicationContext { )); } } + + let mcp_deployments = self + .application + .mcp_deployments(self.application.environment_name()); + if let Some(mcp_deployments) = mcp_deployments { + logln(format!( + "{}", + format!( + "Application MCP deployments for environment {}:", + environment_name.0 + ) + .log_color_help_group() + )); + + for (site, deployment) in mcp_deployments { + logln(format!(" {}", site.to_string().log_color_highlight(),)); + for (agent_name, _) in deployment.value.agents.iter() { + logln(format!(" {}", agent_name.as_str().log_color_highlight(),)); + } + } + logln(""); + } } if config.custom_commands() { @@ -663,6 +689,7 @@ impl ApplicationContext { fn load_app( application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, source_mode: ApplicationSourceMode, ) -> Option> { @@ -672,6 +699,7 @@ fn load_app( loaded_raw_apps.app_root_dir, application_name, environments, + local_server, component_presets, loaded_raw_apps.raw_apps, ) @@ -689,38 +717,45 @@ fn preload_app( let used_language_templates = Application::language_templates_from_raw_apps(&loaded_raw_apps.raw_apps); - Application::environments_from_raw_apps(loaded_raw_apps.raw_apps.as_slice()) - .and_then(|application_name_and_environments| { + Application::preload_from_raw_apps(loaded_raw_apps.raw_apps.as_slice()) + .and_then(|application_preload| { ValidatedResult::from_result(ensure_on_demand_commons( &used_language_templates, dev_mode, )) .map(|on_demand_common_raw_apps| { - (on_demand_common_raw_apps, application_name_and_environments) + (on_demand_common_raw_apps, application_preload) }) }) - .map( - |(on_demand_common_raw_apps, application_name_and_environments)| { - let raw_apps = { - let mut raw_apps = loaded_raw_apps.raw_apps; - raw_apps.extend(on_demand_common_raw_apps); - raw_apps - }; - - ApplicationPreloadResult { - source_mode: ApplicationSourceMode::Preloaded(LoadedRawApps { - app_root_dir: loaded_raw_apps.app_root_dir, - calling_working_dir: loaded_raw_apps.calling_working_dir, - raw_apps, - }), - loaded_with_warnings: false, - application_name_and_environments: Some( - application_name_and_environments, - ), - used_language_templates, - } - }, - ) + .map(|(on_demand_common_raw_apps, application_preload)| { + let resolved_local_server = + application_preload + .local_server + .as_ref() + .map(|local_server| { + ResolvedLocalServer::from_raw_with_source( + local_server, + &loaded_raw_apps.app_root_dir, + ) + }); + let raw_apps = { + let mut raw_apps = loaded_raw_apps.raw_apps; + raw_apps.extend(on_demand_common_raw_apps); + raw_apps + }; + + ApplicationPreloadResult { + source_mode: ApplicationSourceMode::Preloaded(LoadedRawApps { + app_root_dir: loaded_raw_apps.app_root_dir, + calling_working_dir: loaded_raw_apps.calling_working_dir, + raw_apps, + }), + loaded_with_warnings: false, + application_preload: Some(application_preload), + resolved_local_server, + used_language_templates, + } + }) }) }) } @@ -918,7 +953,7 @@ pub fn validated_to_anyhow( #[cfg(test)] mod tests { - use super::ApplicationContext; + use super::{ApplicationContext, preload_app}; use crate::model::app::ApplicationSourceMode; use test_r::test; @@ -958,6 +993,86 @@ app: demo let upgraded = std::fs::read_to_string(manifest).unwrap(); assert!(upgraded.contains("manifestVersion: 1.6.0")); - assert!(upgraded.contains("/1.6.0-dev.2/golem.schema.json")); + assert!(upgraded.contains("/1.6.0-dev.3/golem.schema.json")); + } + + #[test] + fn preload_resolves_local_server_paths_against_declaring_manifest_dir() { + let original_dir = std::env::current_dir().unwrap(); + let temp_dir = tempfile::tempdir().unwrap(); + let app_dir = temp_dir.path().join("app"); + let config_dir = app_dir.join("config"); + std::fs::create_dir(&app_dir).unwrap(); + std::fs::create_dir(&config_dir).unwrap(); + let manifest = app_dir.join("golem.yaml"); + let local_server_manifest = config_dir.join("local-server.yaml"); + std::fs::write( + &manifest, + r#" +manifestVersion: 1.6.0 +app: demo +includes: + - config/local-server.yaml +environments: + local: + server: local +"#, + ) + .unwrap(); + std::fs::write( + &local_server_manifest, + r#" +manifestVersion: 1.6.0 +localServer: + portsFile: .golem/ports.json + dataDir: .golem/data + agentFilesystemRoot: .golem/agents +"#, + ) + .unwrap(); + + let result = preload_app(ApplicationSourceMode::ByRootManifest(manifest), false) + .expect("manifest should be found"); + let (preload_result, warns, errors) = result.into_product(); + std::env::set_current_dir(&original_dir).unwrap(); + + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert!(errors.is_empty(), "\n{}", errors.join("\n\n")); + + let preload_result = preload_result.unwrap(); + let raw_local_server = preload_result + .application_preload + .unwrap() + .local_server + .unwrap() + .value; + + assert_eq!( + raw_local_server.ports_file, + Some(std::path::PathBuf::from(".golem/ports.json")) + ); + assert_eq!( + raw_local_server.data_dir, + Some(std::path::PathBuf::from(".golem/data")) + ); + assert_eq!( + raw_local_server.agent_filesystem_root, + Some(std::path::PathBuf::from(".golem/agents")) + ); + + let resolved_local_server = preload_result.resolved_local_server.unwrap(); + + assert_eq!( + resolved_local_server.ports_file, + Some(config_dir.join(".golem/ports.json")) + ); + assert_eq!( + resolved_local_server.data_dir, + Some(config_dir.join(".golem/data")) + ); + assert_eq!( + resolved_local_server.agent_filesystem_root, + Some(config_dir.join(".golem/agents")) + ); } } diff --git a/cli/golem-cli/src/app/manifest_upgrade.rs b/cli/golem-cli/src/app/manifest_upgrade.rs index be4447fdee..e1eed633d1 100644 --- a/cli/golem-cli/src/app/manifest_upgrade.rs +++ b/cli/golem-cli/src/app/manifest_upgrade.rs @@ -85,7 +85,7 @@ app: demo assert_eq!(steps.len(), 1); assert_eq!(steps[0].path, source); assert!(steps[0].new.contains("manifestVersion: 1.6.0")); - assert!(steps[0].new.contains("/1.6.0-dev.2/golem.schema.json")); + assert!(steps[0].new.contains("/1.6.0-dev.3/golem.schema.json")); assert!(!steps[0].new.contains("/1.5.0/golem.schema.json")); } diff --git a/cli/golem-cli/src/command.rs b/cli/golem-cli/src/command.rs index 4e0965d77e..a564e7e3d4 100644 --- a/cli/golem-cli/src/command.rs +++ b/cli/golem-cli/src/command.rs @@ -2300,6 +2300,9 @@ pub mod account { } pub mod server { + use crate::config::{ + DEFAULT_LOCAL_CUSTOM_REQUEST_PORT, DEFAULT_LOCAL_MCP_PORT, DEFAULT_LOCAL_ROUTER_PORT, + }; use clap::{Args, Subcommand}; use std::path::PathBuf; @@ -2356,14 +2359,15 @@ pub mod server { } pub fn router_port(&self) -> u16 { - self.router_port.unwrap_or(9881) + self.router_port.unwrap_or(DEFAULT_LOCAL_ROUTER_PORT) } pub fn custom_request_port(&self) -> u16 { - self.custom_request_port.unwrap_or(9006) + self.custom_request_port + .unwrap_or(DEFAULT_LOCAL_CUSTOM_REQUEST_PORT) } pub fn mcp_port(&self) -> u16 { - self.mcp_port.unwrap_or(9007) + self.mcp_port.unwrap_or(DEFAULT_LOCAL_MCP_PORT) } } diff --git a/cli/golem-cli/src/config.rs b/cli/golem-cli/src/config.rs index 7ed0dfafa6..af784a31d4 100644 --- a/cli/golem-cli/src/config.rs +++ b/cli/golem-cli/src/config.rs @@ -33,8 +33,16 @@ use std::time::Duration; use url::Url; use uuid::Uuid; -pub const CLOUD_URL: &str = "https://release.api.golem.cloud"; -const BUILTIN_LOCAL_URL: &str = "http://localhost:9881"; +pub const DEFAULT_LOCAL_ROUTER_PORT: u16 = 9881; +pub const DEFAULT_LOCAL_CUSTOM_REQUEST_PORT: u16 = 9006; +pub const DEFAULT_LOCAL_MCP_PORT: u16 = 9007; + +pub const DEFAULT_LOCAL_URL: &str = "http://localhost:9881"; +pub const DEFAULT_CLOUD_URL: &str = "https://release.api.golem.cloud"; + +pub const CLOUD_HTTP_API_DOMAIN: &str = "apps.golem.cloud"; +pub const CLOUD_MCP_DOMAIN: &str = "mcps.golem.cloud"; + const BUILTIN_LOCAL_URL_ENV: &str = "GOLEM_BUILTIN_LOCAL_URL"; const PROFILE_NAME_LOCAL: &str = "local"; const PROFILE_NAME_CLOUD: &str = "cloud"; @@ -58,7 +66,7 @@ fn builtin_local_url_state() -> &'static BuiltinLocalUrlState { } BuiltinLocalUrlState { - url: Url::parse(BUILTIN_LOCAL_URL).expect("Failed to parse BUILTIN_LOCAL_URL"), + url: Url::parse(DEFAULT_LOCAL_URL).expect("Failed to parse DEFAULT_LOCAL_URL"), uses_default: true, } }) @@ -331,7 +339,7 @@ pub struct ClientConfig { impl From<&Profile> for ClientConfig { fn from(profile: &Profile) -> Self { - let default_cloud_url = Url::parse(CLOUD_URL).unwrap(); + let default_cloud_url = Url::parse(DEFAULT_CLOUD_URL).unwrap(); let registry_url = profile.custom_url.clone().unwrap_or(default_cloud_url); let worker_url = profile .custom_worker_url @@ -373,7 +381,8 @@ impl From<&Server> for ClientConfig { allow_insecure: false, }, BuiltinServer::Cloud => { - let cloud_url = Url::parse(CLOUD_URL).expect("Failed to parse CLOUD_URL"); + let cloud_url = + Url::parse(DEFAULT_CLOUD_URL).expect("Failed to parse DEFAULT_CLOUD_URL"); BaseConfig { registry_url: cloud_url.clone(), worker_url: cloud_url.clone(), diff --git a/cli/golem-cli/src/context.rs b/cli/golem-cli/src/context.rs index a974959fcb..ec1bdbae11 100644 --- a/cli/golem-cli/src/context.rs +++ b/cli/golem-cli/src/context.rs @@ -25,9 +25,9 @@ use crate::config::{ use crate::config::{ClientConfig, ProfileName, builtin_local_url}; use crate::error::{ContextInitHintError, HintError, NonSuccessfulExit}; use crate::log::{LogColorize, LogOutput, Output, log_action, set_log_output}; -use crate::model::app::{ApplicationConfig, ComponentPresetSelector}; use crate::model::app::{ - ApplicationNameAndEnvironments, ApplicationSourceMode, ComponentPresetName, WithSource, + ApplicationConfig, ApplicationPreload, ApplicationSourceMode, ComponentPresetName, + ComponentPresetSelector, ResolvedLocalServer, WithSource, }; use crate::model::app_raw::{ BuiltinServer, CustomServerAuth, DeploymentOptions, Environment, Marker, Server, @@ -68,6 +68,7 @@ pub struct Context { environment_reference: Option, manifest_environment: Option, manifest_environment_deployment_options: Option, + manifest_local_server: Option, app_context_config: Option, http_batch_size: u64, http_parallelism: usize, @@ -146,17 +147,19 @@ impl Context { } let app_source_mode = preloaded_app.source_mode; - let application_name_and_environments = preloaded_app.application_name_and_environments; + let manifest_local_server = preloaded_app.resolved_local_server; + let application_preload = preloaded_app.application_preload; let manifest_environment: Option = match &environment_reference { Some(environment_reference) => { match environment_reference { EnvironmentReference::Environment { environment_name } => { - match &application_name_and_environments { - Some(ApplicationNameAndEnvironments { + match &application_preload { + Some(ApplicationPreload { application_name, environments, + .. }) => match environments.get(environment_name) { Some(environment) => Some(SelectedManifestEnvironment { application_name: application_name.value.clone(), @@ -188,10 +191,11 @@ impl Context { EnvironmentReference::AccountApplicationEnvironment { .. } => None, } } - None => match &application_name_and_environments { - Some(ApplicationNameAndEnvironments { + None => match &application_preload { + Some(ApplicationPreload { application_name, environments, + .. }) => environments .iter() .find(|(_, env)| env.default == Some(Marker)) @@ -283,34 +287,31 @@ impl Context { let file_download_client = new_raw_reqwest_client(&client_config.file_download_http_client_config)?; - let app_context_config = manifest_environment - .as_ref() - .zip(application_name_and_environments) - .map( - |(selected_environment, application_name_and_environments)| { - ApplicationContextConfig::new( - &global_flags, - application_name_and_environments, - ComponentPresetSelector { - environment: selected_environment.environment_name.clone(), - presets: { - if global_flags.preset.is_empty() { - selected_environment - .environment - .component_presets - .clone() - .into_vec() - .into_iter() - .map(ComponentPresetName) - .collect::>() - } else { - global_flags.preset.clone() - } - }, + let app_context_config = manifest_environment.as_ref().zip(application_preload).map( + |(selected_environment, application_preload)| { + ApplicationContextConfig::new( + &global_flags, + application_preload, + ComponentPresetSelector { + environment: selected_environment.environment_name.clone(), + presets: { + if global_flags.preset.is_empty() { + selected_environment + .environment + .component_presets + .clone() + .into_vec() + .into_iter() + .map(ComponentPresetName) + .collect::>() + } else { + global_flags.preset.clone() + } }, - ) - }, - ); + }, + ) + }, + ); Ok(Self { config_dir: global_flags.config_dir(), @@ -325,6 +326,7 @@ impl Context { environment_reference, manifest_environment, manifest_environment_deployment_options, + manifest_local_server, yes, dev_mode: global_flags.dev_mode, show_secrets: global_flags.show_secrets, @@ -413,6 +415,10 @@ impl Context { self.manifest_environment_deployment_options.as_ref() } + pub fn manifest_local_server(&self) -> Option<&ResolvedLocalServer> { + self.manifest_local_server.as_ref() + } + pub fn caches(&self) -> &Caches { &self.caches } @@ -675,6 +681,7 @@ struct ApplicationContextConfig { disable_app_manifest_discovery: bool, application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, wasm_rpc_client_build_offline: bool, dev_mode: bool, @@ -684,14 +691,15 @@ struct ApplicationContextConfig { impl ApplicationContextConfig { pub fn new( global_flags: &GolemCliGlobalFlags, - application_name_and_environments: ApplicationNameAndEnvironments, + application_preload: ApplicationPreload, component_presets: ComponentPresetSelector, ) -> Self { Self { app_manifest_path: global_flags.app_manifest_path.clone(), disable_app_manifest_discovery: global_flags.disable_app_manifest_discovery, - application_name: application_name_and_environments.application_name, - environments: application_name_and_environments.environments, + application_name: application_preload.application_name, + environments: application_preload.environments, + local_server: application_preload.local_server, component_presets, wasm_rpc_client_build_offline: global_flags.wasm_rpc_offline, dev_mode: global_flags.dev_mode, @@ -781,6 +789,7 @@ impl ApplicationContextState { app_config, config.application_name.clone(), config.environments.clone(), + config.local_server.clone(), config.component_presets.clone(), file_download_client.clone(), ) diff --git a/cli/golem-cli/src/model/app.rs b/cli/golem-cli/src/model/app.rs index 07ee9f2528..bdc13168e7 100644 --- a/cli/golem-cli/src/model/app.rs +++ b/cli/golem-cli/src/model/app.rs @@ -16,7 +16,7 @@ use super::http_api::{HttpApiDeploymentDeployProperties, McpDeploymentDeployProp use crate::bridge_gen::bridge_client_directory_name; use crate::fs; use crate::log::LogColorize; -use crate::model::app::app_builder::{build_application, build_environments}; +use crate::model::app::app_builder::{build_application, build_application_preload}; use crate::model::cascade::layer::Layer; use crate::model::cascade::property::Property; use crate::model::cascade::property::json::JsonProperty; @@ -320,9 +320,52 @@ impl Default for WithSource { } #[derive(Clone, Debug)] -pub struct ApplicationNameAndEnvironments { +pub struct ApplicationPreload { pub application_name: WithSource, pub environments: BTreeMap, + pub local_server: Option>, +} + +#[derive(Clone, Debug)] +pub struct ResolvedLocalServer { + pub router_addr: Option, + pub router_port: Option, + pub custom_request_port: Option, + pub mcp_port: Option, + pub ports_file: Option, + pub data_dir: Option, + pub agent_filesystem_root: Option, +} + +impl ResolvedLocalServer { + pub fn from_raw_with_source( + local_server: &WithSource, + app_root_dir: &Path, + ) -> Self { + let base_dir = local_server.source.parent().unwrap_or(app_root_dir); + Self::from_raw_with_base_dir(&local_server.value, base_dir) + } + + pub fn from_raw_with_base_dir(local_server: &app_raw::LocalServer, base_dir: &Path) -> Self { + Self { + router_addr: local_server.router_addr.clone(), + router_port: local_server.router_port, + custom_request_port: local_server.custom_request_port, + mcp_port: local_server.mcp_port, + ports_file: local_server + .ports_file + .as_ref() + .map(|path| fs::absolute_lexical_path_from_base_dir(path, base_dir)), + data_dir: local_server + .data_dir + .as_ref() + .map(|path| fs::absolute_lexical_path_from_base_dir(path, base_dir)), + agent_filesystem_root: local_server + .agent_filesystem_root + .as_ref() + .map(|path| fs::absolute_lexical_path_from_base_dir(path, base_dir)), + } + } } #[derive(Clone, Debug)] @@ -433,10 +476,10 @@ impl Application { } } - pub fn environments_from_raw_apps( + pub fn preload_from_raw_apps( apps: &[app_raw::ApplicationWithSource], - ) -> ValidatedResult { - build_environments(apps) + ) -> ValidatedResult { + build_application_preload(apps) } pub fn language_templates_from_raw_apps( @@ -480,6 +523,7 @@ impl Application { root_dir: PathBuf, application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, apps: Vec, ) -> ValidatedResult { @@ -487,6 +531,7 @@ impl Application { root_dir, application_name, environments, + local_server, component_presets, apps, ) @@ -2215,7 +2260,7 @@ mod app_builder { use crate::fuzzy::FuzzySearch; use crate::log::LogColorize; use crate::model::app::{ - Application, ApplicationNameAndEnvironments, ComponentLayer, ComponentLayerApplyContext, + Application, ApplicationPreload, ComponentLayer, ComponentLayerApplyContext, ComponentLayerId, ComponentLayerProperties, ComponentLayerPropertiesKind, ComponentPresetName, ComponentPresetSelector, ComponentProperties, PartitionedComponentPresets, TEMP_DIR, WithSource, @@ -2249,6 +2294,7 @@ mod app_builder { root_dir: PathBuf, application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, apps: Vec, ) -> ValidatedResult { @@ -2256,16 +2302,17 @@ mod app_builder { root_dir, application_name, environments, + local_server, component_presets, apps, ) } - // Load only environments - pub fn build_environments( + // Load manifest fields needed before full application loading. + pub fn build_application_preload( apps: &[app_raw::ApplicationWithSource], - ) -> ValidatedResult { - AppBuilder::build_environments(apps) + ) -> ValidatedResult { + AppBuilder::build_application_preload(apps) } #[derive(Debug, PartialEq, Eq, Hash)] @@ -2281,6 +2328,7 @@ mod app_builder { RetryPolicyDefaults(EnvironmentName), ResourceDefaults(EnvironmentName), Bridge, + LocalServer, } impl UniqueSourceCheckedEntityKey { @@ -2298,6 +2346,7 @@ mod app_builder { UniqueSourceCheckedEntityKey::RetryPolicyDefaults(_) => property, UniqueSourceCheckedEntityKey::ResourceDefaults(_) => property, UniqueSourceCheckedEntityKey::Bridge => "Bridge", + UniqueSourceCheckedEntityKey::LocalServer => property, } } @@ -2344,16 +2393,181 @@ mod app_builder { ) } UniqueSourceCheckedEntityKey::Bridge => "bridge".log_color_highlight().to_string(), + UniqueSourceCheckedEntityKey::LocalServer => { + "localServer".log_color_highlight().to_string() + } } } } + fn resolve_http_api_domain( + validation: &mut ValidationBuilder, + deployment: &app_raw::HttpApiDeployment, + environment_name: &EnvironmentName, + environment: Option<&app_raw::Environment>, + local_server: Option<&WithSource>, + source: &Path, + ) -> Option { + resolve_deployment_domain( + validation, + deployment.domain.as_ref(), + deployment.subdomain.as_ref(), + environment_name, + environment, + local_server.map(|local_server| &local_server.value), + |local_server| local_server.and_then(|local_server| local_server.custom_request_port), + crate::config::DEFAULT_LOCAL_CUSTOM_REQUEST_PORT, + crate::config::CLOUD_HTTP_API_DOMAIN, + "HTTP API", + source, + ) + } + + fn resolve_mcp_domain( + validation: &mut ValidationBuilder, + deployment: &app_raw::McpDeployment, + environment_name: &EnvironmentName, + environment: Option<&app_raw::Environment>, + local_server: Option<&WithSource>, + source: &Path, + ) -> Option { + resolve_deployment_domain( + validation, + deployment.domain.as_ref(), + deployment.subdomain.as_ref(), + environment_name, + environment, + local_server.map(|local_server| &local_server.value), + |local_server| local_server.and_then(|local_server| local_server.mcp_port), + crate::config::DEFAULT_LOCAL_MCP_PORT, + crate::config::CLOUD_MCP_DOMAIN, + "MCP", + source, + ) + } + + fn resolve_deployment_domain( + validation: &mut ValidationBuilder, + domain: Option<&app_raw::DeploymentDomain>, + subdomain: Option<&app_raw::DeploymentSubdomain>, + environment_name: &EnvironmentName, + environment: Option<&app_raw::Environment>, + local_server: Option<&app_raw::LocalServer>, + local_port: impl Fn(Option<&app_raw::LocalServer>) -> Option, + default_local_port: u16, + cloud_domain: &str, + deployment_kind: &str, + source: &Path, + ) -> Option { + match (domain, subdomain) { + (Some(domain), None) => { + let raw = domain.as_str(); + Some(Domain(raw.to_string())) + } + (None, Some(subdomain)) => { + let label = subdomain.as_str(); + if !is_valid_deployment_subdomain(label) { + validation.add_error(invalid_deployment_subdomain_error(label, source)); + return None; + } + + let Some(environment) = environment else { + validation.add_error(format!( + "Cannot resolve {} deployment subdomain {} in {} because environment {} is not defined.", + deployment_kind, + label.log_color_highlight(), + source.display().to_string().log_color_highlight(), + environment_name.0.log_color_highlight(), + )); + return None; + }; + + let resolved_domain = match environment.server.as_ref() { + Some(app_raw::Server::Custom(_)) => { + validation.add_error(format!( + "Cannot use {} deployment subdomain {} in {} for custom server environment {}. Use the {} field with a full domain instead.", + deployment_kind, + label.log_color_highlight(), + source.display().to_string().log_color_highlight(), + environment_name.0.log_color_highlight(), + "domain".log_color_highlight(), + )); + return None; + } + Some(app_raw::Server::Builtin(app_raw::BuiltinServer::Cloud)) => { + Domain(format!("{label}.{cloud_domain}")) + } + Some(app_raw::Server::Builtin(app_raw::BuiltinServer::Local)) | None => { + Domain(format!( + "{label}.localhost:{}", + local_port(local_server).unwrap_or(default_local_port) + )) + } + }; + + Some(resolved_domain) + } + (Some(_), Some(subdomain)) => { + validation.add_error(format!( + "Deployment in {} cannot define both {} and {}. Use {} for a full domain, or {} for a built-in local/cloud server subdomain.", + source.display().to_string().log_color_highlight(), + "domain".log_color_highlight(), + "subdomain".log_color_highlight(), + "domain".log_color_highlight(), + "subdomain".log_color_highlight(), + )); + if !is_valid_deployment_subdomain(subdomain.as_str()) { + validation.add_error(invalid_deployment_subdomain_error( + subdomain.as_str(), + source, + )); + } + None + } + (None, None) => { + validation.add_error(format!( + "Deployment in {} must define either {} or {}.", + source.display().to_string().log_color_highlight(), + "domain".log_color_highlight(), + "subdomain".log_color_highlight(), + )); + None + } + } + } + + fn is_valid_deployment_subdomain(label: &str) -> bool { + !label.is_empty() + && label.len() <= 63 + && label + .chars() + .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-') + && label + .chars() + .next() + .is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) + && label + .chars() + .last() + .is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit()) + } + + fn invalid_deployment_subdomain_error(label: &str, source: &Path) -> String { + format!( + "Invalid deployment subdomain {} in {}. Use a single lowercase DNS label without dots, ports, or URL schemes.", + label.log_color_highlight(), + source.display().to_string().log_color_highlight(), + ) + } + #[derive(Default)] struct AppBuilder { // For environment build app: Option>, default_environment_names: BTreeSet, environments: IndexMap, + local_server: Option>, + deployment_domain_local_server: Option>, // "Consts" for component templating app_root_dir_str: String, @@ -2399,18 +2613,21 @@ mod app_builder { } impl AppBuilder { - // NOTE: build_app DOES NOT include environments, those are preloaded with build_environments, so - // flows that do not use manifest otherwise won't get blocked by high-level validation errors, + // NOTE: build_app receives preloaded application fields, so flows that do not otherwise + // need the full manifest are not blocked by high-level validation errors, // and we do not "steal" manifest loading logs from those which do use the manifest fully. fn build_app( app_root_dir: PathBuf, application_name: WithSource, environments: BTreeMap, + local_server: Option>, component_presets: ComponentPresetSelector, apps: Vec, ) -> ValidatedResult { let mut validation = ValidationBuilder::default(); let mut builder = Self::default(); + builder.environments = environments.clone().into_iter().collect(); + builder.deployment_domain_local_server = local_server; match Ok::<&PathBuf, anyhow::Error>(&app_root_dir).and_then(|app_root_dir| { Ok(( @@ -2470,14 +2687,14 @@ mod app_builder { // NOTE: Unlike build_app, here we do not consume the source apps, so they can be // used for build_app. For more info on this separation, see build_app. - fn build_environments( + fn build_application_preload( apps: &[app_raw::ApplicationWithSource], - ) -> ValidatedResult { + ) -> ValidatedResult { let mut builder = Self::default(); let mut validation = ValidationBuilder::default(); for app in apps { - builder.add_raw_app_environments_only(&mut validation, app); + builder.add_raw_app_preload_only(&mut validation, app); } if builder.default_environment_names.len() > 1 { @@ -2518,9 +2735,10 @@ mod app_builder { } }; - validation.build(ApplicationNameAndEnvironments { + validation.build(ApplicationPreload { application_name, environments: builder.environments.into_iter().collect(), + local_server: builder.local_server, }) } @@ -2615,6 +2833,19 @@ mod app_builder { if let Some(http_api) = app.application.http_api { for (environment, deployments) in http_api.deployments { for api_deployment in deployments { + let Some(domain) = resolve_http_api_domain( + validation, + &api_deployment, + &environment, + self.environments.get(&environment), + self.deployment_domain_local_server + .as_ref() + .or(self.local_server.as_ref()), + &app.source, + ) else { + continue; + }; + let deployments = self.http_api_deployments.entry(environment.clone()).or_default(); @@ -2630,7 +2861,7 @@ mod app_builder { ) .collect(); - deployments.entry(api_deployment.domain).or_insert(WithSource::new( + deployments.entry(domain).or_insert(WithSource::new( app.source.to_path_buf(), HttpApiDeploymentDeployProperties { webhooks_prefix: HttpApiDeploymentCreation::normalize_webhooks_prefix( @@ -2649,6 +2880,19 @@ mod app_builder { if let Some(mcp) = app.application.mcp { for (environment, deployments) in mcp.deployments { for mcp_deployment in deployments { + let Some(domain) = resolve_mcp_domain( + validation, + &mcp_deployment, + &environment, + self.environments.get(&environment), + self.deployment_domain_local_server + .as_ref() + .or(self.local_server.as_ref()), + &app.source, + ) else { + continue; + }; + let mcp_deployments = self.mcp_deployments.entry(environment.clone()).or_default(); @@ -2659,7 +2903,7 @@ mod app_builder { })) .collect(); - mcp_deployments.entry(mcp_deployment.domain.clone()).or_insert(WithSource::new( + mcp_deployments.entry(domain).or_insert(WithSource::new( app.source.to_path_buf(), McpDeploymentDeployProperties { agents }, )); @@ -2782,7 +3026,7 @@ mod app_builder { }); } - fn add_raw_app_environments_only( + fn add_raw_app_preload_only( &mut self, validation: &mut ValidationBuilder, app: &app_raw::ApplicationWithSource, @@ -2840,6 +3084,22 @@ mod app_builder { ); } } + + if let Some(local_server) = &app.application.local_server { + match &self.local_server { + Some(existing) => validation.add_error(format!( + "{} {} is defined in multiple sources: {}, {}", + UniqueSourceCheckedEntityKey::LocalServer.entity_kind(), + UniqueSourceCheckedEntityKey::LocalServer.entity_name(), + existing.source.log_color_highlight(), + app.source.log_color_highlight() + )), + None => { + self.local_server = + Some(WithSource::new(app.source.clone(), local_server.clone())); + } + } + } }, ); } @@ -3256,12 +3516,13 @@ mod app_builder { mod test { use crate::fs; use crate::model::app::{ - Application, ApplicationNameAndEnvironments, ComponentPresetSelector, - includes_from_yaml_file, + Application, ApplicationPreload, ComponentPresetSelector, includes_from_yaml_file, }; use crate::model::app_raw; use golem_common::model::agent::AgentTypeName; use golem_common::model::component::ComponentName; + use golem_common::model::domain_registration::Domain; + use golem_common::model::environment::EnvironmentName; use indoc::indoc; use pretty_assertions::assert_eq; use serde_json::json; @@ -4054,6 +4315,323 @@ mod test { ); } + #[test] + fn deployment_subdomains_resolve_for_local_and_cloud() { + let source = indoc! { r#" + app: hello-app + + localServer: + customRequestPort: 9016 + mcpPort: 9017 + + environments: + local: + server: local + implicit: + default: true + cloud: + server: cloud + + httpApi: + deployments: + local: + - subdomain: hello-api + implicit: + - subdomain: implicit-api + cloud: + - subdomain: hello-api + + mcp: + deployments: + local: + - subdomain: hello-mcp + implicit: + - subdomain: implicit-mcp + cloud: + - subdomain: hello-mcp + "# }; + + let (app, _app_tmp_dir) = load_app_for_env(source, "local", &[]); + + assert!( + app.http_api_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("hello-api.localhost:9016".to_string())) + ); + assert!( + app.http_api_deployments(&EnvironmentName("cloud".to_string())) + .unwrap() + .contains_key(&Domain("hello-api.apps.golem.cloud".to_string())) + ); + assert!( + app.http_api_deployments(&EnvironmentName("implicit".to_string())) + .unwrap() + .contains_key(&Domain("implicit-api.localhost:9016".to_string())) + ); + assert!( + app.mcp_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("hello-mcp.localhost:9017".to_string())) + ); + assert!( + app.mcp_deployments(&EnvironmentName("cloud".to_string())) + .unwrap() + .contains_key(&Domain("hello-mcp.mcps.golem.cloud".to_string())) + ); + assert!( + app.mcp_deployments(&EnvironmentName("implicit".to_string())) + .unwrap() + .contains_key(&Domain("implicit-mcp.localhost:9017".to_string())) + ); + } + + #[test] + fn deployment_domains_keep_full_domains_unchanged() { + let source = indoc! { r#" + app: hello-app + + environments: + local: + server: local + + httpApi: + deployments: + local: + - domain: api.example.com + + mcp: + deployments: + local: + - domain: mcp.example.com + "# }; + + let (app, _app_tmp_dir) = load_app_for_env(source, "local", &[]); + + assert!( + app.http_api_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("api.example.com".to_string())) + ); + assert!( + app.mcp_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("mcp.example.com".to_string())) + ); + } + + #[test] + fn deployment_subdomains_use_default_local_ports_without_local_server() { + let source = indoc! { r#" + app: hello-app + + environments: + local: + server: local + + httpApi: + deployments: + local: + - subdomain: hello-api + + mcp: + deployments: + local: + - subdomain: hello-mcp + "# }; + + let (app, _app_tmp_dir) = load_app_for_env(source, "local", &[]); + + assert!( + app.http_api_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("hello-api.localhost:9006".to_string())) + ); + assert!( + app.mcp_deployments(&EnvironmentName("local".to_string())) + .unwrap() + .contains_key(&Domain("hello-mcp.localhost:9007".to_string())) + ); + } + + #[test] + fn deployment_subdomains_reject_invalid_values() { + let source = indoc! { r#" + app: hello-app + + environments: + local: + server: local + + httpApi: + deployments: + local: + - subdomain: hello.api + - subdomain: Hello + - subdomain: http://hello + "# }; + + let tmp_dir = tempfile::tempdir().unwrap(); + let golem_yaml_path = tmp_dir.path().join("golem.yaml"); + fs::write(&golem_yaml_path, source).unwrap(); + let raw_apps = vec![ + app_raw::ApplicationWithSource::from_yaml_file(&golem_yaml_path) + .expect("raw manifest should parse"), + ]; + + let (app_name_and_envs, warns, errors) = + Application::preload_from_raw_apps(&raw_apps).into_product(); + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert!(errors.is_empty(), "\n{}", errors.join("\n\n")); + let Some(ApplicationPreload { + application_name, + environments, + local_server, + }) = app_name_and_envs + else { + panic!("expected Some(ApplicationPreload)") + }; + + let (_app, warns, errors) = Application::from_raw_apps( + std::env::current_dir().unwrap(), + application_name, + environments, + local_server, + selector("local", &[]), + raw_apps, + ) + .into_product(); + + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert_eq!(errors.len(), 3, "\n{}", errors.join("\n\n")); + assert!( + errors + .iter() + .all(|error| error.contains("Invalid deployment subdomain")) + ); + } + + #[test] + fn deployment_subdomains_reject_custom_server_environments() { + let source = indoc! { r#" + app: hello-app + + environments: + custom: + server: + url: http://localhost:9881 + workerUrl: http://localhost:9881 + allowInsecure: true + auth: + staticToken: token + + httpApi: + deployments: + custom: + - subdomain: hello-api + "# }; + + let tmp_dir = tempfile::tempdir().unwrap(); + let golem_yaml_path = tmp_dir.path().join("golem.yaml"); + fs::write(&golem_yaml_path, source).unwrap(); + let raw_apps = vec![ + app_raw::ApplicationWithSource::from_yaml_file(&golem_yaml_path) + .expect("raw manifest should parse"), + ]; + + let (app_name_and_envs, warns, errors) = + Application::preload_from_raw_apps(&raw_apps).into_product(); + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert!(errors.is_empty(), "\n{}", errors.join("\n\n")); + let Some(ApplicationPreload { + application_name, + environments, + local_server, + }) = app_name_and_envs + else { + panic!("expected Some(ApplicationPreload)") + }; + + let (_app, warns, errors) = Application::from_raw_apps( + std::env::current_dir().unwrap(), + application_name, + environments, + local_server, + selector("custom", &[]), + raw_apps, + ) + .into_product(); + + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert_eq!(errors.len(), 1, "\n{}", errors.join("\n\n")); + assert!( + errors[0].contains("Cannot use HTTP API deployment subdomain"), + "\n{}", + errors.join("\n\n") + ); + } + + #[test] + fn deployment_entries_require_exactly_one_domain_field() { + let source = indoc! { r#" + app: hello-app + + environments: + local: + server: local + + httpApi: + deployments: + local: + - domain: api.example.com + subdomain: hello-api + - agents: {} + "# }; + + let tmp_dir = tempfile::tempdir().unwrap(); + let golem_yaml_path = tmp_dir.path().join("golem.yaml"); + fs::write(&golem_yaml_path, source).unwrap(); + let raw_apps = vec![ + app_raw::ApplicationWithSource::from_yaml_file(&golem_yaml_path) + .expect("raw manifest should parse"), + ]; + + let (app_name_and_envs, warns, errors) = + Application::preload_from_raw_apps(&raw_apps).into_product(); + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert!(errors.is_empty(), "\n{}", errors.join("\n\n")); + let Some(ApplicationPreload { + application_name, + environments, + local_server, + }) = app_name_and_envs + else { + panic!("expected Some(ApplicationPreload)") + }; + + let (_app, warns, errors) = Application::from_raw_apps( + std::env::current_dir().unwrap(), + application_name, + environments, + local_server, + selector("local", &[]), + raw_apps, + ) + .into_product(); + + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert_eq!(errors.len(), 2, "\n{}", errors.join("\n\n")); + assert!( + errors + .iter() + .any(|error| error.contains("cannot define both")) + ); + assert!( + errors + .iter() + .any(|error| error.contains("must define either")) + ); + } + fn component_applied_layers_trace(component: &crate::model::app::Component<'_>) -> Vec { component .applied_layers() @@ -4138,21 +4716,23 @@ mod test { let raw_apps = vec![raw_app]; let (app_name_and_envs, warns, errors) = - Application::environments_from_raw_apps(&raw_apps).into_product(); + Application::preload_from_raw_apps(&raw_apps).into_product(); assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); assert!(errors.is_empty(), "\n{}", errors.join("\n\n")); - let Some(ApplicationNameAndEnvironments { + let Some(ApplicationPreload { application_name, environments, + local_server, }) = app_name_and_envs else { - panic!("expected Some(ApplicationNameAndEnvironments)") + panic!("expected Some(ApplicationPreload)") }; let (app, warns, errors) = Application::from_raw_apps( std::env::current_dir().unwrap(), application_name, environments, + local_server, selector.clone(), raw_apps, ) @@ -4162,6 +4742,50 @@ mod test { (app.unwrap(), tmp_dir) } + #[test] + fn local_server_is_singleton_across_manifest_sources() { + let tmp_dir = tempfile::tempdir().unwrap(); + let root = tmp_dir.path().join("golem.yaml"); + let included = tmp_dir.path().join("included.yaml"); + + fs::write( + &root, + indoc! {r#" + app: test-app + localServer: + routerPort: 9882 + environments: + local: + server: local + "#}, + ) + .unwrap(); + fs::write( + &included, + indoc! {r#" + localServer: + routerPort: 9883 + "#}, + ) + .unwrap(); + + let raw_apps = vec![ + app_raw::ApplicationWithSource::from_yaml_file(&root).unwrap(), + app_raw::ApplicationWithSource::from_yaml_file(&included).unwrap(), + ]; + + let (_app_name_and_envs, warns, errors) = + Application::preload_from_raw_apps(&raw_apps).into_product(); + + assert!(warns.is_empty(), "\n{}", warns.join("\n\n")); + assert_eq!(errors.len(), 1); + assert!( + errors[0].contains("localServer"), + "unexpected error: {}", + errors[0] + ); + } + #[test] fn includes_loader_is_lenient_to_unknown_top_level_fields() { let tmp_dir = tempfile::tempdir().unwrap(); diff --git a/cli/golem-cli/src/model/app_raw.rs b/cli/golem-cli/src/model/app_raw.rs index 6850696add..bb24072076 100644 --- a/cli/golem-cli/src/model/app_raw.rs +++ b/cli/golem-cli/src/model/app_raw.rs @@ -116,6 +116,8 @@ pub struct Application { pub http_api: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub mcp: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub local_server: Option, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub environments: IndexMap, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -516,7 +518,10 @@ pub struct HttpApi { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct HttpApiDeployment { - pub domain: Domain, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subdomain: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub webhook_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -535,7 +540,10 @@ pub struct Mcp { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct McpDeployment { - pub domain: Domain, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub domain: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subdomain: Option, #[serde(default, skip_serializing_if = "IndexMap::is_empty")] pub agents: IndexMap, } @@ -547,6 +555,51 @@ pub struct McpDeploymentAgentOptions { pub security_scheme: Option, } +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(transparent)] +pub struct DeploymentDomain(pub String); + +impl From for DeploymentDomain { + fn from(value: Domain) -> Self { + Self(value.0) + } +} + +impl DeploymentDomain { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(transparent)] +pub struct DeploymentSubdomain(pub String); + +impl DeploymentSubdomain { + pub fn as_str(&self) -> &str { + &self.0 + } +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct LocalServer { + #[serde(skip_serializing_if = "Option::is_none", default)] + pub router_addr: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub router_port: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub custom_request_port: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub mcp_port: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub ports_file: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub data_dir: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub agent_filesystem_root: Option, +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct Environment { @@ -1487,6 +1540,42 @@ mod test { .boxed() } + fn arb_path_buf_model() -> BoxedStrategy { + arb_ident().prop_map(PathBuf::from).boxed() + } + + fn arb_local_server_model() -> BoxedStrategy { + ( + arb_opt(arb_ident()), + arb_opt(any::().boxed()), + arb_opt(any::().boxed()), + arb_opt(any::().boxed()), + arb_opt(arb_path_buf_model()), + arb_opt(arb_path_buf_model()), + arb_opt(arb_path_buf_model()), + ) + .prop_map( + |( + router_addr, + router_port, + custom_request_port, + mcp_port, + ports_file, + data_dir, + agent_filesystem_root, + )| LocalServer { + router_addr, + router_port, + custom_request_port, + mcp_port, + ports_file, + data_dir, + agent_filesystem_root, + }, + ) + .boxed() + } + fn arb_http_api_deployment_model() -> BoxedStrategy { ( arb_dns_label(), @@ -1514,7 +1603,8 @@ mod test { ) .prop_map( |(domain, webhook_url, openapi_endpoint, agents)| HttpApiDeployment { - domain: Domain(format!("{domain}.example.com")), + domain: Some(Domain(format!("{domain}.example.com")).into()), + subdomain: None, webhook_url, openapi_endpoint, agents, @@ -1551,7 +1641,8 @@ mod test { .prop_map(IndexMap::from_iter), ) .prop_map(|(domain, agents)| McpDeployment { - domain: Domain(format!("{domain}.example.com")), + domain: Some(Domain(format!("{domain}.example.com")).into()), + subdomain: None, agents, }) .boxed() @@ -1880,6 +1971,7 @@ mod test { ( arb_opt(arb_http_api_model()), arb_opt(arb_mcp_model()), + arb_opt(arb_local_server_model()), prop::collection::vec((arb_ident(), arb_environment_model()), 0..=3) .prop_map(IndexMap::from_iter), arb_opt(arb_bridge_sdks_model()), @@ -1897,6 +1989,7 @@ mod test { ( http_api, mcp, + local_server, environments, bridge, secret_defaults, @@ -1914,6 +2007,7 @@ mod test { clean, http_api, mcp, + local_server, environments, bridge, secret_defaults, @@ -1995,6 +2089,42 @@ mod test { ); } + #[test] + fn local_server_accepts_server_run_defaults() { + let source = indoc::indoc! { r#" + app: test-app + + localServer: + routerAddr: 127.0.0.1 + routerPort: 9882 + customRequestPort: 9008 + mcpPort: 9009 + portsFile: .golem/ports.json + dataDir: .golem/server-data + agentFilesystemRoot: .golem/agents + "# }; + + let app = Application::from_yaml_str(source).unwrap(); + let local_server = app.local_server.expect("localServer should be parsed"); + + assert_eq!(local_server.router_addr.as_deref(), Some("127.0.0.1")); + assert_eq!(local_server.router_port, Some(9882)); + assert_eq!(local_server.custom_request_port, Some(9008)); + assert_eq!(local_server.mcp_port, Some(9009)); + assert_eq!( + local_server.ports_file, + Some(PathBuf::from(".golem/ports.json")) + ); + assert_eq!( + local_server.data_dir, + Some(PathBuf::from(".golem/server-data")) + ); + assert_eq!( + local_server.agent_filesystem_root, + Some(PathBuf::from(".golem/agents")) + ); + } + proptest! { #![proptest_config(ProptestConfig { cases: 400, diff --git a/cli/golem-cli/src/model/text/server.rs b/cli/golem-cli/src/model/text/server.rs index 570b0f15a0..42171eb204 100644 --- a/cli/golem-cli/src/model/text/server.rs +++ b/cli/golem-cli/src/model/text/server.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::config::{CLOUD_URL, NamedProfile, builtin_local_url}; +use crate::config::{DEFAULT_CLOUD_URL, NamedProfile, builtin_local_url}; use crate::model::app_raw::{BuiltinServer, Environment, Server}; use colored::Colorize; @@ -29,7 +29,7 @@ impl ToFormattedServerContext for NamedProfile { } else { match &self.profile.custom_url { Some(custom_url) => custom_url.as_str().underline().to_string(), - None => CLOUD_URL.underline().to_string(), + None => DEFAULT_CLOUD_URL.underline().to_string(), } } } @@ -55,5 +55,5 @@ fn local_builtin() -> String { } fn cloud_builtin() -> String { - format!("cloud - builtin ({})", CLOUD_URL.underline()) + format!("cloud - builtin ({})", DEFAULT_CLOUD_URL.underline()) } diff --git a/cli/golem-cli/src/versions.rs b/cli/golem-cli/src/versions.rs index 570c1fedeb..1d8e67683c 100644 --- a/cli/golem-cli/src/versions.rs +++ b/cli/golem-cli/src/versions.rs @@ -22,7 +22,7 @@ pub mod sdk { #[macro_export] macro_rules! manifest_schema_version { () => { - "1.6.0-dev.2" + "1.6.0-dev.3" }; } } diff --git a/cli/golem-cli/templates/moonbit/default/golem.yaml b/cli/golem-cli/templates/moonbit/default/golem.yaml index 79afc91484..75e279b372 100644 --- a/cli/golem-cli/templates/moonbit/default/golem.yaml +++ b/cli/golem-cli/templates/moonbit/default/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterAgent: {} diff --git a/cli/golem-cli/templates/moonbit/human-in-the-loop/golem.yaml b/cli/golem-cli/templates/moonbit/human-in-the-loop/golem.yaml index c61d337fc3..89e62912e0 100644 --- a/cli/golem-cli/templates/moonbit/human-in-the-loop/golem.yaml +++ b/cli/golem-cli/templates/moonbit/human-in-the-loop/golem.yaml @@ -3,7 +3,7 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: WorkflowAgent: {} HumanAgent: {} \ No newline at end of file diff --git a/cli/golem-cli/templates/moonbit/json/golem.yaml b/cli/golem-cli/templates/moonbit/json/golem.yaml index c479923c7a..444090249a 100644 --- a/cli/golem-cli/templates/moonbit/json/golem.yaml +++ b/cli/golem-cli/templates/moonbit/json/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: TaskAgent: {} diff --git a/cli/golem-cli/templates/moonbit/snapshotting/golem.yaml b/cli/golem-cli/templates/moonbit/snapshotting/golem.yaml index 1a7e04aa35..68300ecaa0 100644 --- a/cli/golem-cli/templates/moonbit/snapshotting/golem.yaml +++ b/cli/golem-cli/templates/moonbit/snapshotting/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterWithSnapshotAgent: {} diff --git a/cli/golem-cli/templates/rust/default/golem.yaml b/cli/golem-cli/templates/rust/default/golem.yaml index 79afc91484..75e279b372 100644 --- a/cli/golem-cli/templates/rust/default/golem.yaml +++ b/cli/golem-cli/templates/rust/default/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterAgent: {} diff --git a/cli/golem-cli/templates/rust/human-in-the-loop/golem.yaml b/cli/golem-cli/templates/rust/human-in-the-loop/golem.yaml index 4de50106ea..dcd3bd8497 100644 --- a/cli/golem-cli/templates/rust/human-in-the-loop/golem.yaml +++ b/cli/golem-cli/templates/rust/human-in-the-loop/golem.yaml @@ -3,7 +3,7 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: WorkflowAgent: {} HumanAgent: {} diff --git a/cli/golem-cli/templates/rust/json/golem.yaml b/cli/golem-cli/templates/rust/json/golem.yaml index 7c527c65c3..72aa64a4b3 100644 --- a/cli/golem-cli/templates/rust/json/golem.yaml +++ b/cli/golem-cli/templates/rust/json/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: Tasks: {} diff --git a/cli/golem-cli/templates/rust/llm-session/golem.yaml b/cli/golem-cli/templates/rust/llm-session/golem.yaml index bcb3f1bd2f..8c37d36477 100644 --- a/cli/golem-cli/templates/rust/llm-session/golem.yaml +++ b/cli/golem-cli/templates/rust/llm-session/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: ChatAgent: {} diff --git a/cli/golem-cli/templates/rust/llm-websearch-summary-example/golem.yaml b/cli/golem-cli/templates/rust/llm-websearch-summary-example/golem.yaml index db49c8ba35..29399c370c 100644 --- a/cli/golem-cli/templates/rust/llm-websearch-summary-example/golem.yaml +++ b/cli/golem-cli/templates/rust/llm-websearch-summary-example/golem.yaml @@ -3,7 +3,7 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: ResearchAgent: {} diff --git a/cli/golem-cli/templates/rust/snapshotting/golem.yaml b/cli/golem-cli/templates/rust/snapshotting/golem.yaml index 1a7e04aa35..68300ecaa0 100644 --- a/cli/golem-cli/templates/rust/snapshotting/golem.yaml +++ b/cli/golem-cli/templates/rust/snapshotting/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterWithSnapshotAgent: {} diff --git a/cli/golem-cli/templates/scala/default/golem.yaml b/cli/golem-cli/templates/scala/default/golem.yaml index 03b74d40a4..a613957fc3 100644 --- a/cli/golem-cli/templates/scala/default/golem.yaml +++ b/cli/golem-cli/templates/scala/default/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterAgent: {} \ No newline at end of file diff --git a/cli/golem-cli/templates/scala/human-in-the-loop/golem.yaml b/cli/golem-cli/templates/scala/human-in-the-loop/golem.yaml index 4de50106ea..dcd3bd8497 100644 --- a/cli/golem-cli/templates/scala/human-in-the-loop/golem.yaml +++ b/cli/golem-cli/templates/scala/human-in-the-loop/golem.yaml @@ -3,7 +3,7 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: WorkflowAgent: {} HumanAgent: {} diff --git a/cli/golem-cli/templates/scala/json/golem.yaml b/cli/golem-cli/templates/scala/json/golem.yaml index c479923c7a..444090249a 100644 --- a/cli/golem-cli/templates/scala/json/golem.yaml +++ b/cli/golem-cli/templates/scala/json/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: TaskAgent: {} diff --git a/cli/golem-cli/templates/scala/snapshotting/golem.yaml b/cli/golem-cli/templates/scala/snapshotting/golem.yaml index 1a7e04aa35..68300ecaa0 100644 --- a/cli/golem-cli/templates/scala/snapshotting/golem.yaml +++ b/cli/golem-cli/templates/scala/snapshotting/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterWithSnapshotAgent: {} diff --git a/cli/golem-cli/templates/ts/default/golem.yaml b/cli/golem-cli/templates/ts/default/golem.yaml index 79afc91484..75e279b372 100644 --- a/cli/golem-cli/templates/ts/default/golem.yaml +++ b/cli/golem-cli/templates/ts/default/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterAgent: {} diff --git a/cli/golem-cli/templates/ts/human-in-the-loop/golem.yaml b/cli/golem-cli/templates/ts/human-in-the-loop/golem.yaml index 4de50106ea..dcd3bd8497 100644 --- a/cli/golem-cli/templates/ts/human-in-the-loop/golem.yaml +++ b/cli/golem-cli/templates/ts/human-in-the-loop/golem.yaml @@ -3,7 +3,7 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: WorkflowAgent: {} HumanAgent: {} diff --git a/cli/golem-cli/templates/ts/json/golem.yaml b/cli/golem-cli/templates/ts/json/golem.yaml index c479923c7a..444090249a 100644 --- a/cli/golem-cli/templates/ts/json/golem.yaml +++ b/cli/golem-cli/templates/ts/json/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: TaskAgent: {} diff --git a/cli/golem-cli/templates/ts/snapshotting/golem.yaml b/cli/golem-cli/templates/ts/snapshotting/golem.yaml index 1a7e04aa35..68300ecaa0 100644 --- a/cli/golem-cli/templates/ts/snapshotting/golem.yaml +++ b/cli/golem-cli/templates/ts/snapshotting/golem.yaml @@ -3,6 +3,6 @@ httpApi: deployments: local: - - domain: app-name.localhost:9006 + - subdomain: app-name # resolves to app-name.localhost:9006 by default agents: CounterWithSnapshotAgent: {} diff --git a/cli/golem/src/command_handler.rs b/cli/golem/src/command_handler.rs index 011b81f7dc..2c328f4064 100644 --- a/cli/golem/src/command_handler.rs +++ b/cli/golem/src/command_handler.rs @@ -17,6 +17,7 @@ use clap_verbosity_flag::Verbosity; use golem_cli::command::server::{RunArgs, ServerSubcommand}; use golem_cli::command_handler::CommandHandlerHooks; use golem_cli::context::Context; +use golem_cli::model::app::ResolvedLocalServer; use std::path::{Path, PathBuf}; use std::sync::Arc; use tracing::debug; @@ -42,25 +43,15 @@ impl CommandHandlerHooks for ServerCommandHandler { ); } - let data_dir = match &args.data_dir { - Some(data_dir) => data_dir.to_path_buf(), - None => default_data_dir()?, - }; + let launch_args = launch_args_from_run_args_and_manifest(&args, &ctx)?; + let data_dir = launch_args.data_dir.clone(); if args.clean && tokio::fs::metadata(&data_dir).await.is_ok() { clean_data_dir(&data_dir).await?; }; - let mut join_set = launch_golem_services(&LaunchArgs { - router_addr: args.router_addr().to_string(), - router_port: args.router_port(), - custom_request_port: args.custom_request_port(), - mcp_port: args.mcp_port(), - ports_file: args.ports_file.clone(), - data_dir: data_dir.clone(), - agent_filesystem_root: args.agent_filesystem_root.clone(), - }) - .await - .map_err(|err| map_local_server_startup_error(err, &data_dir))?; + let mut join_set = launch_golem_services(&launch_args) + .await + .map_err(|err| map_local_server_startup_error(err, &data_dir))?; while let Some(res) = join_set.join_next().await { res??; @@ -116,8 +107,131 @@ fn default_data_dir() -> anyhow::Result { .join("golem")) } +fn launch_args_from_run_args_and_manifest( + args: &RunArgs, + ctx: &Context, +) -> anyhow::Result { + launch_args_from_run_args_and_local_server(args, ctx.manifest_local_server()) +} + +fn launch_args_from_run_args_and_local_server( + args: &RunArgs, + local_server: Option<&ResolvedLocalServer>, +) -> anyhow::Result { + Ok(LaunchArgs { + router_addr: args + .router_addr + .clone() + .or_else(|| local_server.and_then(|manifest| manifest.router_addr.clone())) + .unwrap_or_else(|| args.router_addr().to_string()), + router_port: args + .router_port + .or_else(|| local_server.and_then(|manifest| manifest.router_port)) + .unwrap_or_else(|| args.router_port()), + custom_request_port: args + .custom_request_port + .or_else(|| local_server.and_then(|manifest| manifest.custom_request_port)) + .unwrap_or_else(|| args.custom_request_port()), + mcp_port: args + .mcp_port + .or_else(|| local_server.and_then(|manifest| manifest.mcp_port)) + .unwrap_or_else(|| args.mcp_port()), + ports_file: args + .ports_file + .clone() + .or_else(|| local_server.and_then(|manifest| manifest.ports_file.clone())), + data_dir: args + .data_dir + .clone() + .or_else(|| local_server.and_then(|manifest| manifest.data_dir.clone())) + .map(Ok) + .unwrap_or_else(default_data_dir)?, + agent_filesystem_root: args + .agent_filesystem_root + .clone() + .or_else(|| local_server.and_then(|manifest| manifest.agent_filesystem_root.clone())), + }) +} + async fn clean_data_dir(data_dir: &Path) -> anyhow::Result<()> { tokio::fs::remove_dir_all(&data_dir) .await .map_err(|err| anyhow!("Failed cleaning data dir ({}): {}", data_dir.display(), err)) } + +#[cfg(test)] +mod tests { + use super::*; + use golem_cli::model::app_raw::LocalServer; + use test_r::test; + + fn local_server(value: LocalServer) -> ResolvedLocalServer { + ResolvedLocalServer::from_raw_with_base_dir(&value, Path::new("/tmp/test-app")) + } + + #[test] + fn manifest_local_server_values_are_used_when_cli_args_are_absent() { + let manifest = local_server(LocalServer { + router_addr: Some("127.0.0.1".to_string()), + router_port: Some(9882), + custom_request_port: Some(9008), + mcp_port: Some(9009), + ports_file: Some(PathBuf::from("/tmp/test-app/.golem/ports.json")), + data_dir: Some(PathBuf::from("/tmp/test-app/.golem/data")), + agent_filesystem_root: Some(PathBuf::from("/tmp/test-app/.golem/agents")), + }); + + let args = launch_args_from_run_args_and_local_server(&RunArgs::default(), Some(&manifest)) + .unwrap(); + + assert_eq!(args.router_addr, "127.0.0.1"); + assert_eq!(args.router_port, 9882); + assert_eq!(args.custom_request_port, 9008); + assert_eq!(args.mcp_port, 9009); + assert_eq!( + args.ports_file, + Some(PathBuf::from("/tmp/test-app/.golem/ports.json")) + ); + assert_eq!(args.data_dir, PathBuf::from("/tmp/test-app/.golem/data")); + assert_eq!( + args.agent_filesystem_root, + Some(PathBuf::from("/tmp/test-app/.golem/agents")) + ); + } + + #[test] + fn cli_args_override_manifest_local_server_values() { + let manifest = local_server(LocalServer { + router_addr: Some("127.0.0.1".to_string()), + router_port: Some(9882), + custom_request_port: Some(9008), + mcp_port: Some(9009), + ports_file: Some(PathBuf::from("/tmp/test-app/.golem/ports.json")), + data_dir: Some(PathBuf::from("/tmp/test-app/.golem/data")), + agent_filesystem_root: Some(PathBuf::from("/tmp/test-app/.golem/agents")), + }); + let run_args = RunArgs { + router_addr: Some("0.0.0.0".to_string()), + router_port: Some(10000), + custom_request_port: Some(10001), + mcp_port: Some(10002), + ports_file: Some(PathBuf::from("cli-ports.json")), + data_dir: Some(PathBuf::from("cli-data")), + clean: false, + agent_filesystem_root: Some(PathBuf::from("cli-agents")), + }; + + let args = launch_args_from_run_args_and_local_server(&run_args, Some(&manifest)).unwrap(); + + assert_eq!(args.router_addr, "0.0.0.0"); + assert_eq!(args.router_port, 10000); + assert_eq!(args.custom_request_port, 10001); + assert_eq!(args.mcp_port, 10002); + assert_eq!(args.ports_file, Some(PathBuf::from("cli-ports.json"))); + assert_eq!(args.data_dir, PathBuf::from("cli-data")); + assert_eq!( + args.agent_filesystem_root, + Some(PathBuf::from("cli-agents")) + ); + } +} diff --git a/cli/schema.golem.cloud/app/golem/1.6.0-dev.3/golem.schema.json b/cli/schema.golem.cloud/app/golem/1.6.0-dev.3/golem.schema.json new file mode 100644 index 0000000000..30a83b6355 --- /dev/null +++ b/cli/schema.golem.cloud/app/golem/1.6.0-dev.3/golem.schema.json @@ -0,0 +1,1903 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "$id": "https://schema.golem.cloud/app/golem/1.6.0-dev.3/golem.schema.json", + "title": "Golem Application Manifest", + "description": "Golem Application Manifest.", + "type": "object", + "additionalProperties": false, + "properties": { + "manifestVersion": { + "type": "string", + "description": "Application manifest document version" + }, + "app": { + "type": "string", + "description": "Application name" + }, + "includes": { + "type": "array", + "description": "Include paths or globs for searching for application manifest documents. Only allowed in root application manifest documents.", + "items": { + "type": "string" + } + }, + "componentTemplates": { + "type": "object", + "description": "Component templates by template names", + "additionalProperties": { + "$ref": "#/definitions/componentTemplate" + } + }, + "components": { + "type": "object", + "description": "Components by component names", + "additionalProperties": { + "$ref": "#/definitions/component" + } + }, + "agents": { + "type": "object", + "description": "Agents by agent type names", + "additionalProperties": { + "$ref": "#/definitions/agent" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + }, + "httpApi": { + "$ref": "#/definitions/httpApi" + }, + "mcp": { + "$ref": "#/definitions/mcp" + }, + "localServer": { + "$ref": "#/definitions/localServer" + }, + "environments": { + "type": "object", + "description": "Application environments", + "additionalProperties": { + "$ref": "#/definitions/environment" + } + }, + "bridge": { + "$ref": "#/definitions/bridgeSdks" + }, + "secretDefaults": { + "type": "object", + "description": "Secret defaults by environment name, using nested config-style object paths.", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } + }, + "retryPolicyDefaults": { + "type": "object", + "description": "Retry policy defaults by environment name. Policies defined here are created in the environment during deployment if they don't already exist.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/retryPolicyDefault" + } + } + }, + "resourceDefaults": { + "type": "object", + "description": "Quota resource defaults by environment name.", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/resourceDefinitionCreation" + } + } + } + }, + "definitions": { + "lenientTokenList": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "templates": { + "description": "List of parent templates", + "type": "object", + "additionalProperties": false, + "properties": { + "templates": { + "$ref": "#/definitions/lenientTokenList" + } + } + }, + "componentTemplate": { + "description": "Component template definition", + "type": "object", + "additionalProperties": false, + "properties": { + "templates": { + "$ref": "#/definitions/lenientTokenList" + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "outputWasm": { + "type": "string", + "description": "File path for the output WASM component which is ready to be uploaded to Golem." + }, + "buildMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/buildCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + }, + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "presets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/componentPreset" + } + } + } + }, + "component": { + "description": "Component definition", + "type": "object", + "additionalProperties": false, + "properties": { + "templates": { + "$ref": "#/definitions/lenientTokenList" + }, + "dir": { + "type": "string", + "description": "Base directory for resolving component paths" + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "outputWasm": { + "type": "string", + "description": "File path for the output WASM component which is ready to be uploaded to Golem." + }, + "buildMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/buildCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + }, + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "presets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/componentPreset" + } + } + } + }, + "componentLayerProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "outputWasm": { + "type": "string", + "description": "File path for the output WASM component which is ready to be uploaded to Golem." + }, + "buildMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/buildCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + } + } + }, + "componentPresets": { + "type": "object", + "description": "Component definition presets", + "additionalProperties": false, + "properties": { + "presets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/componentPreset" + } + } + } + }, + "componentPreset": { + "type": "object", + "additionalProperties": false, + "properties": { + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "componentWasm": { + "type": "string", + "description": "File path for the built WASM component." + }, + "outputWasm": { + "type": "string", + "description": "File path for the output WASM component which is ready to be uploaded to Golem." + }, + "buildMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "build": { + "type": "array", + "description": "Commands used for creating component WASM.", + "items": { + "$ref": "#/definitions/buildCommand" + } + }, + "customCommands": { + "type": "object", + "description": "User defined custom commands.", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/externalCommand" + } + } + }, + "clean": { + "type": "array", + "description": "User defined extra paths used in the clean command.", + "items": { + "type": "string" + } + }, + "default": { + "const": true + } + } + }, + "agent": { + "description": "Agent definition", + "type": "object", + "additionalProperties": false, + "properties": { + "templates": { + "$ref": "#/definitions/lenientTokenList" + }, + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "presets": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/agentPreset" + } + } + } + }, + "agentPreset": { + "type": "object", + "additionalProperties": false, + "properties": { + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + }, + "default": { + "const": true + } + } + }, + "agentLayerProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "config": {}, + "envMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "env": { + "type": "object", + "description": "Environment variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "wasiConfigMergeMode": { + "$ref": "#/definitions/mapMergeMode" + }, + "wasiConfig": { + "type": "object", + "description": "WASI configuration variables for the agent.", + "additionalProperties": { + "type": "string" + } + }, + "pluginsMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "plugins": { + "type": "array", + "description": "Installed plugins for the agent", + "items": { + "$ref": "#/definitions/pluginInstallation" + } + }, + "filesMergeMode": { + "$ref": "#/definitions/vecMergeMode" + }, + "files": { + "type": "array", + "description": "Initial component file system", + "items": { + "$ref": "#/definitions/initialComponentFile" + } + } + } + }, + "httpApi": { + "type": "object", + "additionalProperties": false, + "description": "HTTP API deployments", + "properties": { + "deployments": { + "type": "object", + "description": "HTTP API deployments by environments", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/httpApiDeployment" + } + } + } + } + }, + "httpApiDeployment": { + "type": "object", + "additionalProperties": false, + "description": "HTTP API deployment", + "properties": { + "domain": { + "type": "string", + "description": "Full concrete domain for the HTTP API deployment. Use this for custom domains and custom server environments." + }, + "subdomain": { + "type": "string", + "description": "Single DNS label resolved through the deployment environment's built-in server. For local HTTP API deployments this resolves to .localhost:9006 by default, or .localhost: when localServer.customRequestPort is set. For cloud HTTP API deployments this resolves to .apps.golem.cloud." + }, + "webhookUrl": { + "type": "string", + "description": "Webhook URL prefix" + }, + "openapiEndpoint": { + "type": "string", + "description": "OpenApi endpoint URL prefix" + }, + "agents": { + "type": "object", + "description": "HTTP API deployment options by agent type name", + "additionalProperties": { + "$ref": "#/definitions/httpApiDeploymentAgentOptions" + } + } + }, + "oneOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "subdomain" + ] + } + ] + }, + "httpApiDeploymentAgentOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "securityScheme": { + "type": "string" + }, + "testSessionHeaderName": { + "type": "string" + } + } + }, + "mcp": { + "type": "object", + "additionalProperties": false, + "description": "MCP deployments", + "properties": { + "deployments": { + "type": "object", + "description": "MCP deployments by environments", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/definitions/mcpDeployment" + } + } + } + } + }, + "mcpDeployment": { + "type": "object", + "additionalProperties": false, + "properties": { + "domain": { + "type": "string", + "description": "Full concrete domain for the MCP deployment. Use this for custom domains and custom server environments." + }, + "subdomain": { + "type": "string", + "description": "Single DNS label resolved through the deployment environment's built-in server. For local MCP deployments this resolves to .localhost:9007 by default, or .localhost: when localServer.mcpPort is set. For cloud MCP deployments this resolves to .mcps.golem.cloud." + }, + "agents": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/mcpDeploymentAgentOptions" + } + } + }, + "oneOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "subdomain" + ] + } + ] + }, + "mcpDeploymentAgentOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "securityScheme": { + "type": "string" + } + } + }, + "localServer": { + "type": "object", + "description": "Defaults for `golem server run` when launched from this application.", + "additionalProperties": false, + "properties": { + "routerAddr": { + "type": "string", + "description": "Address to serve the main API on." + }, + "routerPort": { + "type": "integer", + "minimum": 0, + "maximum": 65535, + "description": "Port to serve the main API on." + }, + "customRequestPort": { + "type": "integer", + "minimum": 0, + "maximum": 65535, + "description": "Port to serve custom requests on." + }, + "mcpPort": { + "type": "integer", + "minimum": 0, + "maximum": 65535, + "description": "Port to serve the MCP server on." + }, + "portsFile": { + "type": "string", + "description": "Path where discovered startup ports are written as JSON." + }, + "dataDir": { + "type": "string", + "description": "Directory used for local server data." + }, + "agentFilesystemRoot": { + "type": "string", + "description": "Root directory for deterministic agent filesystem directories." + } + } + }, + "environment": { + "type": "object", + "additionalProperties": false, + "properties": { + "default": { + "const": true, + "description": "Use as default environment, only one can be selected, if missing, the first environment is used as default" + }, + "account": { + "type": "string", + "description": "Optional account that owns the application environment." + }, + "server": { + "$ref": "#/definitions/server" + }, + "componentPresets": { + "$ref": "#/definitions/lenientTokenList" + }, + "cli": { + "$ref": "#/definitions/cliOptions" + }, + "deployment": { + "$ref": "#/definitions/deploymentOptions" + } + } + }, + "server": { + "description": "Server for the environment, can be either the built-in local/cloud servers, or a custom one.", + "oneOf": [ + { + "const": "local" + }, + { + "const": "cloud" + }, + { + "$ref": "#/definitions/customServer" + } + ] + }, + "customServer": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string", + "description": "Custom URL for golem services" + }, + "workerUrl": { + "type": "string", + "description": "Custom URL for golem worker service" + }, + "allowInsecure": { + "type": "boolean", + "description": "Allow insecure connections to the server" + }, + "auth": { + "$ref": "#/definitions/customServerAuth" + } + }, + "required": [ + "url", + "auth" + ] + }, + "customServerAuth": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "properties": { + "oauth2": { + "const": true + } + }, + "required": [ + "oauth2" + ] + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "staticToken": { + "type": "string" + } + }, + "required": [ + "staticToken" + ] + } + ] + }, + "cliOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "format": { + "enum": [ + "text", + "json", + "yaml", + "pretty", + "pretty-json", + "pretty-yaml", + "toon" + ], + "description": "Default output format" + }, + "autoConfirm": { + "const": true, + "description": "Enables auto-confirm (yes) flag by default" + }, + "redeployAgents": { + "const": true, + "description": "Enables redeploy-agents flag by default" + }, + "reset": { + "const": true, + "description": "Enables reset flag by default" + } + } + }, + "deploymentOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "compatibilityCheck": { + "type": "boolean" + }, + "versionCheck": { + "type": "boolean" + }, + "securityOverrides": { + "type": "boolean" + } + } + }, + "buildCommand": { + "oneOf": [ + { + "$ref": "#/definitions/externalCommand" + }, + { + "$ref": "#/definitions/generateQuickJsCrateCommand" + }, + { + "$ref": "#/definitions/generateQuickJsdtsCommand" + }, + { + "$ref": "#/definitions/injectToPrebuiltQuickjsCommand" + }, + { + "$ref": "#/definitions/preinitializeJsCommand" + } + ] + }, + "externalCommand": { + "type": "object", + "additionalProperties": false, + "description": "External command with optional inputs and outputs with up-to-date checks", + "properties": { + "command": { + "type": "string", + "description": "External command to execute" + }, + "dir": { + "type": "string", + "description": "Working directory for the command" + }, + "env": { + "type": "object", + "description": "Environment variables for the command", + "additionalProperties": { + "type": "string" + } + }, + "rmdirs": { + "type": "array", + "description": "List of directories that should be deleted before running the command, runs before mkdirs.", + "items": { + "type": "string" + } + }, + "mkdirs": { + "type": "array", + "description": "List of directories that should be created before running the command, runs after rmdirs.", + "items": { + "type": "string" + } + }, + "sources": { + "type": "array", + "description": "Inputs (paths and globs) for the external command", + "items": { + "type": "string" + } + }, + "targets": { + "type": "array", + "description": "Outputs (paths and globs) for the external command", + "items": { + "type": "string" + } + } + }, + "required": [ + "command" + ] + }, + "generateQuickJsCrateCommand": { + "type": "object", + "additionalProperties": false, + "description": "Generate QuickJS crate", + "properties": { + "generateQuickjsCrate": { + "type": "string", + "description": "QuickJS crate path" + }, + "wit": { + "type": "string", + "description": "WIT directory" + }, + "jsModules": { + "type": "object", + "description": "JS module paths and modes", + "additionalProperties": { + "type": "string" + } + }, + "world": { + "type": "string", + "description": "Optional WIT world" + } + }, + "required": [ + "generateQuickjsCrate", + "wit", + "jsModules" + ] + }, + "generateQuickJsdtsCommand": { + "type": "object", + "additionalProperties": false, + "description": "Generate QuickJS d.ts", + "properties": { + "generateQuickjsDts": { + "type": "string", + "description": "QuickJS d.ts path" + }, + "wit": { + "type": "string", + "description": "WIT directory" + }, + "world": { + "type": "string", + "description": "Optional WIT world" + } + }, + "required": [ + "generateQuickjsDts", + "wit" + ] + }, + "injectToPrebuiltQuickjsCommand": { + "type": "object", + "additionalProperties": false, + "description": "Inject JS to prebuilt QuickJS", + "properties": { + "injectToPrebuiltQuickjs": { + "type": "string", + "description": "Path to the prebuilt QuickJS WASM file that loads a JS module through a get-script import" + }, + "module": { + "type": "string", + "description": "Path to the JS module" + }, + "into": { + "type": "string", + "description": "Path to the output WASM component containing the injected JS module" + } + }, + "required": [ + "injectToPrebuiltQuickjs", + "module", + "into" + ] + }, + "preinitializeJsCommand": { + "type": "object", + "additionalProperties": false, + "description": "Preinitialize JS runtime", + "properties": { + "preinitializeJs": { + "type": "string", + "description": "Path to the input WASM component to pre-initialize" + }, + "into": { + "type": "string", + "description": "Path to the pre-initialized output WASM component" + } + }, + "required": [ + "preinitializeJs", + "into" + ] + }, + "initialComponentFile": { + "type": "object", + "additionalProperties": false, + "description": "File entry for the initial component file system.", + "properties": { + "sourcePath": { + "type": "string", + "description": "Source path for the component file: either a local file or a URL." + }, + "targetPath": { + "type": "string", + "description": "Target path for the component file, must be an absolute path" + }, + "permissions": { + "enum": [ + "read-only", + "read-write" + ], + "description": "Permission for the component file" + } + }, + "required": [ + "sourcePath", + "targetPath" + ] + }, + "pluginInstallation": { + "type": "object", + "additionalProperties": false, + "description": "Represents an installed plugin", + "properties": { + "account": { + "type": "string", + "description": "Account of the plugin" + }, + "name": { + "type": "string", + "description": "Name of the plugin" + }, + "version": { + "type": "string", + "description": "Version of the plugin" + }, + "parameters": { + "type": "object", + "description": "Key-value pairs for configuring the plugin installation", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "name", + "version" + ] + }, + "bridgeSdks": { + "description": "Bridge SDK generator configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "ts": { + "description": "TypeScript SDK configuration", + "$ref": "#/definitions/bridgeSdkLanguageTargets" + }, + "rust": { + "description": "Rust SDK configuration", + "$ref": "#/definitions/bridgeSdkLanguageTargets" + }, + "scala": { + "description": "Scala SDK configuration", + "$ref": "#/definitions/bridgeSdkLanguageTargets" + }, + "moonbit": { + "description": "MoonBit SDK configuration", + "$ref": "#/definitions/bridgeSdkLanguageTargets" + } + } + }, + "bridgeSdkLanguageTargets": { + "description": "Bridge SDK language targets", + "type": "object", + "additionalProperties": false, + "properties": { + "agents": { + "description": "List of agent type names, component names or \"*\" for including all agents", + "$ref": "#/definitions/lenientTokenList" + }, + "outputDir": { + "description": "Custom output directory for the generated SDK", + "type": "string" + } + } + }, + "vecMergeMode": { + "enum": [ + "append", + "prepend", + "replace" + ] + }, + "mapMergeMode": { + "enum": [ + "upsert", + "replace", + "remove" + ] + }, + "retryPolicyDefault": { + "type": "object", + "description": "Retry policy default body for a policy key under retryPolicyDefaults.", + "properties": { + "priority": { + "type": "integer", + "minimum": 0, + "description": "Selection priority - higher values are evaluated first" + }, + "predicate": { + "$ref": "#/definitions/retryPredicate", + "description": "Predicate tree defining when this policy applies" + }, + "policy": { + "$ref": "#/definitions/retryPolicy", + "description": "Retry policy tree defining the retry strategy" + } + }, + "required": [ + "priority", + "predicate", + "policy" + ], + "additionalProperties": false + }, + "predicateValue": { + "description": "Predicate value encoded as primitive JSON value.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "integer" + }, + { + "type": "boolean" + } + ] + }, + "propertyComparison": { + "type": "object", + "description": "A property name and a value to compare against", + "properties": { + "property": { + "type": "string" + }, + "value": { + "$ref": "#/definitions/predicateValue" + } + }, + "required": [ + "property", + "value" + ], + "additionalProperties": false + }, + "propertySetCheck": { + "type": "object", + "description": "A property name and a set of values to check membership", + "properties": { + "property": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "$ref": "#/definitions/predicateValue" + } + } + }, + "required": [ + "property", + "values" + ], + "additionalProperties": false + }, + "retryPredicate": { + "description": "Composable retry predicate encoded as compact unions.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": [ + "true", + "false" + ] + }, + { + "type": "object", + "properties": { + "propEq": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propEq"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propNeq": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propNeq"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propGt": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propGt"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propGte": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propGte"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propLt": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propLt"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propLte": { + "$ref": "#/definitions/propertyComparison" + } + }, + "required": ["propLte"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propExists": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "property": { + "type": "string" + } + }, + "required": ["property"], + "additionalProperties": false + } + ] + } + }, + "required": ["propExists"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propIn": { + "$ref": "#/definitions/propertySetCheck" + } + }, + "required": ["propIn"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propMatches": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "pattern": { + "type": "string" + } + }, + "required": ["property", "pattern"], + "additionalProperties": false + } + }, + "required": ["propMatches"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propStartsWith": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "prefix": { + "type": "string" + } + }, + "required": ["property", "prefix"], + "additionalProperties": false + } + }, + "required": ["propStartsWith"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "propContains": { + "type": "object", + "properties": { + "property": { + "type": "string" + }, + "substring": { + "type": "string" + } + }, + "required": ["property", "substring"], + "additionalProperties": false + } + }, + "required": ["propContains"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "and": { + "type": "array", + "items": { + "$ref": "#/definitions/retryPredicate" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["and"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "or": { + "type": "array", + "items": { + "$ref": "#/definitions/retryPredicate" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["or"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "not": { + "$ref": "#/definitions/retryPredicate" + } + }, + "required": ["not"], + "additionalProperties": false + } + ] + }, + "duration": { + "type": "object", + "description": "A duration with seconds and nanoseconds", + "properties": { + "secs": { + "type": "integer", + "minimum": 0 + }, + "nanos": { + "type": "integer", + "minimum": 0, + "maximum": 999999999 + } + }, + "required": [ + "secs", + "nanos" + ], + "additionalProperties": false + }, + "retryPolicy": { + "description": "Composable retry policy encoded as compact unions.", + "oneOf": [ + { + "type": "string", + "enum": [ + "immediate", + "never" + ] + }, + { + "type": "object", + "properties": { + "periodic": { + "$ref": "#/definitions/duration" + } + }, + "required": ["periodic"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "exponential": { + "type": "object", + "properties": { + "baseDelay": { + "$ref": "#/definitions/duration" + }, + "factor": { + "type": "number" + } + }, + "required": ["baseDelay", "factor"], + "additionalProperties": false + } + }, + "required": ["exponential"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "fibonacci": { + "type": "object", + "properties": { + "first": { + "$ref": "#/definitions/duration" + }, + "second": { + "$ref": "#/definitions/duration" + } + }, + "required": ["first", "second"], + "additionalProperties": false + } + }, + "required": ["fibonacci"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "countBox": { + "type": "object", + "properties": { + "maxRetries": { + "type": "integer", + "minimum": 0 + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["maxRetries", "inner"], + "additionalProperties": false + } + }, + "required": ["countBox"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "timeBox": { + "type": "object", + "properties": { + "limit": { + "$ref": "#/definitions/duration" + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["limit", "inner"], + "additionalProperties": false + } + }, + "required": ["timeBox"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "clamp": { + "type": "object", + "properties": { + "minDelay": { + "$ref": "#/definitions/duration" + }, + "maxDelay": { + "$ref": "#/definitions/duration" + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["minDelay", "maxDelay", "inner"], + "additionalProperties": false + } + }, + "required": ["clamp"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "addDelay": { + "type": "object", + "properties": { + "delay": { + "$ref": "#/definitions/duration" + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["delay", "inner"], + "additionalProperties": false + } + }, + "required": ["addDelay"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "jitter": { + "type": "object", + "properties": { + "factor": { + "type": "number" + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["factor", "inner"], + "additionalProperties": false + } + }, + "required": ["jitter"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "filteredOn": { + "type": "object", + "properties": { + "predicate": { + "$ref": "#/definitions/retryPredicate" + }, + "inner": { + "$ref": "#/definitions/retryPolicy" + } + }, + "required": ["predicate", "inner"], + "additionalProperties": false + } + }, + "required": ["filteredOn"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "andThen": { + "type": "array", + "items": { + "$ref": "#/definitions/retryPolicy" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["andThen"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "union": { + "type": "array", + "items": { + "$ref": "#/definitions/retryPolicy" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["union"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "intersect": { + "type": "array", + "items": { + "$ref": "#/definitions/retryPolicy" + }, + "minItems": 2, + "maxItems": 2 + } + }, + "required": ["intersect"], + "additionalProperties": false + } + ] + }, + "resourceDefinitionCreation": { + "type": "object", + "description": "Quota resource definition body for a resource key under resourceDefaults.", + "additionalProperties": false, + "properties": { + "limit": { + "$ref": "#/definitions/resourceLimit" + }, + "enforcementAction": { + "$ref": "#/definitions/enforcementAction" + }, + "unit": { + "type": "string", + "description": "Single unit of measurement (e.g., token, request)" + }, + "units": { + "type": "string", + "description": "Multiple units of measurement (e.g., tokens, requests)" + } + }, + "required": [ + "limit", + "enforcementAction", + "unit", + "units" + ] + }, + "resourceLimit": { + "oneOf": [ + { + "$ref": "#/definitions/resourceRateLimit" + }, + { + "$ref": "#/definitions/resourceCapacityLimit" + }, + { + "$ref": "#/definitions/resourceConcurrencyLimit" + } + ] + }, + "resourceRateLimit": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "Rate" + ] + }, + "value": { + "type": "integer", + "minimum": 0 + }, + "period": { + "$ref": "#/definitions/timePeriod" + }, + "max": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "type", + "value", + "period", + "max" + ] + }, + "resourceCapacityLimit": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "Capacity" + ] + }, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + "resourceConcurrencyLimit": { + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "enum": [ + "Concurrency" + ] + }, + "value": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "type", + "value" + ] + }, + "enforcementAction": { + "type": "string", + "enum": [ + "reject", + "throttle", + "terminate" + ] + }, + "timePeriod": { + "type": "string", + "enum": [ + "second", + "minute", + "hour", + "day", + "month", + "year" + ] + } + } +} diff --git a/docs/src/content/next/app-manifest.mdx b/docs/src/content/next/app-manifest.mdx index 7d72b30f76..7b0f811f33 100644 --- a/docs/src/content/next/app-manifest.mdx +++ b/docs/src/content/next/app-manifest.mdx @@ -116,6 +116,24 @@ Available naming related string transforming functions: + + ***Optional local server defaults*** used by `golem server run` and by local deployment subdomain expansion. + + + + ***Optional HTTP API port*** for local custom requests. Defaults to `9006`; HTTP deployments using `subdomain` resolve to `.localhost:`. + + + + ***Optional MCP server port*** for local MCP requests. Defaults to `9007`; MCP deployments using `subdomain` resolve to `.localhost:`. + + [*].domain", since: Release.R_1_5_0 }}> - ***Required domain*** for the deployment. + ***Full concrete domain*** for the deployment. Use `domain` for custom registered domains and custom server environments. Define exactly one of `domain` or `subdomain`. + + + [*].subdomain", + since: Release.R_1_5_0 + }}> + ***Built-in server subdomain*** for the deployment. Must be a single DNS label. In a local environment it resolves to `.localhost:9006` by default, or `.localhost:` when `localServer.customRequestPort` is set. In a cloud environment it resolves to `.apps.golem.cloud`. Define exactly one of `domain` or `subdomain`. [*].domain", since: Release.R_1_5_0 }}> - ***Required domain*** for the MCP deployment. + ***Full concrete domain*** for the MCP deployment. Use `domain` for custom registered domains and custom server environments. Define exactly one of `domain` or `subdomain`. + + + [*].subdomain", + since: Release.R_1_5_0 + }}> + ***Built-in server subdomain*** for the MCP deployment. Must be a single DNS label. In a local environment it resolves to `.localhost:9007` by default, or `.localhost:` when `localServer.mcpPort` is set. In a cloud environment it resolves to `.mcps.golem.cloud`. Define exactly one of `domain` or `subdomain`. myapp.localhost:9006 app-component-a-api app-component-b-api @@ -143,6 +143,8 @@ Application custom commands: ts-npm-install ``` +Deployment domains shown by the CLI include the manifest intent and the resolved concrete domain. For example, an HTTP API manifest entry such as `subdomain: myapp` is displayed as `subdomain: myapp -> myapp.localhost:9006` with the default local server port. + ## Starting the local development server The `golem` CLI also includes a local development server that can be used to quickly develop, deploy, test and debug Golem applications locally. @@ -243,4 +245,4 @@ export GOLEM_ENVIRONMENT=cloud golem deploy ``` -For more information see the next seection about [Environments and Profiles](/next/cli/envs_and_profiles). \ No newline at end of file +For more information see the next seection about [Environments and Profiles](/next/cli/envs_and_profiles). diff --git a/docs/src/content/next/develop/webhooks.mdx b/docs/src/content/next/develop/webhooks.mdx index e9835f51f6..092ed71d00 100644 --- a/docs/src/content/next/develop/webhooks.mdx +++ b/docs/src/content/next/develop/webhooks.mdx @@ -164,7 +164,7 @@ To configure the base URL for webhooks and deploy agents with webhook support, u httpApi: deployments: local: - - domain: my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default webhookUrl: http://my-app.localhost:9006 agents: WebhookAgent: {} diff --git a/docs/src/content/next/how-to-guides/common/golem-configure-api-domain.mdx b/docs/src/content/next/how-to-guides/common/golem-configure-api-domain.mdx index 88353527d4..3c350f4ed4 100644 --- a/docs/src/content/next/how-to-guides/common/golem-configure-api-domain.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-configure-api-domain.mdx @@ -2,7 +2,7 @@ ## Overview -After adding HTTP mounts and endpoints to agents in code, you must configure a **domain deployment** in `golem.yaml` so Golem knows which agents to expose and on which domain. This skill covers the `httpApi` manifest section, security scheme setup, and the auto-generated OpenAPI specification. +After adding HTTP mounts and endpoints to agents in code, you must configure an HTTP API deployment in `golem.yaml` so Golem knows which agents to expose and on which domain. This skill covers the `httpApi` manifest section, security scheme setup, and the auto-generated OpenAPI specification. ## Adding a Domain Deployment @@ -12,7 +12,7 @@ Add an `httpApi` section to the root `golem.yaml`: httpApi: deployments: local: - - domain: my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default agents: TaskAgent: {} UserAgent: {} @@ -23,9 +23,12 @@ httpApi: - `httpApi.deployments` is a map keyed by **environment name** (e.g., `local`, `staging`, `prod`) - Each environment contains a list of deployment objects - Each deployment has: - - `domain`: the (sub)domain to bind to (e.g., `my-app.localhost:9006` for local development) + - `subdomain`: a single DNS label resolved through the target environment server. Local HTTP API deployments resolve to `.localhost:9006` by default, or `.localhost:` when `localServer.customRequestPort` is set. Cloud HTTP API deployments resolve to `.apps.golem.cloud`. + - `domain`: a full domain such as `api.example.com` for custom registered domains or custom server environments. - `agents`: a map of agent type names (PascalCase) to their deployment options - - `webhookUrl` (optional): base URL for webhook callbacks + - `webhookUrl` (optional): path prefix for webhook callbacks; defaults to `/webhooks/` + +Define exactly one of `subdomain` or `domain` on each deployment. Prefer `subdomain` for built-in `server: local` and `server: cloud` environments. Use `domain` only when you need a full custom domain or the environment uses a custom server. ### Agent Options @@ -55,7 +58,7 @@ golem api security-scheme create my-oidc \ --provider-type google \ --client-id "YOUR_CLIENT_ID" \ --client-secret "YOUR_CLIENT_SECRET" \ - --redirect-url "http://localhost:9006/auth/callback" \ + --redirect-url "http://my-app.localhost:9006/auth/callback" \ --scope openid --scope email --scope profile ``` @@ -97,7 +100,7 @@ After creating a security scheme, reference it by name in the agent deployment o httpApi: deployments: local: - - domain: my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default agents: SecureAgent: securityScheme: my-oidc @@ -113,7 +116,7 @@ For local development without a real OIDC provider, use a test session header: httpApi: deployments: local: - - domain: my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default agents: SecureAgent: testSessionHeaderName: X-Test-Auth @@ -152,34 +155,40 @@ Define different domains and security configurations per environment: httpApi: deployments: local: - - domain: my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default agents: TaskAgent: {} SecureAgent: testSessionHeaderName: X-Test-Auth - prod: - - domain: api.myapp.com + cloud: + - subdomain: my-app # resolves to my-app.apps.golem.cloud agents: TaskAgent: {} SecureAgent: securityScheme: prod-google-oidc + +environments: + local: + server: local + cloud: + server: cloud ``` ## Webhook URL -If agents use webhooks, configure the base URL: +If agents use webhooks, configure the webhook path prefix: ```yaml httpApi: deployments: local: - - domain: my-app.localhost:9006 - webhookUrl: http://my-app.localhost:9006 + - subdomain: my-app # resolves to my-app.localhost:9006 by default + webhookUrl: /my-custom-webhooks/ agents: WebhookAgent: {} ``` -The `webhookUrl` is combined with the agent's `webhookSuffix` (defined in code) to form the full webhook callback URL. +The deployment domain comes from `subdomain` or `domain`. The `webhookUrl` path prefix is combined with the agent's `webhookSuffix` (defined in code) and the generated webhook ID to form the full callback URL, such as `http://my-app.localhost:9006/my-custom-webhooks/order-hooks/`. ## Deploying @@ -209,18 +218,24 @@ This specification includes all endpoints from all agents deployed to that domai httpApi: deployments: local: - - domain: task-app.localhost:9006 - webhookUrl: http://task-app.localhost:9006 + - subdomain: task-app # resolves to task-app.localhost:9006 by default + webhookUrl: /webhooks/ agents: TaskAgent: {} AdminAgent: testSessionHeaderName: X-Admin-Auth - prod: - - domain: api.taskapp.com + cloud: + - subdomain: task-app # resolves to task-app.apps.golem.cloud agents: TaskAgent: {} AdminAgent: securityScheme: google-oidc + +environments: + local: + server: local + cloud: + server: cloud ``` ## Key Constraints @@ -228,5 +243,5 @@ httpApi: - Agent type names in `golem.yaml` use **PascalCase** (matching the class/trait name in code) - Each agent entry can have at most one of `securityScheme` or `testSessionHeaderName` - Security schemes must be created via `golem api security-scheme create` before they can be referenced -- The domain must be unique per environment +- The resolved domain must be unique per environment - After changing `golem.yaml`, run `golem deploy --yes` to apply changes diff --git a/docs/src/content/next/how-to-guides/common/golem-configure-mcp-server.mdx b/docs/src/content/next/how-to-guides/common/golem-configure-mcp-server.mdx index a84e6f6281..6d84c1172b 100644 --- a/docs/src/content/next/how-to-guides/common/golem-configure-mcp-server.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-configure-mcp-server.mdx @@ -14,7 +14,7 @@ Add an `mcp` section to the root `golem.yaml`: mcp: deployments: local: - - domain: my-app.localhost:9007 + - subdomain: my-app # resolves to my-app.localhost:9007 by default agents: CounterAgent: {} TaskAgent: {} @@ -25,9 +25,12 @@ mcp: - `mcp.deployments` is a map keyed by **environment name** (e.g., `local`, `cloud`, `staging`) - Each environment contains a list of deployment objects - Each deployment has: - - `domain`: the (sub)domain to bind to + - `subdomain`: a single DNS label resolved through the target environment server. Local MCP deployments resolve to `.localhost:9007` by default, or `.localhost:` when `localServer.mcpPort` is set. Cloud MCP deployments resolve to `.mcps.golem.cloud`. + - `domain`: a full domain such as `mcp.example.com` for custom registered domains or custom server environments. - `agents`: a map of agent type names (PascalCase) to their deployment options +Define exactly one of `subdomain` or `domain` on each deployment. Prefer `subdomain` for built-in `server: local` and `server: cloud` environments. Use `domain` only when you need a full custom domain or the environment uses a custom server. + ### Local Development For local development, the MCP server listens on port **9007** by default (separate from the HTTP API gateway on port 9006). Use `*.localhost:9007` domains: @@ -36,7 +39,7 @@ For local development, the MCP server listens on port **9007** by default (separ mcp: deployments: local: - - domain: my-app.localhost:9007 + - subdomain: my-app # resolves to my-app.localhost:9007 by default agents: MyAgent: {} ``` @@ -49,17 +52,17 @@ http://my-app.localhost:9007/mcp ### Cloud Deployment -For Golem Cloud, configure the `cloud` environment with a registered domain: +For Golem Cloud's built-in server, configure the `cloud` environment and use `subdomain`: ```yaml mcp: deployments: local: - - domain: my-app.localhost:9007 + - subdomain: my-app # resolves to my-app.localhost:9007 by default agents: MyAgent: {} cloud: - - domain: my-app.example.com + - subdomain: my-app # resolves to my-app.mcps.golem.cloud agents: MyAgent: securityScheme: my-oauth @@ -77,6 +80,18 @@ Deploy to cloud with: golem deploy --yes --cloud ``` +For custom DNS or custom server environments, use `domain` with a full domain: + +```yaml +mcp: + deployments: + prod: + - domain: mcp.example.com + agents: + MyAgent: + securityScheme: my-oauth +``` + ## Agent Options Each agent entry accepts an optional `securityScheme` field: @@ -134,7 +149,7 @@ After creating a security scheme, reference it by name: mcp: deployments: local: - - domain: my-app.localhost:9007 + - subdomain: my-app # resolves to my-app.localhost:9007 by default agents: SecureAgent: securityScheme: my-oidc @@ -235,11 +250,11 @@ Configure different domains and security settings per environment: mcp: deployments: local: - - domain: my-app.localhost:9007 + - subdomain: my-app # resolves to my-app.localhost:9007 by default agents: MyAgent: {} cloud: - - domain: mcp.myapp.com + - subdomain: my-app # resolves to my-app.mcps.golem.cloud agents: MyAgent: securityScheme: prod-google-oidc @@ -278,11 +293,11 @@ Then connect to `http://my-app.localhost:9007/mcp` using the Streamable HTTP tra mcp: deployments: local: - - domain: counter-app.localhost:9007 + - subdomain: counter-app # resolves to counter-app.localhost:9007 by default agents: CounterAgent: {} cloud: - - domain: mcp.counter-app.com + - subdomain: counter-app # resolves to counter-app.mcps.golem.cloud agents: CounterAgent: securityScheme: google-oidc @@ -300,6 +315,6 @@ environments: - The MCP server listens on port **9007** by default for local development (separate from the HTTP API gateway port 9006) - The MCP endpoint path is always `/mcp` (e.g., `http://my-app.localhost:9007/mcp`) - Security schemes must be created via `golem api security-scheme create` before they can be referenced -- The domain must be unique per environment +- The resolved domain must be unique per environment - After changing `golem.yaml`, run `golem deploy --yes` to apply changes - The OAuth callback path for MCP security schemes is `/mcp/oauth/callback` diff --git a/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx b/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx index f4410c98a2..63358b7df9 100644 --- a/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-edit-manifest.mdx @@ -341,20 +341,25 @@ When `format: toon` is used, structured stdout is emitted as framed TOON documen Configure HTTP API domain deployments per environment. +Each deployment must define exactly one of: + +- `subdomain`: a single DNS label resolved through the target environment server (`my-app.localhost:9006` locally by default, `my-app.apps.golem.cloud` on built-in cloud). +- `domain`: a full custom domain such as `api.example.com` for custom DNS or custom server environments. + ```yaml httpApi: deployments: local: - - domain: my-app.localhost:9006 - webhookUrl: http://my-app.localhost:9006 # Optional webhook base URL + - subdomain: my-app # resolves to my-app.localhost:9006 by default + webhookUrl: /webhooks/ # Optional webhook path prefix agents: TaskAgent: {} # No auth SecureAgent: securityScheme: my-oidc # OIDC security scheme name DevAgent: testSessionHeaderName: X-Test-Auth # Test auth header - prod: - - domain: api.myapp.com + cloud: + - subdomain: my-app # resolves to my-app.apps.golem.cloud agents: TaskAgent: {} SecureAgent: @@ -367,11 +372,16 @@ Agent names use **PascalCase** matching the agent type name in code. Configure MCP (Model Context Protocol) deployments per environment. +Each deployment must define exactly one of: + +- `subdomain`: a single DNS label resolved through the target environment server (`my-mcp.localhost:9007` locally by default, `my-mcp.mcps.golem.cloud` on built-in cloud). +- `domain`: a full custom domain such as `mcp.example.com` for custom DNS or custom server environments. + ```yaml mcp: deployments: local: - - domain: mcp.localhost:9006 + - subdomain: my-mcp # resolves to my-mcp.localhost:9007 by default agents: ToolAgent: {} SecureToolAgent: diff --git a/docs/src/content/next/how-to-guides/common/golem-integration-test-setup.mdx b/docs/src/content/next/how-to-guides/common/golem-integration-test-setup.mdx index bef9167a10..6367b5bf97 100644 --- a/docs/src/content/next/how-to-guides/common/golem-integration-test-setup.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-integration-test-setup.mdx @@ -145,22 +145,24 @@ The `--ports-file` is written **atomically once all services are ready**, so pol | `customRequestPort` | HTTP API endpoints exposed by the application | | `mcpPort` | MCP server requests | -If the test environment in `golem.yaml` uses the built-in `server: local` it always points at `http://localhost:9881`. When you bind to random ports for tests, override the URL with a custom server block instead, templating the port in from the harness: +When you bind to random ports for tests, keep the test environment on the built-in local server and write the discovered ports into `localServer` before deploying: ```yaml +localServer: + routerPort: 51823 # Value read from ports.json + customRequestPort: 51824 # Value read from ports.json + mcpPort: 51825 # Value read from ports.json + environments: test: - server: - url: "http://localhost:{{ GOLEM_TEST_ROUTER_PORT }}" - auth: - staticToken: "{{ GOLEM_TEST_TOKEN }}" + server: local componentPresets: test cli: autoConfirm: true reset: true ``` -The harness then exports `GOLEM_TEST_ROUTER_PORT` (read from `ports.json`) before invoking `golem`. If you prefer fixed ports during local debugging, drop `--router-port 0` and reference the defaults (`9881` / `9006` / `9007`) directly. +The `localServer.routerPort` value makes `golem -E test deploy` connect to the dynamic management port, while `customRequestPort` and `mcpPort` make deployment `subdomain` values resolve to the dynamic HTTP API and MCP ports instead of the defaults. If you prefer fixed ports during local debugging, drop `--router-port 0` and reference the defaults (`9881` / `9006` / `9007`) directly. ## 5. Deploy Against the Test Environment @@ -179,8 +181,8 @@ For faster iteration during test development, reuse the same server process acro Inside the test code, invoke agents either via the CLI or by calling the HTTP / MCP endpoints directly: - **Management / invocation API** — `http://localhost:` (also where `golem -E test agent invoke ...` connects). -- **HTTP API endpoints** — `http://localhost:/`. -- **MCP** — `http://localhost:`. +- **HTTP API endpoints** — `http://.localhost:/`, for example `http://test-api.localhost:/`. +- **MCP** — `http://.localhost:/mcp`, for example `http://test-mcp.localhost:/mcp`. Use the `httpApi.deployments.test`, `mcp.deployments.test`, `secretDefaults.test`, and `retryPolicyDefaults.test` sections of `golem.yaml` to provide test-specific routing and configuration; these sections are keyed by environment name. @@ -188,7 +190,14 @@ Use the `httpApi.deployments.test`, `mcp.deployments.test`, `secretDefaults.test httpApi: deployments: test: - - domain: localhost # Port comes from --custom-request-port + - subdomain: test-api # resolves to test-api.localhost: + agents: + MyAgent: {} + +mcp: + deployments: + test: + - subdomain: test-mcp # resolves to test-mcp.localhost: agents: MyAgent: {} @@ -217,9 +226,9 @@ Pseudocode for a typical test harness: --router-port 0 --custom-request-port 0 --mcp-port 0 \ --ports-file tests/fixtures/ports.json --clean 3. wait until tests/fixtures/ports.json exists -4. read ports → export GOLEM_TEST_ROUTER_PORT, GOLEM_TEST_CUSTOM_PORT, GOLEM_TEST_MCP_PORT +4. read ports → write routerPort, customRequestPort, and mcpPort into localServer 5. run: golem -E test deploy -6. run integration tests, hitting http://localhost:... +6. run integration tests, hitting resolved subdomain hosts with the discovered ports 7. terminate the server process and clean up tests/fixtures/data ``` diff --git a/docs/src/content/next/how-to-guides/common/golem-local-dev-server.mdx b/docs/src/content/next/how-to-guides/common/golem-local-dev-server.mdx index cf5297eab5..f3700920f6 100644 --- a/docs/src/content/next/how-to-guides/common/golem-local-dev-server.mdx +++ b/docs/src/content/next/how-to-guides/common/golem-local-dev-server.mdx @@ -81,6 +81,31 @@ When `--ports-file` is specified, the server writes a JSON file with the actual } ``` +The application manifest can mirror these local ports with `localServer.customRequestPort` and `localServer.mcpPort`. Those fields control how deployment `subdomain` values expand: HTTP API domains resolve to `