From ba913810c05f7c3f89ab3fbd4be22d048523a28f Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 16:33:01 +0200 Subject: [PATCH 01/13] agent-permissions: store cards in registry service --- .../golem/component/component_metadata.proto | 1 + golem-common/src/base_model/card/types.rs | 19 +++- .../src/base_model/component_metadata.rs | 2 + golem-common/src/model/component_metadata.rs | 58 +++++++++- golem-registry-service/src/api/error.rs | 4 + golem-registry-service/src/bootstrap/mod.rs | 6 +- golem-registry-service/src/services/card.rs | 58 ++++++++++ .../src/services/component/error.rs | 10 ++ .../src/services/component/write.rs | 101 +++++++++++++----- golem-registry-service/src/services/mod.rs | 1 + golem-worker-executor/src/durable_host/mod.rs | 11 +- .../src/services/component.rs | 39 +++++++ .../src/services/registry_event_subscriber.rs | 10 +- 13 files changed, 288 insertions(+), 32 deletions(-) create mode 100644 golem-registry-service/src/services/card.rs diff --git a/golem-api-grpc/proto/golem/component/component_metadata.proto b/golem-api-grpc/proto/golem/component/component_metadata.proto index ec55865e67..c2ccf9ee80 100644 --- a/golem-api-grpc/proto/golem/component/component_metadata.proto +++ b/golem-api-grpc/proto/golem/component/component_metadata.proto @@ -22,4 +22,5 @@ message ComponentMetadata { optional string root_package_version = 7; repeated golem.schema.AgentTypeSchema agent_types = 8; map agent_type_provision_configs = 9; + map agent_type_initial_permissions = 11; } diff --git a/golem-common/src/base_model/card/types.rs b/golem-common/src/base_model/card/types.rs index f1ea3402cf..e3ae6feada 100644 --- a/golem-common/src/base_model/card/types.rs +++ b/golem-common/src/base_model/card/types.rs @@ -16,6 +16,8 @@ use super::{ PermissionPattern, PolymorphicManifestPermissionPattern, PolymorphicPermissionPattern, }; use crate::base_model::account::AccountId; +use crate::base_model::agent::AgentTypeName; +use crate::base_model::component::{ComponentId, ComponentRevision}; use crate::base_model::environment::EnvironmentId; use crate::{declare_revision, newtype_uuid}; use chrono::{DateTime, Utc}; @@ -29,9 +31,20 @@ declare_revision!(CardRevision); #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))] pub enum CardManagedBy { - AccountRoot { account_id: AccountId }, - EnvironmentDefault { environment_id: EnvironmentId }, - PermissionShare { permission_share_id: Uuid }, + AccountRoot { + account_id: AccountId, + }, + EnvironmentDefault { + environment_id: EnvironmentId, + }, + PermissionShare { + permission_share_id: Uuid, + }, + AgentInitial { + component_id: ComponentId, + component_revision: ComponentRevision, + agent_type: AgentTypeName, + }, } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/golem-common/src/base_model/component_metadata.rs b/golem-common/src/base_model/component_metadata.rs index f6bac1efaf..bbcda5d963 100644 --- a/golem-common/src/base_model/component_metadata.rs +++ b/golem-common/src/base_model/component_metadata.rs @@ -15,6 +15,7 @@ use crate::base_model::component::{InitialAgentFile, InstalledPlugin}; use crate::base_model::worker::TypedAgentConfigEntry; use crate::model::agent::AgentTypeName; +use crate::model::card::CardId; use crate::model::card::PolymorphicPermissionPattern; use crate::schema::AgentTypeSchema; use serde::{Deserialize, Serialize, Serializer}; @@ -204,6 +205,7 @@ pub struct ComponentMetadataInnerData { #[cfg_attr(feature = "full", desert(evolution()))] #[serde(rename_all = "camelCase")] pub struct AgentInitialPermissionTemplate { + pub card_id: CardId, #[serde(default)] pub lower_positive: Vec, #[serde(default)] diff --git a/golem-common/src/model/component_metadata.rs b/golem-common/src/model/component_metadata.rs index 9955ccaa3f..e06a88b8c9 100644 --- a/golem-common/src/model/component_metadata.rs +++ b/golem-common/src/model/component_metadata.rs @@ -160,6 +160,12 @@ impl ComponentMetadata { self.data.agent_type_initial_permissions.get(name) } + pub fn agent_type_initial_permission_templates( + &self, + ) -> &BTreeMap { + &self.data.agent_type_initial_permissions + } + pub fn agent_type_env(&self, name: &AgentTypeName) -> Option<&BTreeMap> { self.agent_type_provision_config(name) .map(|config| &config.env) @@ -496,6 +502,7 @@ impl AgentInitialPermissionTemplate { ) -> Self { let recipient = RecipientPattern::Any; Self { + card_id: crate::model::card::CardId::new(), lower_positive: vec![ PolymorphicPermissionPattern::Environment(PolymorphicClassPermissionPattern { owner: PolymorphicEnvironmentOwnerPattern::Env, @@ -749,7 +756,14 @@ mod protobuf { .map(|config| (AgentTypeName(k), config)) }) .collect::>()?, - agent_type_initial_permissions: std::collections::BTreeMap::new(), + agent_type_initial_permissions: value + .agent_type_initial_permissions + .into_iter() + .map(|(k, v)| { + crate::serialization::deserialize(&v) + .map(|template| (AgentTypeName(k), template)) + }) + .collect::>()?, }) } } @@ -813,6 +827,15 @@ mod protobuf { ) }) .collect(), + agent_type_initial_permissions: value + .agent_type_initial_permissions + .into_iter() + .map(|(k, v)| { + crate::serialization::serialize(&v) + .map(|template| (k.0, template)) + .expect("failed to serialize agent initial permission template") + }) + .collect(), } } } @@ -891,6 +914,10 @@ mod protobuf { mod tests { use super::*; use crate::component_introspection::{ExportedInstance, TopLevelExport}; + use crate::model::agent::AgentTypeName; + use crate::model::card::CardId; + use crate::model::component::ComponentName; + use crate::model::environment::EnvironmentName; use test_r::test; fn instance_export(name: &str) -> TopLevelExport { @@ -974,4 +1001,33 @@ mod tests { Some("golem:api/oplog-processor@1.5.0.{process}".to_string()) ); } + + #[test] + fn component_metadata_grpc_roundtrip_preserves_agent_initial_permissions() { + let agent_type = AgentTypeName("Cart".to_string()); + let card_id = CardId::new(); + let mut template = AgentInitialPermissionTemplate::default_for( + &EnvironmentName::try_from("prod").unwrap(), + &ComponentName("cart-svc".to_string()), + ); + template.card_id = card_id; + + let metadata = ComponentMetadata::from_parts( + KnownExports::default(), + Vec::new(), + None, + None, + Vec::new(), + BTreeMap::new(), + ) + .with_agent_initial_permissions(BTreeMap::from([(agent_type.clone(), template.clone())])); + + let proto: golem_api_grpc::proto::golem::component::ComponentMetadata = metadata.into(); + let decoded = ComponentMetadata::try_from(proto).unwrap(); + + assert_eq!( + decoded.agent_type_initial_permission_template(&agent_type), + Some(&template) + ); + } } diff --git a/golem-registry-service/src/api/error.rs b/golem-registry-service/src/api/error.rs index 2781aa9dce..11cdcf1c57 100644 --- a/golem-registry-service/src/api/error.rs +++ b/golem-registry-service/src/api/error.rs @@ -522,6 +522,10 @@ impl From for ApiError { ComponentError::UndeclaredAgentTypeInProvisionConfig(_) => { Self::bad_request(api::error_code::AGENT_TYPE_NOT_DECLARED, error) } + ComponentError::MissingAgentInitialPermissionTemplate(_) + | ComponentError::UndeclaredAgentTypeInInitialPermissionTemplate(_) => { + Self::bad_request(api::error_code::AGENT_TYPE_NOT_DECLARED, error) + } ComponentError::Unauthorized(inner) => inner.into(), ComponentError::LimitExceeded(inner) => inner.into(), diff --git a/golem-registry-service/src/bootstrap/mod.rs b/golem-registry-service/src/bootstrap/mod.rs index 49fef0b70b..830bac8db4 100644 --- a/golem-registry-service/src/bootstrap/mod.rs +++ b/golem-registry-service/src/bootstrap/mod.rs @@ -46,6 +46,7 @@ use crate::services::account_usage::AccountUsageService; use crate::services::agent_secret::AgentSecretService; use crate::services::application::ApplicationService; use crate::services::auth::AuthService; +use crate::services::card::CardService; use crate::services::component::{ComponentService, ComponentWriteService}; use crate::services::component_compilation::ComponentCompilationService; use crate::services::component_object_store::ComponentObjectStore; @@ -268,6 +269,8 @@ impl Services { deployment_service.clone(), )); + let card_service = Arc::new(CardService::new(repos.card_repo.clone())); + let plugin_registration_service = Arc::new(PluginRegistrationService::new( repos.plugin_repo.clone(), account_service.clone(), @@ -282,7 +285,8 @@ impl Services { )); let component_write_service = Arc::new(ComponentWriteService::new( - repos.component_repo, + repos.component_repo.clone(), + card_service, component_object_store, component_compilation_service.clone(), initial_agent_files, diff --git a/golem-registry-service/src/services/card.rs b/golem-registry-service/src/services/card.rs new file mode 100644 index 0000000000..268a1c65b1 --- /dev/null +++ b/golem-registry-service/src/services/card.rs @@ -0,0 +1,58 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::repo::card::CardRepo; +use crate::repo::model::card::{CardRecord, CardRepoError}; +use golem_common::model::agent::AgentTypeName; +use golem_common::model::card::{CardId, CardManagedBy}; +use golem_common::model::component::{ComponentId, ComponentRevision}; +use std::sync::Arc; + +pub struct CardService { + card_repo: Arc, +} + +impl CardService { + pub fn new(card_repo: Arc) -> Self { + Self { card_repo } + } + + pub async fn create_agent_initial_card( + &self, + component_id: ComponentId, + component_revision: ComponentRevision, + agent_type: AgentTypeName, + ) -> Result { + let card_id = CardId::new(); + self.card_repo + .create(CardRecord::creation( + card_id, + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + Vec::new(), + None, + false, + Some(CardManagedBy::AgentInitial { + component_id, + component_revision, + agent_type, + }), + )) + .await?; + + Ok(card_id) + } +} diff --git a/golem-registry-service/src/services/component/error.rs b/golem-registry-service/src/services/component/error.rs index 2593a4dee5..e10ce0fe64 100644 --- a/golem-registry-service/src/services/component/error.rs +++ b/golem-registry-service/src/services/component/error.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::repo::model::card::CardRepoError; use crate::repo::model::component::ComponentRepoError; use crate::services::account_usage::error::{AccountUsageError, LimitExceededError}; use crate::services::application::ApplicationError; @@ -83,6 +84,12 @@ pub enum ComponentError { "Agent type '{0}' is referenced in provision config but not declared in the component's agent types" )] UndeclaredAgentTypeInProvisionConfig(AgentTypeName), + #[error("Agent type '{0}' has no initial permission template")] + MissingAgentInitialPermissionTemplate(AgentTypeName), + #[error( + "Agent type '{0}' is referenced in initial permissions but not declared in the component's agent types" + )] + UndeclaredAgentTypeInInitialPermissionTemplate(AgentTypeName), #[error("Config for agent {agent} at key {rendered_key} is not declared", rendered_key = key.join("."))] AgentConfigNotDeclared { agent: AgentTypeName, @@ -154,6 +161,8 @@ impl SafeDisplay for ComponentError { Self::AgentTypeForNameNotFound(_) => self.to_string(), Self::DuplicateAgentTypeName(_) => self.to_string(), Self::UndeclaredAgentTypeInProvisionConfig(_) => self.to_string(), + Self::MissingAgentInitialPermissionTemplate(_) => self.to_string(), + Self::UndeclaredAgentTypeInInitialPermissionTemplate(_) => self.to_string(), Self::AgentConfigNotDeclared { .. } => self.to_string(), Self::AgentConfigTypeMismatch { .. } => self.to_string(), Self::AgentConfigProvidedSecretWhereOnlyLocalAllowed { .. } => self.to_string(), @@ -171,6 +180,7 @@ error_forwarding!( ComponentError, RepoError, ApplicationError, + CardRepoError, ComponentRepoError, EnvironmentError, EnvironmentPluginGrantError, diff --git a/golem-registry-service/src/services/component/write.rs b/golem-registry-service/src/services/component/write.rs index ced5835528..0ed855bd92 100644 --- a/golem-registry-service/src/services/component/write.rs +++ b/golem-registry-service/src/services/component/write.rs @@ -17,6 +17,7 @@ use crate::metrics::storage::record_component_uploaded; use crate::repo::component::ComponentRepo; use crate::repo::model::component::{ComponentRepoError, ComponentRevisionRecord}; use crate::services::account_usage::AccountUsageService; +use crate::services::card::CardService; use crate::services::component::utils::prepare_component_files_for_upload; use crate::services::component_compilation::ComponentCompilationService; use crate::services::component_object_store::ComponentObjectStore; @@ -68,6 +69,7 @@ use tracing::info; pub struct ComponentWriteService { component_repo: Arc, + card_service: Arc, object_store: Arc, component_compilation: Arc, initial_agent_files_service: Arc, @@ -79,6 +81,7 @@ pub struct ComponentWriteService { impl ComponentWriteService { pub fn new( component_repo: Arc, + card_service: Arc, object_store: Arc, component_compilation: Arc, initial_agent_files_service: Arc, @@ -88,6 +91,7 @@ impl ComponentWriteService { ) -> Self { Self { component_repo, + card_service, object_store, component_compilation, initial_agent_files_service, @@ -97,6 +101,36 @@ impl ComponentWriteService { } } + async fn create_default_initial_permissions( + &self, + component_id: ComponentId, + component_revision: ComponentRevision, + agent_types: &[AgentType], + environment_name: &EnvironmentName, + component_name: &ComponentName, + ) -> Result, ComponentError> { + let mut result = BTreeMap::new(); + + for agent_type in agent_types { + let card_id = self + .card_service + .create_agent_initial_card( + component_id, + component_revision, + agent_type.type_name.clone(), + ) + .await?; + let template = AgentInitialPermissionTemplate { + card_id, + ..AgentInitialPermissionTemplate::default_for(environment_name, component_name) + }; + + result.insert(agent_type.type_name.clone(), template); + } + + Ok(result) + } + pub async fn create( &self, environment_id: EnvironmentId, @@ -218,12 +252,16 @@ impl ComponentWriteService { provision_configs, ) .await?; - let component_metadata = - component_metadata.with_agent_initial_permissions(default_initial_permissions( + let component_metadata = component_metadata.with_agent_initial_permissions( + self.create_default_initial_permissions( + component_id, + ComponentRevision::INITIAL, component_metadata.agent_types(), &environment.name, &component_creation.component_name, - )); + ) + .await?, + ); validate_component_metadata_invariants(&component_metadata)?; let component_size = wasm.len() as u64; @@ -418,12 +456,16 @@ impl ComponentWriteService { final_provision_configs, ) .await?; - component.metadata = - metadata.with_agent_initial_permissions(default_initial_permissions( + component.metadata = metadata.with_agent_initial_permissions( + self.create_default_initial_permissions( + component.id, + component.revision, metadata.agent_types(), &environment.name, &component.component_name, - )); + ) + .await?, + ); } else if agent_types_changed { // TODO: skip the download here let old_data = self @@ -437,12 +479,16 @@ impl ComponentWriteService { final_provision_configs, ) .await?; - component.metadata = - metadata.with_agent_initial_permissions(default_initial_permissions( + component.metadata = metadata.with_agent_initial_permissions( + self.create_default_initial_permissions( + component.id, + component.revision, metadata.agent_types(), &environment.name, &component_name, - )); + ) + .await?, + ); } else if provision_configs_changed { component.metadata = component .metadata @@ -999,22 +1045,6 @@ fn provision_configs_for_agent_types( .collect() } -fn default_initial_permissions( - agent_types: &[AgentTypeSchema], - environment_name: &EnvironmentName, - component_name: &ComponentName, -) -> BTreeMap { - agent_types - .iter() - .map(|agent_type| { - ( - agent_type.type_name.clone(), - AgentInitialPermissionTemplate::default_for(environment_name, component_name), - ) - }) - .collect() -} - fn validate_component_metadata_invariants( metadata: &ComponentMetadata, ) -> Result<(), ComponentError> { @@ -1036,6 +1066,27 @@ fn validate_component_metadata_invariants( } } + for agent_type in metadata.agent_types() { + match metadata.agent_type_initial_permission_template(&agent_type.type_name) { + Some(_) => {} + _ => { + return Err(ComponentError::MissingAgentInitialPermissionTemplate( + agent_type.type_name.clone(), + )); + } + } + } + + for agent_type_name in metadata.agent_type_initial_permission_templates().keys() { + if !agent_type_names.contains(agent_type_name) { + return Err( + ComponentError::UndeclaredAgentTypeInInitialPermissionTemplate( + agent_type_name.clone(), + ), + ); + } + } + Ok(()) } diff --git a/golem-registry-service/src/services/mod.rs b/golem-registry-service/src/services/mod.rs index f84ac40b2c..c34b511af7 100644 --- a/golem-registry-service/src/services/mod.rs +++ b/golem-registry-service/src/services/mod.rs @@ -18,6 +18,7 @@ pub mod agent_secret; pub mod application; pub mod auth; pub mod builtin_plugin_provisioner; +pub mod card; pub mod component; pub mod component_compilation; pub mod component_object_store; diff --git a/golem-worker-executor/src/durable_host/mod.rs b/golem-worker-executor/src/durable_host/mod.rs index bf59181bf1..11a7c58b97 100644 --- a/golem-worker-executor/src/durable_host/mod.rs +++ b/golem-worker-executor/src/durable_host/mod.rs @@ -650,6 +650,14 @@ impl DurableWorkerCtx { ) }); + if self + .state + .component_service + .is_card_revoked(template.card_id) + { + return golem_common::model::card::EffectiveSurface::default(); + } + let context = golem_common::model::card::AgentPermissionMonomorphizationContext { account: component.account_email.clone(), application: component.application_name.clone(), @@ -659,7 +667,7 @@ impl DurableWorkerCtx { agent_type: agent_id.agent_type.clone(), }; - let Ok(card) = golem_common::model::card::monomorphize_agent_initial_card( + let Ok(mut card) = golem_common::model::card::monomorphize_agent_initial_card( &template.lower_positive, &template.lower_negative, &template.upper_positive, @@ -668,6 +676,7 @@ impl DurableWorkerCtx { ) else { return golem_common::model::card::EffectiveSurface::default(); }; + card.card_id = template.card_id; golem_common::model::card::EffectiveSurface::from_cards( std::slice::from_ref(&card), diff --git a/golem-worker-executor/src/services/component.rs b/golem-worker-executor/src/services/component.rs index 0cd81670b6..20b1e0badb 100644 --- a/golem-worker-executor/src/services/component.rs +++ b/golem-worker-executor/src/services/component.rs @@ -20,6 +20,7 @@ use golem_common::cache::SimpleCache; use golem_common::cache::{BackgroundEvictionMode, Cache, FullCacheEvictionMode}; use golem_common::model::account::AccountId; use golem_common::model::application::ApplicationId; +use golem_common::model::card::CardId; use golem_common::model::component::{ComponentId, ComponentRevision}; use golem_common::model::environment::EnvironmentId; use golem_service_base::clients::registry::{RegistryService, RegistryServiceError}; @@ -28,7 +29,9 @@ use golem_service_base::model::component::Component; use golem_service_base::service::compiled_component::CompiledComponentService; use golem_service_base::service::compiled_component::CompiledComponentServiceConfig; use golem_service_base::storage::blob::BlobStorage; +use std::collections::HashSet; use std::sync::Arc; +use std::sync::RwLock; use std::time::Duration; use std::time::Instant; use tokio::task::spawn_blocking; @@ -86,6 +89,12 @@ pub trait ComponentService: Send + Sync { async fn invalidate_all(&self) { self.invalidate_current_deployed_metadata().await } + + fn record_revoked_cards(&self, _card_ids: &[CardId]) {} + + fn is_card_revoked(&self, _card_id: CardId) -> bool { + false + } } pub fn configured( @@ -112,6 +121,7 @@ pub struct ComponentServiceDefault { current_component_metadata_cache: Cache, compiled_component_service: Arc, registry_client: Arc, + revoked_cards: Arc>>, } impl ComponentServiceDefault { @@ -134,6 +144,7 @@ impl ComponentServiceDefault { time_to_idle, ), compiled_component_service, + revoked_cards: Arc::new(RwLock::new(HashSet::new())), } } } @@ -394,6 +405,15 @@ impl ComponentService for ComponentServiceDefault { self.component_metadata_cache.remove(&key).await; } } + + fn record_revoked_cards(&self, card_ids: &[CardId]) { + let mut revoked_cards = self.revoked_cards.write().unwrap(); + revoked_cards.extend(card_ids.iter().copied()); + } + + fn is_card_revoked(&self, card_id: CardId) -> bool { + self.revoked_cards.read().unwrap().contains(&card_id) + } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -801,4 +821,23 @@ mod tests { "other environments should stay cached" ); } + + #[test] + fn records_revoked_card_ids() { + let service = ComponentServiceDefault::new( + Arc::new(MockRegistryService::new([])), + 1, + 8, + Duration::from_secs(60), + Arc::new(CompiledComponentServiceDisabled::new()), + ); + let revoked = CardId::new(); + let live = CardId::new(); + + assert!(!service.is_card_revoked(revoked)); + service.record_revoked_cards(&[revoked]); + + assert!(service.is_card_revoked(revoked)); + assert!(!service.is_card_revoked(live)); + } } diff --git a/golem-worker-executor/src/services/registry_event_subscriber.rs b/golem-worker-executor/src/services/registry_event_subscriber.rs index 9c04e022fb..eb9d8aac52 100644 --- a/golem-worker-executor/src/services/registry_event_subscriber.rs +++ b/golem-worker-executor/src/services/registry_event_subscriber.rs @@ -18,6 +18,7 @@ use crate::services::component::ComponentService; use crate::services::environment_state::EnvironmentStateService; use crate::workerctx::WorkerCtx; use golem_common::model::agent::RegistryInvalidationEvent; +use golem_common::model::card::CardId; use golem_service_base::clients::registry::{RegistryInvalidationHandler, RegistryService}; use std::sync::Arc; use tokio_util::sync::CancellationToken; @@ -141,7 +142,14 @@ impl RegistryInvalidationHandler .invalidate_environment(*environment_id) .await; } - RegistryInvalidationEvent::CardRevoked { .. } => {} + RegistryInvalidationEvent::CardRevoked { card_ids, .. } => { + let card_ids = card_ids.iter().copied().map(CardId).collect::>(); + debug!( + card_count = card_ids.len(), + "Received card revocation event, recording revoked card ids" + ); + self.component_service.record_revoked_cards(&card_ids); + } RegistryInvalidationEvent::ApplicationDeleted { application_id, account_id, From e3f0c601556e87d01e5b134bf44b4f959b5ef956 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 19:40:21 +0200 Subject: [PATCH 02/13] basic revocation --- cli/golem-cli/src/model/text/worker.rs | 11 +++ .../golem/registry/v1/registry_service.proto | 18 +++++ .../proto/golem/worker/public_oplog.proto | 6 ++ .../proto/golem/worker/raw_oplog.proto | 5 ++ golem-common/src/base_model/oplog/mod.rs | 12 ++++ golem-common/src/model/mod.rs | 3 + golem-common/src/model/oplog/matcher.rs | 5 ++ golem-common/src/model/oplog/protobuf.rs | 44 +++++++++++- golem-registry-service/src/bootstrap/mod.rs | 4 +- golem-registry-service/src/grpc/api_impl.rs | 70 +++++++++++++++---- golem-registry-service/src/grpc/error.rs | 11 +++ golem-registry-service/src/grpc/mod.rs | 1 + golem-registry-service/src/services/card.rs | 4 ++ golem-service-base/src/clients/registry.rs | 59 +++++++++++++--- golem-test-framework/src/dsl/debug_render.rs | 5 ++ .../src/model/public_oplog/mod.rs | 19 +++++ .../src/model/public_oplog/wit.rs | 21 ++++++ .../src/services/component.rs | 29 ++++++++ golem-worker-executor/src/worker/mod.rs | 34 +++++++++ golem-worker-executor/src/worker/status.rs | 45 ++++++++++++ 20 files changed, 382 insertions(+), 24 deletions(-) diff --git a/cli/golem-cli/src/model/text/worker.rs b/cli/golem-cli/src/model/text/worker.rs index d0e6dc6e96..7cff028f2b 100644 --- a/cli/golem-cli/src/model/text/worker.rs +++ b/cli/golem-cli/src/model/text/worker.rs @@ -1060,6 +1060,17 @@ impl TextView for PublicOplogEntry { format_id(¶ms.name) )); } + PublicOplogEntry::CardRevoked(params) => { + logln(format_message_highlight("CARD REVOKED")); + logln(format!( + "{pad}at: {}", + format_id(¶ms.timestamp) + )); + logln(format!( + "{pad}card id: {}", + format_id(¶ms.card_id) + )); + } } } } diff --git a/golem-api-grpc/proto/golem/registry/v1/registry_service.proto b/golem-api-grpc/proto/golem/registry/v1/registry_service.proto index 3687b4d939..a3ded66c34 100644 --- a/golem-api-grpc/proto/golem/registry/v1/registry_service.proto +++ b/golem-api-grpc/proto/golem/registry/v1/registry_service.proto @@ -53,6 +53,9 @@ service RegistryService { rpc GetResourceDefinitionById (GetResourceDefinitionByIdRequest) returns (GetResourceDefinitionByIdResponse); rpc GetResourceDefinitionByName (GetResourceDefinitionByNameRequest) returns (GetResourceDefinitionByNameResponse); + // cards api + rpc BatchGetExistingCards (BatchGetExistingCardsRequest) returns (BatchGetExistingCardsResponse); + // registry invalidation stream — multiplexed event types rpc SubscribeRegistryInvalidations(SubscribeRegistryInvalidationsRequest) returns (stream RegistryInvalidationEvent); @@ -116,6 +119,21 @@ message BatchUpdateResourceUsageSuccessResponse { golem.common.AccountResourceLimits account_resource_limits = 1; } +message BatchGetExistingCardsRequest { + repeated golem.common.UUID card_ids = 1; +} + +message BatchGetExistingCardsResponse { + oneof result { + BatchGetExistingCardsSuccessResponse success = 1; + RegistryServiceError error = 2; + } +} + +message BatchGetExistingCardsSuccessResponse { + repeated golem.common.UUID card_ids = 1; +} + message DownloadComponentRequest { golem.component.ComponentId componentId = 1; uint64 revision = 2; diff --git a/golem-api-grpc/proto/golem/worker/public_oplog.proto b/golem-api-grpc/proto/golem/worker/public_oplog.proto index 0e9bc831f5..14f3f00bad 100644 --- a/golem-api-grpc/proto/golem/worker/public_oplog.proto +++ b/golem-api-grpc/proto/golem/worker/public_oplog.proto @@ -67,6 +67,7 @@ message OplogEntry { StartParameters Start = 44; EndParameters End = 45; CancelledParameters Cancelled = 46; + CardRevokedParameters CardRevoked = 47; } } @@ -275,6 +276,11 @@ message RemoveRetryPolicyParameters { string policy_name = 2; } +message CardRevokedParameters { + google.protobuf.Timestamp timestamp = 1; + golem.common.UUID card_id = 2; +} + message EndAtomicRegionParameters { google.protobuf.Timestamp timestamp = 1; uint64 begin_index = 2; diff --git a/golem-api-grpc/proto/golem/worker/raw_oplog.proto b/golem-api-grpc/proto/golem/worker/raw_oplog.proto index b772e32fb3..b6ab68ebbf 100644 --- a/golem-api-grpc/proto/golem/worker/raw_oplog.proto +++ b/golem-api-grpc/proto/golem/worker/raw_oplog.proto @@ -59,6 +59,7 @@ message RawOplogEntry { RawStartParameters start = 43; RawEndParameters end = 44; RawCancelledParameters cancelled = 45; + RawCardRevokedParameters card_revoked = 46; } } @@ -311,6 +312,10 @@ message RawRemoveRetryPolicyParameters { string name = 1; } +message RawCardRevokedParameters { + golem.common.UUID card_id = 1; +} + message RawOplogEntryWithIndex { uint64 oplog_index = 1; RawOplogEntry entry = 2; diff --git a/golem-common/src/base_model/oplog/mod.rs b/golem-common/src/base_model/oplog/mod.rs index 8f3b242876..df41438321 100644 --- a/golem-common/src/base_model/oplog/mod.rs +++ b/golem-common/src/base_model/oplog/mod.rs @@ -667,5 +667,17 @@ oplog_entry! { public { name: String, } + }, + /// Records that a permission card used by the worker has been revoked. + CardRevoked { + hint: true + wit_raw_type: "card-revoked-parameters" + wit_public_type: "card-revoked-parameters" + raw { + card_id: Uuid, + } + public { + card_id: Uuid, + } } } diff --git a/golem-common/src/model/mod.rs b/golem-common/src/model/mod.rs index 4c1d899bce..975257a8e1 100644 --- a/golem-common/src/model/mod.rs +++ b/golem-common/src/model/mod.rs @@ -63,6 +63,7 @@ use crate::base_model::agent::Principal; use crate::base_model::environment_plugin_grant::EnvironmentPluginGrantId; use crate::model::account::{AccountEmail, AccountId}; use crate::model::agent::{AgentTypeSchemaResolver, ParsedAgentId}; +use crate::model::card::CardId; use crate::model::invocation_context::InvocationContextStack; use crate::model::oplog::types::AgentMetadataForGuests; use crate::model::oplog::{AgentResourceId, OplogEntry, RawSnapshotData}; @@ -685,6 +686,7 @@ pub struct AgentStatusRecord { pub active_plugins: HashSet, pub oplog_processor_checkpoints: HashMap, + pub revoked_cards: HashSet, pub deleted_regions: DeletedRegions, /// The component version at the starting point of the replay. Will be the version of the Create oplog entry /// if only automatic updates were used or the version of the latest snapshot-based update @@ -728,6 +730,7 @@ impl Default for AgentStatusRecord { oplog_idx: OplogIndex::default(), active_plugins: HashSet::new(), oplog_processor_checkpoints: HashMap::new(), + revoked_cards: HashSet::new(), deleted_regions: DeletedRegions::new(), component_revision_for_replay: ComponentRevision::INITIAL, current_retry_state: HashMap::new(), diff --git a/golem-common/src/model/oplog/matcher.rs b/golem-common/src/model/oplog/matcher.rs index a729b19bfb..5aaccaed11 100644 --- a/golem-common/src/model/oplog/matcher.rs +++ b/golem-common/src/model/oplog/matcher.rs @@ -435,6 +435,11 @@ impl PublicOplogEntry { Self::string_match("removeretrypolicy", &[], query_path, query) || Self::string_match("remove-retry-policy", &[], query_path, query) } + PublicOplogEntry::CardRevoked(params) => { + Self::string_match("cardrevoked", &[], query_path, query) + || Self::string_match("card-revoked", &[], query_path, query) + || Self::string_match(¶ms.card_id.to_string(), &[], query_path, query) + } } } diff --git a/golem-common/src/model/oplog/protobuf.rs b/golem-common/src/model/oplog/protobuf.rs index 4e690a824c..07b67b7b7d 100644 --- a/golem-common/src/model/oplog/protobuf.rs +++ b/golem-common/src/model/oplog/protobuf.rs @@ -48,6 +48,19 @@ use crate::model::oplog::public_oplog_entry::{ SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, }; +use crate::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, + CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + HostCallParams, InterruptedParams, JumpParams, LogParams, NoOpParams, + OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, + PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, + RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, + SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, +}; use crate::model::oplog::{ AgentTerminatedByQuotaError, DurableFunctionType, EphemeralCannotSuspendError, EphemeralFuelExhaustedError, EphemeralSleepTooLongError, OplogEntry, PersistenceLevel, @@ -802,6 +815,12 @@ impl TryFrom for PublicOplogEn name: params.policy_name, }), ), + oplog_entry::Entry::CardRevoked(params) => { + Ok(PublicOplogEntry::CardRevoked(CardRevokedParams { + timestamp: params.timestamp.ok_or("Missing timestamp field")?.into(), + card_id: params.card_id.ok_or("Missing card_id field")?.into(), + })) + } } } } @@ -1323,6 +1342,16 @@ impl TryFrom for golem_api_grpc::proto::golem::worker::OplogEn )), } } + PublicOplogEntry::CardRevoked(params) => { + golem_api_grpc::proto::golem::worker::OplogEntry { + entry: Some(oplog_entry::Entry::CardRevoked( + golem_api_grpc::proto::golem::worker::CardRevokedParameters { + timestamp: Some(params.timestamp.into()), + card_id: Some(params.card_id.into()), + }, + )), + } + } }) } } @@ -2491,6 +2520,10 @@ impl TryFrom for OplogEntry { timestamp: p.timestamp, name: p.name, }), + PublicOplogEntry::CardRevoked(p) => Ok(OplogEntry::CardRevoked { + timestamp: p.timestamp, + card_id: p.card_id, + }), } } } @@ -2851,7 +2884,7 @@ impl TryFrom for golem_api_grpc::proto::golem::worker::RawOplogEntry use golem_api_grpc::proto::golem::worker::{ RawActivatePluginParameters, RawAgentInvocationFinishedParameters, RawAgentInvocationStartedParameters, RawBeginRemoteTransactionParameters, - RawCancelPendingInvocationParameters, RawCancelledParameters, + RawCancelPendingInvocationParameters, RawCancelledParameters, RawCardRevokedParameters, RawChangePersistenceLevelParameters, RawCreateParameters, RawCreateResourceParameters, RawDeactivatePluginParameters, RawDropResourceParameters, RawEndAtomicRegionParameters, RawEndParameters, RawEnvVar, RawErrorParameters, RawFailedUpdateParameters, @@ -3204,6 +3237,11 @@ impl TryFrom for golem_api_grpc::proto::golem::worker::RawOplogEntry OplogEntry::RemoveRetryPolicy { name, .. } => { Entry::RemoveRetryPolicy(RawRemoveRetryPolicyParameters { name }) } + OplogEntry::CardRevoked { card_id, .. } => { + Entry::CardRevoked(RawCardRevokedParameters { + card_id: Some(card_id.into()), + }) + } }; Ok(golem_api_grpc::proto::golem::worker::RawOplogEntry { @@ -3620,6 +3658,10 @@ impl TryFrom for OplogEntry timestamp, name: p.name, }), + Entry::CardRevoked(p) => Ok(OplogEntry::CardRevoked { + timestamp, + card_id: p.card_id.ok_or("Missing card_id")?.into(), + }), } } } diff --git a/golem-registry-service/src/bootstrap/mod.rs b/golem-registry-service/src/bootstrap/mod.rs index 830bac8db4..56f8ba5d31 100644 --- a/golem-registry-service/src/bootstrap/mod.rs +++ b/golem-registry-service/src/bootstrap/mod.rs @@ -94,6 +94,7 @@ pub struct Services { pub agent_secret_service: Arc, pub application_service: Arc, pub auth_service: Arc, + pub card_service: Arc, pub component_compilation_service: Arc, pub component_resolver_service: Arc, pub component_service: Arc, @@ -286,7 +287,7 @@ impl Services { let component_write_service = Arc::new(ComponentWriteService::new( repos.component_repo.clone(), - card_service, + card_service.clone(), component_object_store, component_compilation_service.clone(), initial_agent_files, @@ -421,6 +422,7 @@ impl Services { agent_secret_service, application_service, auth_service, + card_service, component_compilation_service, component_resolver_service, component_service, diff --git a/golem-registry-service/src/grpc/api_impl.rs b/golem-registry-service/src/grpc/api_impl.rs index d81d4c94b6..d3ae8d8e86 100644 --- a/golem-registry-service/src/grpc/api_impl.rs +++ b/golem-registry-service/src/grpc/api_impl.rs @@ -16,6 +16,7 @@ use super::error::GrpcApiError; use crate::repo::registry_change::RegistryChangeRepo; use crate::services::account_usage::{AccountUsageService, ResourceUsageUpdate}; use crate::services::auth::AuthService; +use crate::services::card::CardService; use crate::services::component::ComponentService; use crate::services::component_resolver::ComponentResolverService; use crate::services::deployment::{DeployedMcpService, DeployedRoutesService, DeploymentService}; @@ -29,16 +30,17 @@ use futures::stream::BoxStream; use golem_api_grpc::proto::golem::common::Empty as EmptySuccessResponse; use golem_api_grpc::proto::golem::registry::v1::{ AuthenticateTokenRequest, AuthenticateTokenResponse, AuthenticateTokenSuccessResponse, - BatchUpdateResourceUsageRequest, BatchUpdateResourceUsageResponse, - BatchUpdateResourceUsageSuccessResponse, DownloadComponentRequest, DownloadComponentResponse, - GetActiveMcpForDomainRequest, GetActiveMcpForDomainResponse, - GetActiveMcpForDomainSuccessResponse, GetActiveRoutesForDomainRequest, - GetActiveRoutesForDomainResponse, GetActiveRoutesForDomainSuccessResponse, GetAgentTypeRequest, - GetAgentTypeResponse, GetAgentTypeSuccessResponse, GetAllAgentTypesRequest, - GetAllAgentTypesResponse, GetAllAgentTypesSuccessResponse, - GetAllDeployedComponentRevisionsRequest, GetAllDeployedComponentRevisionsResponse, - GetAllDeployedComponentRevisionsSuccessResponse, GetComponentMetadataRequest, - GetComponentMetadataResponse, GetComponentMetadataSuccessResponse, + BatchGetExistingCardsRequest, BatchGetExistingCardsResponse, + BatchGetExistingCardsSuccessResponse, BatchUpdateResourceUsageRequest, + BatchUpdateResourceUsageResponse, BatchUpdateResourceUsageSuccessResponse, + DownloadComponentRequest, DownloadComponentResponse, GetActiveMcpForDomainRequest, + GetActiveMcpForDomainResponse, GetActiveMcpForDomainSuccessResponse, + GetActiveRoutesForDomainRequest, GetActiveRoutesForDomainResponse, + GetActiveRoutesForDomainSuccessResponse, GetAgentTypeRequest, GetAgentTypeResponse, + GetAgentTypeSuccessResponse, GetAllAgentTypesRequest, GetAllAgentTypesResponse, + GetAllAgentTypesSuccessResponse, GetAllDeployedComponentRevisionsRequest, + GetAllDeployedComponentRevisionsResponse, GetAllDeployedComponentRevisionsSuccessResponse, + GetComponentMetadataRequest, GetComponentMetadataResponse, GetComponentMetadataSuccessResponse, GetCurrentEnvironmentStateRequest, GetCurrentEnvironmentStateResponse, GetCurrentEnvironmentStateSuccessResponse, GetDeployedComponentMetadataRequest, GetDeployedComponentMetadataResponse, GetDeployedComponentMetadataSuccessResponse, @@ -51,9 +53,9 @@ use golem_api_grpc::proto::golem::registry::v1::{ ResolveComponentRequest, ResolveComponentResponse, ResolveComponentSuccessResponse, SubscribeRegistryInvalidationsRequest, UpdateWorkerConnectionLimitRequest, UpdateWorkerConnectionLimitResponse, authenticate_token_response, - batch_update_resource_usage_response, download_component_response, - get_active_mcp_for_domain_response, get_active_routes_for_domain_response, - get_agent_type_response, get_all_agent_types_response, + batch_get_existing_cards_response, batch_update_resource_usage_response, + download_component_response, get_active_mcp_for_domain_response, + get_active_routes_for_domain_response, get_agent_type_response, get_all_agent_types_response, get_all_deployed_component_revisions_response, get_component_metadata_response, get_current_environment_state_response, get_deployed_component_metadata_response, get_resource_definition_by_id_response, get_resource_definition_by_name_response, @@ -64,6 +66,7 @@ use golem_common::model::account::AccountId; use golem_common::model::agent::{AgentTypeName, RegisteredAgentType}; use golem_common::model::application::{ApplicationId, ApplicationName}; use golem_common::model::auth::TokenSecret; +use golem_common::model::card::CardId; use golem_common::model::component::{ComponentId, ComponentRevision}; use golem_common::model::deployment::DeploymentRevision; use golem_common::model::domain_registration::Domain; @@ -79,6 +82,7 @@ use tracing_futures::Instrument; pub struct RegistryServiceGrpcApi { auth_service: Arc, account_usage_service: Arc, + card_service: Arc, component_service: Arc, component_resolver_service: Arc, deployment_service: Arc, @@ -94,6 +98,7 @@ impl RegistryServiceGrpcApi { pub fn new( auth_service: Arc, account_usage_service: Arc, + card_service: Arc, component_service: Arc, component_resolver_service: Arc, deployment_service: Arc, @@ -107,6 +112,7 @@ impl RegistryServiceGrpcApi { Self { auth_service, account_usage_service, + card_service, component_service, component_resolver_service, deployment_service, @@ -199,6 +205,22 @@ impl RegistryServiceGrpcApi { }) } + async fn batch_get_existing_cards_internal( + &self, + request: BatchGetExistingCardsRequest, + ) -> Result { + let card_ids = request + .card_ids + .into_iter() + .map(|id| CardId(id.into())) + .collect::>(); + let existing = self.card_service.existing(card_ids).await?; + + Ok(BatchGetExistingCardsSuccessResponse { + card_ids: existing.into_iter().map(|id| id.0.into()).collect(), + }) + } + async fn download_component_internal( &self, request: DownloadComponentRequest, @@ -607,6 +629,28 @@ impl golem_api_grpc::proto::golem::registry::v1::registry_service_server::Regist })) } + async fn batch_get_existing_cards( + &self, + request: Request, + ) -> Result, tonic::Status> { + let request = request.into_inner(); + let record = recorded_grpc_api_request!("batch_get_existing_cards",); + + let response = match self + .batch_get_existing_cards_internal(request) + .instrument(record.span.clone()) + .await + .apply(|r| record.result(r)) + { + Ok(result) => batch_get_existing_cards_response::Result::Success(result), + Err(error) => batch_get_existing_cards_response::Result::Error(error.into()), + }; + + Ok(Response::new(BatchGetExistingCardsResponse { + result: Some(response), + })) + } + type DownloadComponentStream = BoxStream<'static, Result>; diff --git a/golem-registry-service/src/grpc/error.rs b/golem-registry-service/src/grpc/error.rs index 51c12832cb..48c98d3604 100644 --- a/golem-registry-service/src/grpc/error.rs +++ b/golem-registry-service/src/grpc/error.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::repo::model::card::CardRepoError; use crate::services::account_usage::error::{AccountUsageError, LimitExceededError}; use crate::services::auth::AuthError; use crate::services::component::ComponentError; @@ -111,6 +112,16 @@ impl From for GrpcApiError { } } +impl From for GrpcApiError { + fn from(value: CardRepoError) -> Self { + Self::InternalError(ErrorBody { + error: value.to_string(), + code: api::error_code::INTERNAL_UNKNOWN.to_string(), + cause: Some(value.into_anyhow()), + }) + } +} + impl From for GrpcApiError { fn from(value: AuthError) -> Self { let error: String = value.to_string(); diff --git a/golem-registry-service/src/grpc/mod.rs b/golem-registry-service/src/grpc/mod.rs index 7a35f308e6..4262fb2b1d 100644 --- a/golem-registry-service/src/grpc/mod.rs +++ b/golem-registry-service/src/grpc/mod.rs @@ -70,6 +70,7 @@ pub async fn start_grpc_server( RegistryServiceServer::new(RegistryServiceGrpcApi::new( services.auth_service.clone(), services.account_usage_service.clone(), + services.card_service.clone(), services.component_service.clone(), services.component_resolver_service.clone(), services.deployment_service.clone(), diff --git a/golem-registry-service/src/services/card.rs b/golem-registry-service/src/services/card.rs index 268a1c65b1..59680e9741 100644 --- a/golem-registry-service/src/services/card.rs +++ b/golem-registry-service/src/services/card.rs @@ -55,4 +55,8 @@ impl CardService { Ok(card_id) } + + pub async fn existing(&self, card_ids: Vec) -> Result, CardRepoError> { + self.card_repo.existing(card_ids).await.map_err(Into::into) + } } diff --git a/golem-service-base/src/clients/registry.rs b/golem-service-base/src/clients/registry.rs index 3ca22e54c3..522ef24353 100644 --- a/golem-service-base/src/clients/registry.rs +++ b/golem-service-base/src/clients/registry.rs @@ -23,15 +23,16 @@ use async_trait::async_trait; use golem_api_grpc::proto::golem::registry::ResourceUsageUpdate as GrpcResourceUsageUpdate; use golem_api_grpc::proto::golem::registry::v1::registry_service_client::RegistryServiceClient; use golem_api_grpc::proto::golem::registry::v1::{ - AuthenticateTokenRequest, BatchUpdateResourceUsageRequest, DownloadComponentRequest, - GetActiveMcpForDomainRequest, GetActiveRoutesForDomainRequest, GetAgentTypeRequest, - GetAllAgentTypesRequest, GetAllDeployedComponentRevisionsRequest, GetComponentMetadataRequest, - GetCurrentEnvironmentStateRequest, GetDeployedComponentMetadataRequest, - GetResourceDefinitionByIdRequest, GetResourceDefinitionByNameRequest, GetResourceLimitsRequest, - ResolveAgentTypeByNamesRequest, ResolveComponentRequest, UpdateWorkerConnectionLimitRequest, - authenticate_token_response, batch_update_resource_usage_response, download_component_response, - get_active_mcp_for_domain_response, get_active_routes_for_domain_response, - get_agent_type_response, get_all_agent_types_response, + AuthenticateTokenRequest, BatchGetExistingCardsRequest, BatchUpdateResourceUsageRequest, + DownloadComponentRequest, GetActiveMcpForDomainRequest, GetActiveRoutesForDomainRequest, + GetAgentTypeRequest, GetAllAgentTypesRequest, GetAllDeployedComponentRevisionsRequest, + GetComponentMetadataRequest, GetCurrentEnvironmentStateRequest, + GetDeployedComponentMetadataRequest, GetResourceDefinitionByIdRequest, + GetResourceDefinitionByNameRequest, GetResourceLimitsRequest, ResolveAgentTypeByNamesRequest, + ResolveComponentRequest, UpdateWorkerConnectionLimitRequest, authenticate_token_response, + batch_get_existing_cards_response, batch_update_resource_usage_response, + download_component_response, get_active_mcp_for_domain_response, + get_active_routes_for_domain_response, get_agent_type_response, get_all_agent_types_response, get_all_deployed_component_revisions_response, get_component_metadata_response, get_current_environment_state_response, get_deployed_component_metadata_response, get_resource_definition_by_id_response, get_resource_definition_by_name_response, @@ -46,6 +47,7 @@ use golem_common::model::agent::{ }; use golem_common::model::application::{ApplicationId, ApplicationName}; use golem_common::model::auth::TokenSecret; +use golem_common::model::card::CardId; use golem_common::model::component::{ComponentId, ComponentRevision}; use golem_common::model::deployment::{CurrentDeploymentRevision, DeploymentRevision}; use golem_common::model::domain_registration::Domain; @@ -105,6 +107,13 @@ pub trait RegistryService: Send + Sync { updates: HashMap, ) -> Result; + async fn batch_get_existing_cards( + &self, + card_ids: Vec, + ) -> Result, RegistryServiceError> { + Ok(card_ids) + } + // components api // will return the component even if it is deleted async fn download_component( @@ -547,6 +556,38 @@ impl RegistryService for GrpcRegistryService { } } + async fn batch_get_existing_cards( + &self, + card_ids: Vec, + ) -> Result, RegistryServiceError> { + let request_card_ids = card_ids + .into_iter() + .map(|id| id.0.into()) + .collect::>(); + + let response = self + .client + .call("batch_get_existing_cards", move |client| { + let request = BatchGetExistingCardsRequest { + card_ids: request_card_ids.clone(), + }; + + Box::pin(client.batch_get_existing_cards(request)) + }) + .await? + .into_inner(); + + match response.result { + None => Err(RegistryServiceError::empty_response()), + Some(batch_get_existing_cards_response::Result::Success(payload)) => Ok(payload + .card_ids + .into_iter() + .map(|id| CardId(id.into())) + .collect()), + Some(batch_get_existing_cards_response::Result::Error(error)) => Err(error.into()), + } + } + async fn download_component( &self, component_id: ComponentId, diff --git a/golem-test-framework/src/dsl/debug_render.rs b/golem-test-framework/src/dsl/debug_render.rs index fd947f7086..269b735be3 100644 --- a/golem-test-framework/src/dsl/debug_render.rs +++ b/golem-test-framework/src/dsl/debug_render.rs @@ -466,6 +466,11 @@ pub fn debug_render_oplog_entry(entry: &PublicOplogEntry) -> String { let _ = writeln!(result, "{pad}at: {}", ¶ms.timestamp); let _ = writeln!(result, "{pad}name: {}", ¶ms.name); } + PublicOplogEntry::CardRevoked(params) => { + let _ = writeln!(result, "CARD REVOKED"); + let _ = writeln!(result, "{pad}at: {}", ¶ms.timestamp); + let _ = writeln!(result, "{pad}card id: {}", ¶ms.card_id); + } } result diff --git a/golem-worker-executor/src/model/public_oplog/mod.rs b/golem-worker-executor/src/model/public_oplog/mod.rs index ff204ca65b..ea8a847947 100644 --- a/golem-worker-executor/src/model/public_oplog/mod.rs +++ b/golem-worker-executor/src/model/public_oplog/mod.rs @@ -35,6 +35,19 @@ use golem_common::model::oplog::public_oplog_entry::{ SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, }; +use golem_common::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, + CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + HostCallParams, InterruptedParams, JumpParams, LogParams, NoOpParams, + OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, + PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, + RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, + SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, +}; use golem_common::model::oplog::types::encode_span_data; use golem_common::model::oplog::{ AgentInitializationParameters, AgentInvocationOutputParameters, @@ -880,6 +893,12 @@ impl PublicOplogEntryOps for PublicOplogEntry { OplogEntry::RemoveRetryPolicy { timestamp, name } => Ok( PublicOplogEntry::RemoveRetryPolicy(RemoveRetryPolicyParams { timestamp, name }), ), + OplogEntry::CardRevoked { timestamp, card_id } => { + Ok(PublicOplogEntry::CardRevoked(CardRevokedParams { + timestamp, + card_id, + })) + } } } } diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index fd7d58e95e..de181386c9 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -30,6 +30,23 @@ use golem_common::model::oplog::public_oplog_entry::{ StringAttributeValue, SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, WriteRemoteTransactionParameters, }; +use golem_common::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, + CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + HostCallParams, InterruptedParams, JumpParams, LogParams, ManualUpdateParameters, NoOpParams, + OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, + PluginInstallationDescription, PreCommitRemoteTransactionParams, + PreRollbackRemoteTransactionParams, PublicAgentInvocation, PublicAgentInvocationResult, + PublicAttributeValue, PublicDurableFunctionType, PublicSpanData, RemoveRetryPolicyParams, + RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, + SetSpanAttributeParams, SnapshotParams, StartSpanParams, StringAttributeValue, + SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, + WriteRemoteTransactionParameters, +}; use golem_common::model::oplog::{ AgentInvocationOutputParameters, AgentTerminatedByQuotaError, EphemeralCannotSuspendError, EphemeralFuelExhaustedError, EphemeralSleepTooLongError, FallibleResultParameters, @@ -443,6 +460,10 @@ impl From for oplog::PublicOplogEntry { name, }) } + PublicOplogEntry::CardRevoked(CardRevokedParams { timestamp, card_id }) => { + let _ = card_id; + Self::NoOp(timestamp.into()) + } } } } diff --git a/golem-worker-executor/src/services/component.rs b/golem-worker-executor/src/services/component.rs index 20b1e0badb..1b1a911de0 100644 --- a/golem-worker-executor/src/services/component.rs +++ b/golem-worker-executor/src/services/component.rs @@ -95,6 +95,13 @@ pub trait ComponentService: Send + Sync { fn is_card_revoked(&self, _card_id: CardId) -> bool { false } + + async fn existing_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError> { + Ok(card_ids) + } } pub fn configured( @@ -414,6 +421,28 @@ impl ComponentService for ComponentServiceDefault { fn is_card_revoked(&self, card_id: CardId) -> bool { self.revoked_cards.read().unwrap().contains(&card_id) } + + async fn existing_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError> { + let existing = self + .registry_client + .batch_get_existing_cards(card_ids.clone()) + .await + .map_err(|err| { + WorkerExecutorError::runtime(format!( + "Failed checking card existence: {}", + err.to_safe_string() + )) + })?; + let missing = card_ids + .into_iter() + .filter(|card_id| !existing.contains(card_id)) + .collect::>(); + self.record_revoked_cards(&missing); + Ok(existing) + } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] diff --git a/golem-worker-executor/src/worker/mod.rs b/golem-worker-executor/src/worker/mod.rs index e2f9727bf2..d631ca1c5c 100644 --- a/golem-worker-executor/src/worker/mod.rs +++ b/golem-worker-executor/src/worker/mod.rs @@ -3645,6 +3645,40 @@ impl RunningWorker { .current_component .store(Arc::new(component_metadata.clone())); + let component_service = parent.component_service(); + component_service.record_revoked_cards( + &worker_metadata + .last_known_status + .revoked_cards + .iter() + .copied() + .collect::>(), + ); + + if let Some(card_id) = parent.parsed_agent_id.as_ref().and_then(|agent_id| { + component_metadata + .metadata + .agent_type_initial_permission_template(&agent_id.agent_type) + .map(|template| template.card_id) + }) { + let exists = !component_service.is_card_revoked(card_id) + && component_service + .existing_cards(vec![card_id]) + .await? + .contains(&card_id); + if !exists + && !worker_metadata + .last_known_status + .revoked_cards + .contains(&card_id) + { + component_service.record_revoked_cards(&[card_id]); + parent + .add_and_commit_oplog(OplogEntry::card_revoked(card_id.0)) + .await; + } + } + let component_version_for_replay = worker_metadata .last_known_status .pending_updates diff --git a/golem-worker-executor/src/worker/status.rs b/golem-worker-executor/src/worker/status.rs index 1f2ec73907..7a95109b77 100644 --- a/golem-worker-executor/src/worker/status.rs +++ b/golem-worker-executor/src/worker/status.rs @@ -318,6 +318,9 @@ pub fn update_status_with_new_entries( let active_plugins = calculate_active_plugins(active_plugins, &deleted_regions, &new_entries); + let revoked_cards = + calculate_revoked_cards(last_known.revoked_cards, &deleted_regions, &new_entries); + let oplog_processor_checkpoints = calculate_oplog_processor_checkpoints( last_known.oplog_processor_checkpoints, &active_plugins, @@ -347,6 +350,7 @@ pub fn update_status_with_new_entries( current_filesystem_storage_usage, active_plugins, oplog_processor_checkpoints, + revoked_cards, deleted_regions, component_revision_for_replay, current_retry_state, @@ -519,6 +523,7 @@ fn calculate_latest_worker_status( } OplogEntry::Snapshot { .. } => {} OplogEntry::OplogProcessorCheckpoint { .. } => {} + OplogEntry::CardRevoked { .. } => {} OplogEntry::Error { .. } => { // .. handled separately } @@ -527,6 +532,24 @@ fn calculate_latest_worker_status( (current_status, current_retry_state, current_retry_policy) } +fn calculate_revoked_cards( + mut revoked_cards: HashSet, + deleted_regions: &DeletedRegions, + entries: &BTreeMap, +) -> HashSet { + for (idx, entry) in entries { + if deleted_regions.is_in_deleted_region(*idx) { + continue; + } + + if let OplogEntry::CardRevoked { card_id, .. } = entry { + revoked_cards.insert(golem_common::model::card::CardId(*card_id)); + } + } + + revoked_cards +} + fn calculate_deleted_regions( initial_deleted: DeletedRegions, entries: &BTreeMap, @@ -3120,4 +3143,26 @@ mod test { let result = super::calculate_current_filesystem_storage_usage(1024, &deleted, &entries); assert_eq!(result, 1536, "seed + delta"); } + + #[test] + fn card_revoked_entry_is_recorded_in_status() { + let card_id = golem_common::model::card::CardId::new(); + let entries = BTreeMap::from([( + OplogIndex::from_u64(1), + OplogEntry::CardRevoked { + timestamp: Timestamp::now_utc(), + card_id: card_id.0, + }, + )]); + + let status = super::update_status_with_new_entries( + AgentMode::Durable, + AgentStatusRecord::default(), + entries, + &RetryConfig::default(), + ) + .unwrap(); + + assert!(status.revoked_cards.contains(&card_id)); + } } From f93beda324353d3e00966dca38eae817ab9d1fd8 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 21:03:58 +0200 Subject: [PATCH 03/13] update executor --- golem-debugging-service/src/debug_context.rs | 7 + golem-debugging-service/src/lib.rs | 7 + .../tests/debug_mode/debug_bootstrap.rs | 3 + golem-worker-executor-test-utils/src/lib.rs | 7 + golem-worker-executor/src/durable_host/mod.rs | 16 ++- golem-worker-executor/src/lib.rs | 12 +- golem-worker-executor/src/services/card.rs | 125 ++++++++++++++++++ .../src/services/component.rs | 68 ---------- golem-worker-executor/src/services/mod.rs | 18 +++ .../src/services/registry_event_subscriber.rs | 6 +- golem-worker-executor/src/services/rpc.rs | 29 ++-- .../src/services/worker_fork.rs | 27 ++-- golem-worker-executor/src/worker/mod.rs | 23 ++-- .../src/workerctx/default.rs | 7 + golem-worker-executor/src/workerctx/mod.rs | 4 + 15 files changed, 256 insertions(+), 103 deletions(-) create mode 100644 golem-worker-executor/src/services/card.rs diff --git a/golem-debugging-service/src/debug_context.rs b/golem-debugging-service/src/debug_context.rs index 93ab7f3bde..e7aabf197e 100644 --- a/golem-debugging-service/src/debug_context.rs +++ b/golem-debugging-service/src/debug_context.rs @@ -49,6 +49,7 @@ use golem_worker_executor::services::active_workers::ActiveWorkers; use golem_worker_executor::services::agent_types::AgentTypesService; use golem_worker_executor::services::agent_webhooks::AgentWebhooksService; use golem_worker_executor::services::blob_store::BlobStoreService; +use golem_worker_executor::services::card::CardService; use golem_worker_executor::services::component::ComponentService; use golem_worker_executor::services::environment_state::EnvironmentStateService; use golem_worker_executor::services::file_loader::FileLoader; @@ -562,6 +563,7 @@ impl WorkerCtx for DebugContext { scheduler_service: Arc, rpc: Arc, worker_proxy: Arc, + card_service: Arc, component_service: Arc, _extra_deps: Self::ExtraDeps, config: Arc, @@ -604,6 +606,7 @@ impl WorkerCtx for DebugContext { scheduler_service, rpc, worker_proxy, + card_service, component_service, account_resource_limits, config, @@ -688,6 +691,10 @@ impl WorkerCtx for DebugContext { self.durable_ctx.worker_proxy() } + fn card_service(&self) -> Arc { + self.durable_ctx.card_service() + } + fn component_service(&self) -> Arc { self.durable_ctx().component_service() } diff --git a/golem-debugging-service/src/lib.rs b/golem-debugging-service/src/lib.rs index 794b6aca7e..078f04770d 100644 --- a/golem-debugging-service/src/lib.rs +++ b/golem-debugging-service/src/lib.rs @@ -39,6 +39,7 @@ use golem_worker_executor::services::active_workers::ActiveWorkers; use golem_worker_executor::services::agent_types::AgentTypesService; use golem_worker_executor::services::agent_webhooks::AgentWebhooksService; use golem_worker_executor::services::blob_store::BlobStoreService; +use golem_worker_executor::services::card::CardService; use golem_worker_executor::services::component::ComponentService; use golem_worker_executor::services::direct_invocation_auth::DirectInvocationAuthService; use golem_worker_executor::services::environment_state::EnvironmentStateService; @@ -132,6 +133,7 @@ impl Bootstrap for ServerBootstrap { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, worker_service: Arc, @@ -167,6 +169,7 @@ impl Bootstrap for ServerBootstrap { engine, linker, runtime, + card_service, component_service, shard_manager_service, worker_service, @@ -211,6 +214,7 @@ pub async fn create_debugging_service_services( engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, worker_service: Arc, @@ -255,6 +259,7 @@ pub async fn create_debugging_service_services( engine.clone(), linker.clone(), runtime.clone(), + card_service.clone(), component_service.clone(), shard_manager_service.clone(), quota_service.clone(), @@ -297,6 +302,7 @@ pub async fn create_debugging_service_services( engine.clone(), linker.clone(), runtime.clone(), + card_service.clone(), component_service.clone(), worker_fork.clone(), worker_service.clone(), @@ -331,6 +337,7 @@ pub async fn create_debugging_service_services( active_workers, agent_types_service, agent_webhooks_service, + card_service, engine, linker, runtime, diff --git a/golem-debugging-service/tests/debug_mode/debug_bootstrap.rs b/golem-debugging-service/tests/debug_mode/debug_bootstrap.rs index c5d1c29e20..832b0e18e8 100644 --- a/golem-debugging-service/tests/debug_mode/debug_bootstrap.rs +++ b/golem-debugging-service/tests/debug_mode/debug_bootstrap.rs @@ -15,6 +15,7 @@ use golem_worker_executor::services::active_workers::ActiveWorkers; use golem_worker_executor::services::agent_types::AgentTypesService; use golem_worker_executor::services::agent_webhooks::AgentWebhooksService; use golem_worker_executor::services::blob_store::BlobStoreService; +use golem_worker_executor::services::card::CardService; use golem_worker_executor::services::component::ComponentService; use golem_worker_executor::services::direct_invocation_auth::DirectInvocationAuthService; use golem_worker_executor::services::environment_state::EnvironmentStateService; @@ -146,6 +147,7 @@ impl Bootstrap for TestDebuggingServerBootStrap { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, worker_service: Arc, @@ -181,6 +183,7 @@ impl Bootstrap for TestDebuggingServerBootStrap { engine, linker, runtime, + card_service, component_service, shard_manager_service, worker_service, diff --git a/golem-worker-executor-test-utils/src/lib.rs b/golem-worker-executor-test-utils/src/lib.rs index 2636c8102f..898b6bd479 100644 --- a/golem-worker-executor-test-utils/src/lib.rs +++ b/golem-worker-executor-test-utils/src/lib.rs @@ -89,6 +89,7 @@ use golem_worker_executor::services::agent_webhooks::AgentWebhooksService; use golem_worker_executor::services::blob_store::{ BlobStoreError, BlobStoreService, DefaultBlobStoreService, }; +use golem_worker_executor::services::card::CardService; use golem_worker_executor::services::component::ComponentService; use golem_worker_executor::services::direct_invocation_auth::{ DirectInvocationAuthService, NoOpDirectInvocationAuthService, @@ -1482,6 +1483,7 @@ impl WorkerCtx for TestWorkerCtx { scheduler_service: Arc, rpc: Arc, worker_proxy: Arc, + card_service: Arc, component_service: Arc, extra_deps: Self::ExtraDeps, config: Arc, @@ -1534,6 +1536,7 @@ impl WorkerCtx for TestWorkerCtx { scheduler_service, rpc, worker_proxy, + card_service, component_service, account_resource_limits, config, @@ -1618,6 +1621,10 @@ impl WorkerCtx for TestWorkerCtx { self.durable_ctx.worker_proxy() } + fn card_service(&self) -> Arc { + self.durable_ctx.card_service() + } + fn component_service(&self) -> Arc { self.durable_ctx.component_service() } diff --git a/golem-worker-executor/src/durable_host/mod.rs b/golem-worker-executor/src/durable_host/mod.rs index 11a7c58b97..4f652be341 100644 --- a/golem-worker-executor/src/durable_host/mod.rs +++ b/golem-worker-executor/src/durable_host/mod.rs @@ -51,6 +51,7 @@ use crate::model::{ use crate::services::agent_types::AgentTypesService; use crate::services::agent_webhooks::AgentWebhooksService; use crate::services::blob_store::BlobStoreService; +use crate::services::card::CardService; use crate::services::component::ComponentService; use crate::services::environment_state::EnvironmentStateService; use crate::services::file_loader::{FileLoader, FileUseToken}; @@ -337,6 +338,7 @@ impl DurableWorkerCtx { scheduler_service: Arc, rpc: Arc, worker_proxy: Arc, + card_service: Arc, component_service: Arc, resource_limits: Arc, config: Arc, @@ -502,6 +504,7 @@ impl DurableWorkerCtx { blob_store_service, rdbms_service, quota_service, + card_service, component_service, agent_types_service, environment_state_service, @@ -650,11 +653,7 @@ impl DurableWorkerCtx { ) }); - if self - .state - .component_service - .is_card_revoked(template.card_id) - { + if self.state.card_service.is_card_revoked(template.card_id) { return golem_common::model::card::EffectiveSurface::default(); } @@ -762,6 +761,10 @@ impl DurableWorkerCtx { self.state.component_service.clone() } + pub fn card_service(&self) -> Arc { + self.state.card_service.clone() + } + pub fn agent_types_service(&self) -> Arc { self.state.agent_types_service.clone() } @@ -4424,6 +4427,7 @@ struct PrivateDurableWorkerState { blob_store_service: Arc, rdbms_service: Arc, quota_service: Arc, + card_service: Arc, component_service: Arc, agent_types_service: Arc, agent_webhooks_service: Arc, @@ -4604,6 +4608,7 @@ impl PrivateDurableWorkerState { blob_store_service: Arc, rdbms_service: Arc, quota_service: Arc, + card_service: Arc, component_service: Arc, agent_types_service: Arc, environment_state_service: Arc, @@ -4664,6 +4669,7 @@ impl PrivateDurableWorkerState { blob_store_service, rdbms_service, quota_service, + card_service, component_service, agent_types_service, environment_state_service, diff --git a/golem-worker-executor/src/lib.rs b/golem-worker-executor/src/lib.rs index be11de4af2..feef659bc3 100644 --- a/golem-worker-executor/src/lib.rs +++ b/golem-worker-executor/src/lib.rs @@ -48,6 +48,7 @@ use crate::grpc::WorkerExecutorImpl; use crate::services::active_workers::ActiveWorkers; use crate::services::agent_types::AgentTypesService; use crate::services::blob_store::{BlobStoreService, DefaultBlobStoreService}; +use crate::services::card::{CardService, CardServiceDefault}; use crate::services::component::ComponentService; use crate::services::events::Events; use crate::services::golem_config::{ @@ -77,7 +78,7 @@ use crate::services::worker_enumeration::{ }; use crate::services::worker_proxy::{RemoteWorkerProxy, WorkerProxy}; use crate::services::{ - All, HasActiveWorkers, HasAgentTypesService, HasComponentService, HasConfig, + All, HasActiveWorkers, HasAgentTypesService, HasCardService, HasComponentService, HasConfig, HasEnvironmentStateService, HasOplogService, HasWorkerActivator, HasWorkerService, rdbms, }; use crate::storage::indexed::IndexedStorage; @@ -300,6 +301,7 @@ pub trait Bootstrap { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, worker_service: Arc, @@ -338,6 +340,7 @@ pub trait Bootstrap { engine.clone(), linker.clone(), runtime.clone(), + card_service.clone(), component_service.clone(), shard_manager_service.clone(), quota_service.clone(), @@ -378,6 +381,7 @@ pub trait Bootstrap { engine.clone(), linker.clone(), runtime.clone(), + card_service.clone(), component_service.clone(), worker_fork.clone(), worker_service.clone(), @@ -413,6 +417,7 @@ pub trait Bootstrap { active_workers, agent_types_service, agent_webhooks_service, + card_service, engine, linker, runtime.clone(), @@ -698,6 +703,8 @@ pub async fn create_worker_executor_impl< registry_service.clone(), blob_storage.clone(), ); + let card_service: Arc = + Arc::new(CardServiceDefault::new(registry_service.clone())); let environment_state_service = bootstrap.create_environment_state_service( &golem_config.environment_state_service, @@ -919,6 +926,7 @@ pub async fn create_worker_executor_impl< engine, linker, runtime.clone(), + card_service, component_service, shard_manager_service, worker_service, @@ -1048,6 +1056,7 @@ pub async fn bootstrap_and_run_worker_executor< if start_registry_invalidation_handler { let registry_service = registry_service.clone(); let active_workers = worker_executor_impl.active_workers(); + let card_service = worker_executor_impl.card_service(); let component_service = worker_executor_impl.component_service(); let environment_state_service = worker_executor_impl.environment_state_service(); let agent_types_service = worker_executor_impl.agent_types(); @@ -1056,6 +1065,7 @@ pub async fn bootstrap_and_run_worker_executor< WorkerExecutorRegistryInvalidationHandler::run( registry_service, active_workers, + card_service, component_service, environment_state_service, agent_types_service, diff --git a/golem-worker-executor/src/services/card.rs b/golem-worker-executor/src/services/card.rs new file mode 100644 index 0000000000..e27906490a --- /dev/null +++ b/golem-worker-executor/src/services/card.rs @@ -0,0 +1,125 @@ +// Copyright 2024-2026 Golem Cloud +// +// Licensed under the Golem Source License v1.1 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://license.golem.cloud/LICENSE +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use async_trait::async_trait; +use golem_common::SafeDisplay; +use golem_common::model::card::CardId; +use golem_service_base::clients::registry::RegistryService; +use golem_service_base::error::worker_executor::WorkerExecutorError; +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; + +#[async_trait] +pub trait CardService: Send + Sync { + fn record_revoked_cards(&self, card_ids: &[CardId]); + + fn is_card_revoked(&self, card_id: CardId) -> bool; + + async fn existing_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError>; +} + +pub struct CardServiceDefault { + registry_service: Option>, + revoked_cards: RwLock>, +} + +pub struct NoopCardService; + +#[async_trait] +impl CardService for NoopCardService { + fn record_revoked_cards(&self, _card_ids: &[CardId]) {} + + fn is_card_revoked(&self, _card_id: CardId) -> bool { + false + } + + async fn existing_cards(&self, card_ids: Vec) -> Result, WorkerExecutorError> { + Ok(card_ids) + } +} + +impl CardServiceDefault { + pub fn new(registry_service: Arc) -> Self { + Self { + registry_service: Some(registry_service), + revoked_cards: RwLock::new(HashSet::new()), + } + } + + #[cfg(test)] + fn for_tests() -> Self { + Self { + registry_service: None, + revoked_cards: RwLock::new(HashSet::new()), + } + } +} + +#[async_trait] +impl CardService for CardServiceDefault { + fn record_revoked_cards(&self, card_ids: &[CardId]) { + let mut revoked_cards = self.revoked_cards.write().unwrap(); + revoked_cards.extend(card_ids.iter().copied()); + } + + fn is_card_revoked(&self, card_id: CardId) -> bool { + self.revoked_cards.read().unwrap().contains(&card_id) + } + + async fn existing_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError> { + let Some(registry_service) = &self.registry_service else { + return Ok(card_ids); + }; + let existing = registry_service + .batch_get_existing_cards(card_ids.clone()) + .await + .map_err(|err| { + WorkerExecutorError::runtime(format!( + "Failed checking card existence: {}", + err.to_safe_string() + )) + })?; + let missing = card_ids + .into_iter() + .filter(|card_id| !existing.contains(card_id)) + .collect::>(); + self.record_revoked_cards(&missing); + Ok(existing) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_r::test; + + #[test] + fn records_revoked_card_ids() { + let service = CardServiceDefault::for_tests(); + let revoked = CardId::new(); + let live = CardId::new(); + + assert!(!service.is_card_revoked(revoked)); + service.record_revoked_cards(&[revoked]); + + assert!(service.is_card_revoked(revoked)); + assert!(!service.is_card_revoked(live)); + } +} diff --git a/golem-worker-executor/src/services/component.rs b/golem-worker-executor/src/services/component.rs index 1b1a911de0..0cd81670b6 100644 --- a/golem-worker-executor/src/services/component.rs +++ b/golem-worker-executor/src/services/component.rs @@ -20,7 +20,6 @@ use golem_common::cache::SimpleCache; use golem_common::cache::{BackgroundEvictionMode, Cache, FullCacheEvictionMode}; use golem_common::model::account::AccountId; use golem_common::model::application::ApplicationId; -use golem_common::model::card::CardId; use golem_common::model::component::{ComponentId, ComponentRevision}; use golem_common::model::environment::EnvironmentId; use golem_service_base::clients::registry::{RegistryService, RegistryServiceError}; @@ -29,9 +28,7 @@ use golem_service_base::model::component::Component; use golem_service_base::service::compiled_component::CompiledComponentService; use golem_service_base::service::compiled_component::CompiledComponentServiceConfig; use golem_service_base::storage::blob::BlobStorage; -use std::collections::HashSet; use std::sync::Arc; -use std::sync::RwLock; use std::time::Duration; use std::time::Instant; use tokio::task::spawn_blocking; @@ -89,19 +86,6 @@ pub trait ComponentService: Send + Sync { async fn invalidate_all(&self) { self.invalidate_current_deployed_metadata().await } - - fn record_revoked_cards(&self, _card_ids: &[CardId]) {} - - fn is_card_revoked(&self, _card_id: CardId) -> bool { - false - } - - async fn existing_cards( - &self, - card_ids: Vec, - ) -> Result, WorkerExecutorError> { - Ok(card_ids) - } } pub fn configured( @@ -128,7 +112,6 @@ pub struct ComponentServiceDefault { current_component_metadata_cache: Cache, compiled_component_service: Arc, registry_client: Arc, - revoked_cards: Arc>>, } impl ComponentServiceDefault { @@ -151,7 +134,6 @@ impl ComponentServiceDefault { time_to_idle, ), compiled_component_service, - revoked_cards: Arc::new(RwLock::new(HashSet::new())), } } } @@ -412,37 +394,6 @@ impl ComponentService for ComponentServiceDefault { self.component_metadata_cache.remove(&key).await; } } - - fn record_revoked_cards(&self, card_ids: &[CardId]) { - let mut revoked_cards = self.revoked_cards.write().unwrap(); - revoked_cards.extend(card_ids.iter().copied()); - } - - fn is_card_revoked(&self, card_id: CardId) -> bool { - self.revoked_cards.read().unwrap().contains(&card_id) - } - - async fn existing_cards( - &self, - card_ids: Vec, - ) -> Result, WorkerExecutorError> { - let existing = self - .registry_client - .batch_get_existing_cards(card_ids.clone()) - .await - .map_err(|err| { - WorkerExecutorError::runtime(format!( - "Failed checking card existence: {}", - err.to_safe_string() - )) - })?; - let missing = card_ids - .into_iter() - .filter(|card_id| !existing.contains(card_id)) - .collect::>(); - self.record_revoked_cards(&missing); - Ok(existing) - } } #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -850,23 +801,4 @@ mod tests { "other environments should stay cached" ); } - - #[test] - fn records_revoked_card_ids() { - let service = ComponentServiceDefault::new( - Arc::new(MockRegistryService::new([])), - 1, - 8, - Duration::from_secs(60), - Arc::new(CompiledComponentServiceDisabled::new()), - ); - let revoked = CardId::new(); - let live = CardId::new(); - - assert!(!service.is_card_revoked(revoked)); - service.record_revoked_cards(&[revoked]); - - assert!(service.is_card_revoked(revoked)); - assert!(!service.is_card_revoked(live)); - } } diff --git a/golem-worker-executor/src/services/mod.rs b/golem-worker-executor/src/services/mod.rs index 2f5b2d210f..842a5aa01e 100644 --- a/golem-worker-executor/src/services/mod.rs +++ b/golem-worker-executor/src/services/mod.rs @@ -16,6 +16,7 @@ pub mod active_workers; pub mod agent_types; pub mod agent_webhooks; pub mod blob_store; +pub mod card; pub mod component; pub mod direct_invocation_auth; pub mod environment_state; @@ -86,6 +87,10 @@ pub trait HasComponentService { fn component_service(&self) -> Arc; } +pub trait HasCardService { + fn card_service(&self) -> Arc; +} + pub trait HasShardManagerService { fn shard_manager_service(&self) -> Arc; } @@ -215,6 +220,7 @@ pub trait HasAll: HasActiveWorkers + HasAgentTypesService + HasAgentWebhooksService + + HasCardService + HasComponentService + HasConfig + HasWorkerForkService @@ -254,6 +260,7 @@ impl< T: HasActiveWorkers + HasAgentTypesService + HasAgentWebhooksService + + HasCardService + HasComponentService + HasConfig + HasWorkerForkService @@ -295,6 +302,7 @@ pub struct All { active_workers: Arc>, agent_types: Arc, agent_webhooks: Arc, + card_service: Arc, engine: Arc, linker: Arc>, runtime: Handle, @@ -338,6 +346,7 @@ impl Clone for All { active_workers: self.active_workers.clone(), agent_types: self.agent_types.clone(), agent_webhooks: self.agent_webhooks.clone(), + card_service: self.card_service.clone(), engine: self.engine.clone(), linker: self.linker.clone(), runtime: self.runtime.clone(), @@ -379,6 +388,7 @@ impl All { active_workers: Arc>, agent_types: Arc, agent_webhooks: Arc, + card_service: Arc, engine: Arc, linker: Arc>, runtime: Handle, @@ -417,6 +427,7 @@ impl All { active_workers, agent_types, agent_webhooks, + card_service, engine, linker, runtime, @@ -463,6 +474,7 @@ impl All { this.active_workers(), this.agent_types(), this.agent_webhooks(), + this.card_service(), this.engine(), this.linker(), this.runtime(), @@ -530,6 +542,12 @@ impl> HasAgentWebhooksService for T { } } +impl> HasCardService for T { + fn card_service(&self) -> Arc { + self.all().card_service.clone() + } +} + impl> HasComponentService for T { fn component_service(&self) -> Arc { self.all().component_service.clone() diff --git a/golem-worker-executor/src/services/registry_event_subscriber.rs b/golem-worker-executor/src/services/registry_event_subscriber.rs index eb9d8aac52..bc9f3aa2b3 100644 --- a/golem-worker-executor/src/services/registry_event_subscriber.rs +++ b/golem-worker-executor/src/services/registry_event_subscriber.rs @@ -14,6 +14,7 @@ use crate::services::active_workers::ActiveWorkers; use crate::services::agent_types::AgentTypesService; +use crate::services::card::CardService; use crate::services::component::ComponentService; use crate::services::environment_state::EnvironmentStateService; use crate::workerctx::WorkerCtx; @@ -26,6 +27,7 @@ use tracing::{debug, warn}; pub(crate) struct WorkerExecutorRegistryInvalidationHandler { active_workers: Arc>, + card_service: Arc, component_service: Arc, environment_state_service: Arc, agent_types_service: Arc, @@ -35,6 +37,7 @@ impl WorkerExecutorRegistryInvalidationHandler { pub async fn run( registry_service: Arc, active_workers: Arc>, + card_service: Arc, component_service: Arc, environment_state_service: Arc, agent_types_service: Arc, @@ -46,6 +49,7 @@ impl WorkerExecutorRegistryInvalidationHandler { Some(shutdown_token), Arc::new(Self { active_workers, + card_service, component_service, environment_state_service, agent_types_service, @@ -148,7 +152,7 @@ impl RegistryInvalidationHandler card_count = card_ids.len(), "Received card revocation event, recording revoked card ids" ); - self.component_service.record_revoked_cards(&card_ids); + self.card_service.record_revoked_cards(&card_ids); } RegistryInvalidationEvent::ApplicationDeleted { application_id, diff --git a/golem-worker-executor/src/services/rpc.rs b/golem-worker-executor/src/services/rpc.rs index 8f30d1b2ac..89949a2eb0 100644 --- a/golem-worker-executor/src/services/rpc.rs +++ b/golem-worker-executor/src/services/rpc.rs @@ -24,15 +24,16 @@ use crate::services::resource_limits::ResourceLimits; use crate::services::shard::ShardService; use crate::services::worker_proxy::{WorkerProxy, WorkerProxyError}; use crate::services::{ - HasActiveWorkers, HasAgentTypesService, HasBlobStoreService, HasComponentService, HasConfig, - HasEvents, HasExtraDeps, HasFileLoader, HasHttpConnectionPool, HasKeyValueService, - HasLeakSentinel, HasOplogProcessorPlugin, HasOplogService, HasPromiseService, HasQuotaService, - HasRdbmsService, HasResourceLimits, HasRpc, HasRunningWorkerEnumerationService, - HasSchedulerService, HasShardManagerService, HasShardService, HasShutdownToken, - HasWasmtimeEngine, HasWorkerActivator, HasWorkerEnumerationService, HasWorkerForkService, - HasWorkerProxy, HasWorkerService, active_workers, agent_types, blob_store, component, - golem_config, key_value, oplog, promise, rdbms, scheduler, shard_manager, worker, - worker_activator, worker_enumeration, worker_fork, + HasActiveWorkers, HasAgentTypesService, HasBlobStoreService, HasCardService, + HasComponentService, HasConfig, HasEvents, HasExtraDeps, HasFileLoader, HasHttpConnectionPool, + HasKeyValueService, HasLeakSentinel, HasOplogProcessorPlugin, HasOplogService, + HasPromiseService, HasQuotaService, HasRdbmsService, HasResourceLimits, HasRpc, + HasRunningWorkerEnumerationService, HasSchedulerService, HasShardManagerService, + HasShardService, HasShutdownToken, HasWasmtimeEngine, HasWorkerActivator, + HasWorkerEnumerationService, HasWorkerForkService, HasWorkerProxy, HasWorkerService, + active_workers, agent_types, blob_store, card, component, golem_config, key_value, oplog, + promise, rdbms, scheduler, shard_manager, worker, worker_activator, worker_enumeration, + worker_fork, }; use crate::worker::Worker; use crate::workerctx::WorkerCtx; @@ -412,6 +413,7 @@ pub struct DirectWorkerInvocationRpc { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, quota_service: Arc, @@ -452,6 +454,7 @@ impl Clone for DirectWorkerInvocationRpc { engine: self.engine.clone(), linker: self.linker.clone(), runtime: self.runtime.clone(), + card_service: self.card_service.clone(), component_service: self.component_service.clone(), shard_manager_service: self.shard_manager_service.clone(), quota_service: self.quota_service.clone(), @@ -514,6 +517,12 @@ impl HasComponentService for DirectWorkerInvocationRpc { } } +impl HasCardService for DirectWorkerInvocationRpc { + fn card_service(&self) -> Arc { + self.card_service.clone() + } +} + impl HasConfig for DirectWorkerInvocationRpc { fn config(&self) -> Arc { self.golem_config.clone() @@ -696,6 +705,7 @@ impl DirectWorkerInvocationRpc { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, worker_fork: Arc, worker_service: Arc, @@ -734,6 +744,7 @@ impl DirectWorkerInvocationRpc { engine, linker, runtime, + card_service, component_service, shard_manager_service, quota_service, diff --git a/golem-worker-executor/src/services/worker_fork.rs b/golem-worker-executor/src/services/worker_fork.rs index 2fd80eeb0f..9c16993691 100644 --- a/golem-worker-executor/src/services/worker_fork.rs +++ b/golem-worker-executor/src/services/worker_fork.rs @@ -27,14 +27,15 @@ use crate::services::rpc::Rpc; use crate::services::shard::ShardService; use crate::services::worker_proxy::WorkerProxy; use crate::services::{ - HasActiveWorkers, HasAgentTypesService, HasBlobStoreService, HasComponentService, HasConfig, - HasEvents, HasExtraDeps, HasFileLoader, HasHttpConnectionPool, HasKeyValueService, - HasLeakSentinel, HasOplogProcessorPlugin, HasOplogService, HasPromiseService, HasQuotaService, - HasResourceLimits, HasRpc, HasRunningWorkerEnumerationService, HasSchedulerService, - HasShardManagerService, HasShardService, HasShutdownToken, HasWasmtimeEngine, - HasWebSocketConnectionPool, HasWorkerActivator, HasWorkerEnumerationService, HasWorkerProxy, - HasWorkerService, active_workers, agent_types, blob_store, component, golem_config, key_value, - oplog, promise, scheduler, shard_manager, worker, worker_activator, worker_enumeration, + HasActiveWorkers, HasAgentTypesService, HasBlobStoreService, HasCardService, + HasComponentService, HasConfig, HasEvents, HasExtraDeps, HasFileLoader, HasHttpConnectionPool, + HasKeyValueService, HasLeakSentinel, HasOplogProcessorPlugin, HasOplogService, + HasPromiseService, HasQuotaService, HasResourceLimits, HasRpc, + HasRunningWorkerEnumerationService, HasSchedulerService, HasShardManagerService, + HasShardService, HasShutdownToken, HasWasmtimeEngine, HasWebSocketConnectionPool, + HasWorkerActivator, HasWorkerEnumerationService, HasWorkerProxy, HasWorkerService, + active_workers, agent_types, blob_store, card, component, golem_config, key_value, oplog, + promise, scheduler, shard_manager, worker, worker_activator, worker_enumeration, }; use crate::services::{HasOplog, HasRdbmsService, HasWorkerForkService, rdbms}; use crate::worker::Worker; @@ -94,6 +95,7 @@ pub struct DefaultWorkerFork { pub engine: Arc, pub linker: Arc>, pub runtime: Handle, + pub card_service: Arc, pub component_service: Arc, pub shard_manager_service: Arc, pub quota_service: Arc, @@ -153,6 +155,12 @@ impl HasComponentService for DefaultWorkerFork { } } +impl HasCardService for DefaultWorkerFork { + fn card_service(&self) -> Arc { + self.card_service.clone() + } +} + impl HasConfig for DefaultWorkerFork { fn config(&self) -> Arc { self.golem_config.clone() @@ -335,6 +343,7 @@ impl Clone for DefaultWorkerFork { engine: self.engine.clone(), linker: self.linker.clone(), runtime: self.runtime.clone(), + card_service: self.card_service.clone(), component_service: self.component_service.clone(), shard_manager_service: self.shard_manager_service.clone(), quota_service: self.quota_service.clone(), @@ -373,6 +382,7 @@ impl DefaultWorkerFork { engine: Arc, linker: Arc>, runtime: Handle, + card_service: Arc, component_service: Arc, shard_manager_service: Arc, quota_service: Arc, @@ -412,6 +422,7 @@ impl DefaultWorkerFork { engine, linker, runtime, + card_service, component_service, shard_manager_service, quota_service, diff --git a/golem-worker-executor/src/worker/mod.rs b/golem-worker-executor/src/worker/mod.rs index d631ca1c5c..84da864425 100644 --- a/golem-worker-executor/src/worker/mod.rs +++ b/golem-worker-executor/src/worker/mod.rs @@ -39,12 +39,12 @@ use crate::services::worker::GetWorkerMetadataResult; use crate::services::worker_event::{WorkerEventService, WorkerEventServiceDefault}; use crate::services::{ All, HasActiveWorkers, HasAgentTypesService, HasAgentWebhooksService, HasAll, - HasBlobStoreService, HasComponentService, HasConfig, HasEnvironmentStateService, HasEvents, - HasExtraDeps, HasFileLoader, HasHttpConnectionPool, HasKeyValueService, HasOplog, - HasOplogService, HasPromiseService, HasQuotaService, HasRdbmsService, HasResourceLimits, - HasRpc, HasSchedulerService, HasShardService, HasWasmtimeEngine, HasWebSocketConnectionPool, - HasWorkerEnumerationService, HasWorkerForkService, HasWorkerProxy, HasWorkerService, - UsesAllDeps, + HasBlobStoreService, HasCardService, HasComponentService, HasConfig, + HasEnvironmentStateService, HasEvents, HasExtraDeps, HasFileLoader, HasHttpConnectionPool, + HasKeyValueService, HasOplog, HasOplogService, HasPromiseService, HasQuotaService, + HasRdbmsService, HasResourceLimits, HasRpc, HasSchedulerService, HasShardService, + HasWasmtimeEngine, HasWebSocketConnectionPool, HasWorkerEnumerationService, + HasWorkerForkService, HasWorkerProxy, HasWorkerService, UsesAllDeps, }; use crate::worker::invocation_loop::InvocationLoop; use crate::worker::status::calculate_last_known_status_with_checkpoint; @@ -3645,8 +3645,8 @@ impl RunningWorker { .current_component .store(Arc::new(component_metadata.clone())); - let component_service = parent.component_service(); - component_service.record_revoked_cards( + let card_service = parent.card_service(); + card_service.record_revoked_cards( &worker_metadata .last_known_status .revoked_cards @@ -3661,8 +3661,8 @@ impl RunningWorker { .agent_type_initial_permission_template(&agent_id.agent_type) .map(|template| template.card_id) }) { - let exists = !component_service.is_card_revoked(card_id) - && component_service + let exists = !card_service.is_card_revoked(card_id) + && card_service .existing_cards(vec![card_id]) .await? .contains(&card_id); @@ -3672,7 +3672,7 @@ impl RunningWorker { .revoked_cards .contains(&card_id) { - component_service.record_revoked_cards(&[card_id]); + card_service.record_revoked_cards(&[card_id]); parent .add_and_commit_oplog(OplogEntry::card_revoked(card_id.0)) .await; @@ -3735,6 +3735,7 @@ impl RunningWorker { parent.scheduler_service(), parent.rpc(), parent.worker_proxy(), + parent.card_service(), parent.component_service(), parent.extra_deps(), parent.config(), diff --git a/golem-worker-executor/src/workerctx/default.rs b/golem-worker-executor/src/workerctx/default.rs index 06ed36a878..06bd3b5bf0 100644 --- a/golem-worker-executor/src/workerctx/default.rs +++ b/golem-worker-executor/src/workerctx/default.rs @@ -25,6 +25,7 @@ use crate::services::active_workers::ActiveWorkers; use crate::services::agent_types::AgentTypesService; use crate::services::agent_webhooks::AgentWebhooksService; use crate::services::blob_store::BlobStoreService; +use crate::services::card::CardService; use crate::services::component::ComponentService; use crate::services::environment_state::EnvironmentStateService; use crate::services::file_loader::FileLoader; @@ -863,6 +864,7 @@ impl WorkerCtx for Context { scheduler_service: Arc, rpc: Arc, worker_proxy: Arc, + card_service: Arc, component_service: Arc, _extra_deps: Self::ExtraDeps, config: Arc, @@ -898,6 +900,7 @@ impl WorkerCtx for Context { scheduler_service, rpc, worker_proxy, + card_service, component_service, account_resource_limits.clone(), config.clone(), @@ -980,6 +983,10 @@ impl WorkerCtx for Context { self.durable_ctx.worker_proxy() } + fn card_service(&self) -> Arc { + self.durable_ctx.card_service() + } + fn component_service(&self) -> Arc { self.durable_ctx.component_service() } diff --git a/golem-worker-executor/src/workerctx/mod.rs b/golem-worker-executor/src/workerctx/mod.rs index ea0bc503f3..008afb7af0 100644 --- a/golem-worker-executor/src/workerctx/mod.rs +++ b/golem-worker-executor/src/workerctx/mod.rs @@ -21,6 +21,7 @@ use crate::services::active_workers::ActiveWorkers; use crate::services::agent_types::AgentTypesService; use crate::services::agent_webhooks::AgentWebhooksService; use crate::services::blob_store::BlobStoreService; +use crate::services::card::CardService; use crate::services::component::ComponentService; use crate::services::environment_state::EnvironmentStateService; use crate::services::file_loader::FileLoader; @@ -137,6 +138,7 @@ pub trait WorkerCtx: scheduler_service: Arc, rpc: Arc, worker_proxy: Arc, + card_service: Arc, component_service: Arc, extra_deps: Self::ExtraDeps, config: Arc, @@ -200,6 +202,8 @@ pub trait WorkerCtx: /// in the cluster fn worker_proxy(&self) -> Arc; + fn card_service(&self) -> Arc; + fn component_service(&self) -> Arc; fn worker_fork(&self) -> Arc; From a6b6d2cc5a39ca44a04ce8fe73fa7d8fd92af7ae Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 22:01:25 +0200 Subject: [PATCH 04/13] wip --- golem-common/src/model/oplog/protobuf.rs | 21 +-- golem-worker-executor/src/durable_host/mod.rs | 170 +++++++++++------- golem-worker-executor/src/model/mod.rs | 4 + .../src/model/public_oplog/mod.rs | 15 +- .../src/model/public_oplog/wit.rs | 19 +- .../src/services/active_workers/mod.rs | 4 +- golem-worker-executor/src/services/card.rs | 103 ++++++----- golem-worker-executor/src/services/mod.rs | 2 +- golem-worker-executor/src/worker/mod.rs | 64 ++++--- 9 files changed, 206 insertions(+), 196 deletions(-) diff --git a/golem-common/src/model/oplog/protobuf.rs b/golem-common/src/model/oplog/protobuf.rs index 07b67b7b7d..09804b882b 100644 --- a/golem-common/src/model/oplog/protobuf.rs +++ b/golem-common/src/model/oplog/protobuf.rs @@ -37,29 +37,16 @@ use crate::model::oplog::payload::host_functions::{ }; use crate::model::oplog::public_oplog_entry::{ ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, - CancelledParams, ChangePersistenceLevelParams, CommittedRemoteTransactionParams, CreateParams, - CreateResourceParams, DeactivatePluginParams, DropResourceParams, EndAtomicRegionParams, - EndParams, ErrorParams, ExitedParams, FailedUpdateParams, FilesystemStorageUsageUpdateParams, - FinishSpanParams, GrowMemoryParams, InterruptedParams, JumpParams, LogParams, NoOpParams, - OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, - PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, - RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, - SuspendParams, -}; -use crate::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, - DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, + DropResourceParams, EndAtomicRegionParams, EndParams, ErrorParams, ExitedParams, FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, - HostCallParams, InterruptedParams, JumpParams, LogParams, NoOpParams, + StartParams, InterruptedParams, JumpParams, LogParams, NoOpParams, OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, + SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, CancelledParams }; use crate::model::oplog::{ AgentTerminatedByQuotaError, DurableFunctionType, EphemeralCannotSuspendError, diff --git a/golem-worker-executor/src/durable_host/mod.rs b/golem-worker-executor/src/durable_host/mod.rs index 4f652be341..e30bcab67f 100644 --- a/golem-worker-executor/src/durable_host/mod.rs +++ b/golem-worker-executor/src/durable_host/mod.rs @@ -258,6 +258,49 @@ pub enum InvocationStrictness { ReadOnly, } +pub(crate) fn agent_effective_surface_from_component_metadata( + component: &Component, + owned_agent_id: &OwnedAgentId, + agent_id: &LegacyParsedAgentId, +) -> golem_common::model::card::EffectiveSurface { + let template = component + .metadata + .agent_type_initial_permission_template(&agent_id.agent_type) + .cloned() + .unwrap_or_else(|| { + golem_common::model::component_metadata::AgentInitialPermissionTemplate::default_for( + &component.environment_name, + &component.component_name, + ) + }); + + let context = golem_common::model::card::AgentPermissionMonomorphizationContext { + account: component.account_email.clone(), + application: component.application_name.clone(), + environment: component.environment_name.clone(), + component: component.component_name.clone(), + agent_name: owned_agent_id.agent_id.agent_id.clone(), + agent_type: agent_id.agent_type.clone(), + }; + + let Ok(mut card) = golem_common::model::card::monomorphize_agent_initial_card( + &template.lower_positive, + &template.lower_negative, + &template.upper_positive, + &template.upper_negative, + &context, + ) else { + return golem_common::model::card::EffectiveSurface::default(); + }; + card.card_id = template.card_id; + + golem_common::model::card::EffectiveSurface::from_cards( + std::slice::from_ref(&card), + &golem_common::model::card::recipient::RecipientPattern::Any, + ) + .unwrap_or_default() +} + /// Partial implementation of the WorkerCtx interfaces for adding durable execution to workers. pub struct DurableWorkerCtx { table: Arc>, // Required because of the dropped Sync constraints in https://github.com/bytecodealliance/wasmtime/pull/7802 @@ -517,6 +560,7 @@ impl DurableWorkerCtx { component_metadata, worker_config.total_linear_memory_size, worker_config.current_filesystem_storage_usage, + worker_config.agent_effective_surface, worker_fork, RwLock::new(compute_read_only_paths(&files)), TRwLock::new(files), @@ -637,57 +681,7 @@ impl DurableWorkerCtx { } pub fn agent_effective_surface(&self) -> golem_common::model::card::EffectiveSurface { - let Some(agent_id) = &self.state.agent_id else { - return golem_common::model::card::EffectiveSurface::default(); - }; - - let component = &self.state.component_metadata; - let template = component - .metadata - .agent_type_initial_permission_template(&agent_id.agent_type) - .cloned() - .unwrap_or_else(|| { - golem_common::model::component_metadata::AgentInitialPermissionTemplate::default_for( - &component.environment_name, - &component.component_name, - ) - }); - - if self.state.card_service.is_card_revoked(template.card_id) { - return golem_common::model::card::EffectiveSurface::default(); - } - - let context = golem_common::model::card::AgentPermissionMonomorphizationContext { - account: component.account_email.clone(), - application: component.application_name.clone(), - environment: component.environment_name.clone(), - component: component.component_name.clone(), - agent_name: self.owned_agent_id.agent_id.agent_id.clone(), - agent_type: agent_id.agent_type.clone(), - }; - - let Ok(mut card) = golem_common::model::card::monomorphize_agent_initial_card( - &template.lower_positive, - &template.lower_negative, - &template.upper_positive, - &template.upper_negative, - &context, - ) else { - return golem_common::model::card::EffectiveSurface::default(); - }; - card.card_id = template.card_id; - - golem_common::model::card::EffectiveSurface::from_cards( - std::slice::from_ref(&card), - &golem_common::model::card::recipient::RecipientPattern::Agent { - account: component.account_email.as_str().to_string(), - application: component.application_name.0.clone(), - environment: component.environment_name.0.clone(), - component: component.component_name.0.clone(), - agent: self.owned_agent_id.agent_id.agent_id.clone(), - }, - ) - .unwrap_or_default() + self.state.agent_effective_surface.clone() } pub fn agent_auth_ctx(&self) -> AuthCtx { @@ -2526,23 +2520,7 @@ impl DurableWorkerCtx { .cloned() }); - let mut current_files = self.state.files.write().await; - update_filesystem( - &mut current_files, - &self.state.file_loader, - self.owned_agent_id.environment_id, - self.worker_dir.path(), - new_agent_type_provision_configs - .as_ref() - .map(|c| c.files.as_slice()) - .unwrap_or_default(), - ) - .await?; - - let mut read_only_paths = self.state.read_only_paths.write().unwrap(); - *read_only_paths = compute_read_only_paths(¤t_files); - - if let Some(agent_id) = self.parsed_agent_id() { + let updated_agent_state = if let Some(agent_id) = self.parsed_agent_id() { let agent_type = new_metadata .metadata .find_agent_type_by_name_ref(&agent_id.agent_type) @@ -2563,8 +2541,61 @@ impl DurableWorkerCtx { validate_agent_config(&updated_agent_config, agent_type)?; + let liveness = match new_metadata + .metadata + .agent_type_initial_permission_template(&agent_id.agent_type) + .map(|template| template.card_id) + { + Some(card_id) => { + let liveness = self.state.card_service.check_card(card_id).await?; + if liveness.newly_detected_revocation() { + self.public_state + .worker() + .add_and_commit_oplog(OplogEntry::card_revoked(card_id.0)) + .await; + } + liveness + } + None => crate::services::card::CardLiveness::Live, + }; + + let agent_effective_surface = if liveness.is_live() { + agent_effective_surface_from_component_metadata( + &new_metadata, + &self.owned_agent_id, + &agent_id, + ) + } else { + golem_common::model::card::EffectiveSurface::default() + }; + + Some((updated_agent_config, agent_effective_surface)) + } else { + None + }; + + { + let mut current_files = self.state.files.write().await; + update_filesystem( + &mut current_files, + &self.state.file_loader, + self.owned_agent_id.environment_id, + self.worker_dir.path(), + new_agent_type_provision_configs + .as_ref() + .map(|c| c.files.as_slice()) + .unwrap_or_default(), + ) + .await?; + + let mut read_only_paths = self.state.read_only_paths.write().unwrap(); + *read_only_paths = compute_read_only_paths(¤t_files); + } + + if let Some((updated_agent_config, agent_effective_surface)) = updated_agent_state { self.state.agent_config = updated_agent_config; self.state.cached_agent_config_retry_policies = None; + self.state.agent_effective_surface = agent_effective_surface; }; self.state.component_metadata = new_metadata; @@ -4481,6 +4512,7 @@ struct PrivateDurableWorkerState { read_only_method_name: Option, component_metadata: Component, + agent_effective_surface: golem_common::model::card::EffectiveSurface, total_linear_memory_size: u64, /// Running total of storage bytes acquired from the executor semaphore pool @@ -4621,6 +4653,7 @@ impl PrivateDurableWorkerState { component_metadata: Component, total_linear_memory_size: u64, current_filesystem_storage_usage: u64, + agent_effective_surface: golem_common::model::card::EffectiveSurface, worker_fork: Arc, read_only_paths: RwLock>, files: TRwLock>, @@ -4693,6 +4726,7 @@ impl PrivateDurableWorkerState { invocation_strictness: InvocationStrictness::Normal, read_only_method_name: None, component_metadata, + agent_effective_surface, total_linear_memory_size, current_filesystem_storage_usage, replay_state, diff --git a/golem-worker-executor/src/model/mod.rs b/golem-worker-executor/src/model/mod.rs index 5c896f0b5b..96a42761ad 100644 --- a/golem-worker-executor/src/model/mod.rs +++ b/golem-worker-executor/src/model/mod.rs @@ -18,6 +18,7 @@ use futures::Stream; use futures::future::ready; use golem_common::model::account::{AccountEmail, AccountId}; use golem_common::model::agent::{AgentMode, AgentTypeName}; +use golem_common::model::card::EffectiveSurface; use golem_common::model::component::ComponentRevision; use golem_common::model::invocation_context::{ AttributeValue, InvocationContextSpan, InvocationContextStack, SpanId, TraceId, @@ -75,6 +76,7 @@ pub struct AgentConfig { pub created_by_email: AccountEmail, pub initial_agent_config: Vec, pub last_snapshot_index: Option, + pub agent_effective_surface: EffectiveSurface, } impl AgentConfig { @@ -87,6 +89,7 @@ impl AgentConfig { created_by_email: AccountEmail, initial_agent_config: Vec, last_snapshot_index: Option, + agent_effective_surface: EffectiveSurface, ) -> AgentConfig { AgentConfig { deleted_regions, @@ -97,6 +100,7 @@ impl AgentConfig { created_by_email, initial_agent_config, last_snapshot_index, + agent_effective_surface, } } diff --git a/golem-worker-executor/src/model/public_oplog/mod.rs b/golem-worker-executor/src/model/public_oplog/mod.rs index ea8a847947..67180effdf 100644 --- a/golem-worker-executor/src/model/public_oplog/mod.rs +++ b/golem-worker-executor/src/model/public_oplog/mod.rs @@ -33,20 +33,7 @@ use golem_common::model::oplog::public_oplog_entry::{ PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, - SuspendParams, -}; -use golem_common::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, - CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, - CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, - DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, - FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, - HostCallParams, InterruptedParams, JumpParams, LogParams, NoOpParams, - OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, - PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, - RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, + SuspendParams, CardRevokedParams, }; use golem_common::model::oplog::types::encode_span_data; use golem_common::model::oplog::{ diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index de181386c9..06336238f4 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -28,24 +28,7 @@ use golem_common::model::oplog::public_oplog_entry::{ RemoveRetryPolicyParams, RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, StringAttributeValue, SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, - WriteRemoteTransactionParameters, -}; -use golem_common::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, BeginRemoteWriteParams, - CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, - CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, - DropResourceParams, EndAtomicRegionParams, EndRemoteWriteParams, ErrorParams, ExitedParams, - FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, - HostCallParams, InterruptedParams, JumpParams, LogParams, ManualUpdateParameters, NoOpParams, - OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, - PluginInstallationDescription, PreCommitRemoteTransactionParams, - PreRollbackRemoteTransactionParams, PublicAgentInvocation, PublicAgentInvocationResult, - PublicAttributeValue, PublicDurableFunctionType, PublicSpanData, RemoveRetryPolicyParams, - RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartSpanParams, StringAttributeValue, - SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, - WriteRemoteTransactionParameters, + WriteRemoteTransactionParameters,CardRevokedParams }; use golem_common::model::oplog::{ AgentInvocationOutputParameters, AgentTerminatedByQuotaError, EphemeralCannotSuspendError, diff --git a/golem-worker-executor/src/services/active_workers/mod.rs b/golem-worker-executor/src/services/active_workers/mod.rs index 1a7ea2c22d..4e9a305384 100644 --- a/golem-worker-executor/src/services/active_workers/mod.rs +++ b/golem-worker-executor/src/services/active_workers/mod.rs @@ -41,11 +41,11 @@ use tokio_util::sync::CancellationToken; use tracing::{Instrument, debug}; -use crate::services::HasAll; use crate::services::golem_config::{ AgentStatusFlushConfig, FilesystemStorageConfig, MemoryConfig, }; use crate::services::resource_limits::AtomicResourceEntry; +use crate::services::{HasAll, HasCardService}; use crate::worker::Worker; use crate::worker::status_flusher::AgentStatusFlushQueue; use crate::workerctx::WorkerCtx; @@ -203,7 +203,7 @@ impl ActiveWorkers { principal: Principal, ) -> Result>, WorkerExecutorError> where - T: HasAll + Clone + Send + Sync + 'static, + T: HasAll + HasCardService + Clone + Send + Sync + 'static, { let agent_id = owned_agent_id.agent_id(); diff --git a/golem-worker-executor/src/services/card.rs b/golem-worker-executor/src/services/card.rs index e27906490a..c6c2603e1b 100644 --- a/golem-worker-executor/src/services/card.rs +++ b/golem-worker-executor/src/services/card.rs @@ -20,20 +20,36 @@ use golem_service_base::error::worker_executor::WorkerExecutorError; use std::collections::HashSet; use std::sync::{Arc, RwLock}; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CardLiveness { + Live, + Revoked { newly_detected: bool }, +} + +impl CardLiveness { + pub fn is_live(self) -> bool { + matches!(self, Self::Live) + } + + pub fn newly_detected_revocation(self) -> bool { + matches!( + self, + Self::Revoked { + newly_detected: true + } + ) + } +} + #[async_trait] pub trait CardService: Send + Sync { fn record_revoked_cards(&self, card_ids: &[CardId]); - fn is_card_revoked(&self, card_id: CardId) -> bool; - - async fn existing_cards( - &self, - card_ids: Vec, - ) -> Result, WorkerExecutorError>; + async fn check_card(&self, card_id: CardId) -> Result; } pub struct CardServiceDefault { - registry_service: Option>, + registry_service: Arc, revoked_cards: RwLock>, } @@ -43,52 +59,41 @@ pub struct NoopCardService; impl CardService for NoopCardService { fn record_revoked_cards(&self, _card_ids: &[CardId]) {} - fn is_card_revoked(&self, _card_id: CardId) -> bool { - false - } - - async fn existing_cards(&self, card_ids: Vec) -> Result, WorkerExecutorError> { - Ok(card_ids) + async fn check_card(&self, _card_id: CardId) -> Result { + Ok(CardLiveness::Live) } } impl CardServiceDefault { pub fn new(registry_service: Arc) -> Self { Self { - registry_service: Some(registry_service), + registry_service, revoked_cards: RwLock::new(HashSet::new()), } } - #[cfg(test)] - fn for_tests() -> Self { - Self { - registry_service: None, - revoked_cards: RwLock::new(HashSet::new()), - } + fn cache_revoked_cards(&self, card_ids: &[CardId]) { + let mut revoked_cards = self.revoked_cards.write().unwrap(); + revoked_cards.extend(card_ids.iter().copied()); } } #[async_trait] impl CardService for CardServiceDefault { fn record_revoked_cards(&self, card_ids: &[CardId]) { - let mut revoked_cards = self.revoked_cards.write().unwrap(); - revoked_cards.extend(card_ids.iter().copied()); + self.cache_revoked_cards(card_ids); } - fn is_card_revoked(&self, card_id: CardId) -> bool { - self.revoked_cards.read().unwrap().contains(&card_id) - } + async fn check_card(&self, card_id: CardId) -> Result { + if self.revoked_cards.read().unwrap().contains(&card_id) { + return Ok(CardLiveness::Revoked { + newly_detected: false, + }); + } - async fn existing_cards( - &self, - card_ids: Vec, - ) -> Result, WorkerExecutorError> { - let Some(registry_service) = &self.registry_service else { - return Ok(card_ids); - }; - let existing = registry_service - .batch_get_existing_cards(card_ids.clone()) + let existing = self + .registry_service + .batch_get_existing_cards(vec![card_id]) .await .map_err(|err| { WorkerExecutorError::runtime(format!( @@ -96,12 +101,15 @@ impl CardService for CardServiceDefault { err.to_safe_string() )) })?; - let missing = card_ids - .into_iter() - .filter(|card_id| !existing.contains(card_id)) - .collect::>(); - self.record_revoked_cards(&missing); - Ok(existing) + + if existing.contains(&card_id) { + Ok(CardLiveness::Live) + } else { + self.cache_revoked_cards(&[card_id]); + Ok(CardLiveness::Revoked { + newly_detected: true, + }) + } } } @@ -111,15 +119,14 @@ mod tests { use test_r::test; #[test] - fn records_revoked_card_ids() { - let service = CardServiceDefault::for_tests(); + fn noop_card_service_treats_cards_as_live() { + let service = NoopCardService; let revoked = CardId::new(); - let live = CardId::new(); - - assert!(!service.is_card_revoked(revoked)); - service.record_revoked_cards(&[revoked]); - assert!(service.is_card_revoked(revoked)); - assert!(!service.is_card_revoked(live)); + assert!( + futures::executor::block_on(service.check_card(revoked)) + .unwrap() + .is_live() + ); } } diff --git a/golem-worker-executor/src/services/mod.rs b/golem-worker-executor/src/services/mod.rs index 842a5aa01e..3433d2067c 100644 --- a/golem-worker-executor/src/services/mod.rs +++ b/golem-worker-executor/src/services/mod.rs @@ -469,7 +469,7 @@ impl All { Arc::downgrade(&self.leak_sentinel) } - pub fn from_other>(this: &T) -> All { + pub fn from_other + HasCardService>(this: &T) -> All { All::new( this.active_workers(), this.agent_types(), diff --git a/golem-worker-executor/src/worker/mod.rs b/golem-worker-executor/src/worker/mod.rs index 84da864425..a29c502575 100644 --- a/golem-worker-executor/src/worker/mod.rs +++ b/golem-worker-executor/src/worker/mod.rs @@ -24,7 +24,7 @@ use self::agent_config::{ ensure_required_agent_secrets_are_configured, parse_worker_creation_agent_config, }; use self::status::update_status_with_new_entries; -use crate::durable_host::recover_stderr_logs; +use crate::durable_host::{agent_effective_surface_from_component_metadata, recover_stderr_logs}; use crate::metrics::storage::record_filesystem_pool_released; use crate::model::{AgentConfig, ExecutionStatus, LookupResult, ReadFileResult, TrapType}; use crate::services::active_workers::{ @@ -321,7 +321,7 @@ impl Worker { principal: Principal, ) -> Result, WorkerExecutorError> where - T: HasAll + Clone + Send + Sync + 'static, + T: HasAll + HasCardService + Clone + Send + Sync + 'static, { deps.active_workers() .get_or_add( @@ -349,7 +349,7 @@ impl Worker { principal: Principal, ) -> Result, WorkerExecutorError> where - T: HasAll + Send + Sync + Clone + 'static, + T: HasAll + HasCardService + Send + Sync + Clone + 'static, { let worker = Self::get_or_create_suspended( deps, @@ -396,7 +396,7 @@ impl Worker { } } - pub async fn new>( + pub async fn new + HasCardService>( deps: &T, owned_agent_id: OwnedAgentId, worker_env: Option>, @@ -3645,38 +3645,45 @@ impl RunningWorker { .current_component .store(Arc::new(component_metadata.clone())); - let card_service = parent.card_service(); - card_service.record_revoked_cards( - &worker_metadata - .last_known_status - .revoked_cards - .iter() - .copied() - .collect::>(), - ); - - if let Some(card_id) = parent.parsed_agent_id.as_ref().and_then(|agent_id| { + let initial_agent_card_id = parent.parsed_agent_id.as_ref().and_then(|agent_id| { component_metadata .metadata .agent_type_initial_permission_template(&agent_id.agent_type) .map(|template| template.card_id) - }) { - let exists = !card_service.is_card_revoked(card_id) - && card_service - .existing_cards(vec![card_id]) - .await? - .contains(&card_id); - if !exists - && !worker_metadata + }); + let initial_agent_card_liveness = match initial_agent_card_id { + Some(card_id) + if worker_metadata .last_known_status .revoked_cards - .contains(&card_id) + .contains(&card_id) => { - card_service.record_revoked_cards(&[card_id]); - parent - .add_and_commit_oplog(OplogEntry::card_revoked(card_id.0)) - .await; + crate::services::card::CardLiveness::Revoked { + newly_detected: false, + } } + Some(card_id) => parent.card_service().check_card(card_id).await?, + None => crate::services::card::CardLiveness::Live, + }; + let agent_effective_surface = match ( + &parent.parsed_agent_id, + initial_agent_card_liveness.is_live(), + ) { + (Some(agent_id), true) => agent_effective_surface_from_component_metadata( + &component_metadata, + &parent.owned_agent_id, + agent_id, + ), + (Some(_), false) => golem_common::model::card::EffectiveSurface::default(), + (None, _) => golem_common::model::card::EffectiveSurface::default(), + }; + + if let Some(card_id) = initial_agent_card_id + && initial_agent_card_liveness.newly_detected_revocation() + { + parent + .add_and_commit_oplog(OplogEntry::card_revoked(card_id.0)) + .await; } let component_version_for_replay = worker_metadata @@ -3750,6 +3757,7 @@ impl RunningWorker { worker_metadata.created_by_email, worker_metadata.config, last_snapshot_index, + agent_effective_surface, ), parent.execution_status.clone(), parent.file_loader(), From 6aca005cee4547937496edbe67f1d1dd02be1097 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 22:42:15 +0200 Subject: [PATCH 05/13] update --- golem-worker-executor/src/durable_host/mod.rs | 11 ++- golem-worker-executor/src/services/card.rs | 79 ++++++++++++++----- golem-worker-executor/src/worker/mod.rs | 10 ++- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/golem-worker-executor/src/durable_host/mod.rs b/golem-worker-executor/src/durable_host/mod.rs index e30bcab67f..c44e32ba79 100644 --- a/golem-worker-executor/src/durable_host/mod.rs +++ b/golem-worker-executor/src/durable_host/mod.rs @@ -2547,7 +2547,16 @@ impl DurableWorkerCtx { .map(|template| template.card_id) { Some(card_id) => { - let liveness = self.state.card_service.check_card(card_id).await?; + let liveness = self + .state + .card_service + .check_cards(vec![card_id]) + .await? + .get(&card_id) + .copied() + .unwrap_or(crate::services::card::CardLiveness::Revoked { + newly_detected: true, + }); if liveness.newly_detected_revocation() { self.public_state .worker() diff --git a/golem-worker-executor/src/services/card.rs b/golem-worker-executor/src/services/card.rs index c6c2603e1b..32293ab039 100644 --- a/golem-worker-executor/src/services/card.rs +++ b/golem-worker-executor/src/services/card.rs @@ -17,7 +17,7 @@ use golem_common::SafeDisplay; use golem_common::model::card::CardId; use golem_service_base::clients::registry::RegistryService; use golem_service_base::error::worker_executor::WorkerExecutorError; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -45,7 +45,10 @@ impl CardLiveness { pub trait CardService: Send + Sync { fn record_revoked_cards(&self, card_ids: &[CardId]); - async fn check_card(&self, card_id: CardId) -> Result; + async fn check_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError>; } pub struct CardServiceDefault { @@ -59,8 +62,14 @@ pub struct NoopCardService; impl CardService for NoopCardService { fn record_revoked_cards(&self, _card_ids: &[CardId]) {} - async fn check_card(&self, _card_id: CardId) -> Result { - Ok(CardLiveness::Live) + async fn check_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError> { + Ok(card_ids + .into_iter() + .map(|card_id| (card_id, CardLiveness::Live)) + .collect()) } } @@ -84,16 +93,34 @@ impl CardService for CardServiceDefault { self.cache_revoked_cards(card_ids); } - async fn check_card(&self, card_id: CardId) -> Result { - if self.revoked_cards.read().unwrap().contains(&card_id) { - return Ok(CardLiveness::Revoked { - newly_detected: false, - }); + async fn check_cards( + &self, + card_ids: Vec, + ) -> Result, WorkerExecutorError> { + let revoked_cards = self.revoked_cards.read().unwrap().clone(); + let mut result = HashMap::with_capacity(card_ids.len()); + let mut needs_registry_lookup = Vec::new(); + + for card_id in card_ids { + if revoked_cards.contains(&card_id) { + result.insert( + card_id, + CardLiveness::Revoked { + newly_detected: false, + }, + ); + } else if !result.contains_key(&card_id) { + needs_registry_lookup.push(card_id); + } + } + + if needs_registry_lookup.is_empty() { + return Ok(result); } let existing = self .registry_service - .batch_get_existing_cards(vec![card_id]) + .batch_get_existing_cards(needs_registry_lookup.clone()) .await .map_err(|err| { WorkerExecutorError::runtime(format!( @@ -101,15 +128,26 @@ impl CardService for CardServiceDefault { err.to_safe_string() )) })?; - - if existing.contains(&card_id) { - Ok(CardLiveness::Live) - } else { - self.cache_revoked_cards(&[card_id]); - Ok(CardLiveness::Revoked { - newly_detected: true, - }) + let existing = existing.into_iter().collect::>(); + let missing = needs_registry_lookup + .iter() + .copied() + .filter(|card_id| !existing.contains(card_id)) + .collect::>(); + self.cache_revoked_cards(&missing); + + for card_id in needs_registry_lookup { + let liveness = if existing.contains(&card_id) { + CardLiveness::Live + } else { + CardLiveness::Revoked { + newly_detected: true, + } + }; + result.insert(card_id, liveness); } + + Ok(result) } } @@ -124,7 +162,10 @@ mod tests { let revoked = CardId::new(); assert!( - futures::executor::block_on(service.check_card(revoked)) + futures::executor::block_on(service.check_cards(vec![revoked])) + .unwrap() + .get(&revoked) + .copied() .unwrap() .is_live() ); diff --git a/golem-worker-executor/src/worker/mod.rs b/golem-worker-executor/src/worker/mod.rs index a29c502575..2a6b97ddfb 100644 --- a/golem-worker-executor/src/worker/mod.rs +++ b/golem-worker-executor/src/worker/mod.rs @@ -3662,7 +3662,15 @@ impl RunningWorker { newly_detected: false, } } - Some(card_id) => parent.card_service().check_card(card_id).await?, + Some(card_id) => parent + .card_service() + .check_cards(vec![card_id]) + .await? + .get(&card_id) + .copied() + .unwrap_or(crate::services::card::CardLiveness::Revoked { + newly_detected: true, + }), None => crate::services::card::CardLiveness::Live, }; let agent_effective_surface = match ( From 7b21bab680a3ba2f62835d88aead4c00da0a3600 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 11 Jun 2026 23:11:49 +0200 Subject: [PATCH 06/13] regenerate openapi --- openapi/golem-service.yaml | 27 +++++++++++++++++++++++++++ openapi/golem-worker-service.yaml | 27 +++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/openapi/golem-service.yaml b/openapi/golem-service.yaml index 67aedd871f..15cce64c19 100644 --- a/openapi/golem-service.yaml +++ b/openapi/golem-service.yaml @@ -12041,6 +12041,7 @@ components: OplogProcessorCheckpoint: '#/components/schemas/PublicOplogEntry_r#OplogProcessorCheckpointParams' SetRetryPolicy: '#/components/schemas/PublicOplogEntry_r#SetRetryPolicyParams' RemoveRetryPolicy: '#/components/schemas/PublicOplogEntry_r#RemoveRetryPolicyParams' + CardRevoked: '#/components/schemas/PublicOplogEntry_r#CardRevokedParams' type: object oneOf: - $ref: '#/components/schemas/PublicOplogEntry_r#CreateParams' @@ -12084,6 +12085,7 @@ components: - $ref: '#/components/schemas/PublicOplogEntry_r#OplogProcessorCheckpointParams' - $ref: '#/components/schemas/PublicOplogEntry_r#SetRetryPolicyParams' - $ref: '#/components/schemas/PublicOplogEntry_r#RemoveRetryPolicyParams' + - $ref: '#/components/schemas/PublicOplogEntry_r#CardRevokedParams' PublicOplogEntryWithIndex: title: PublicOplogEntryWithIndex type: object @@ -12180,6 +12182,18 @@ components: required: - type - $ref: '#/components/schemas/r#CancelledParams' + PublicOplogEntry_r#CardRevokedParams: + allOf: + - type: object + properties: + type: + example: CardRevoked + type: string + enum: + - CardRevoked + required: + - type + - $ref: '#/components/schemas/r#CardRevokedParams' PublicOplogEntry_r#ChangePersistenceLevelParams: allOf: - type: object @@ -14109,6 +14123,19 @@ components: required: - timestamp - startIndex + r#CardRevokedParams: + title: r#CardRevokedParams + type: object + properties: + timestamp: + type: string + format: date-time + cardId: + type: string + format: uuid + required: + - timestamp + - cardId r#ChangePersistenceLevelParams: title: r#ChangePersistenceLevelParams type: object diff --git a/openapi/golem-worker-service.yaml b/openapi/golem-worker-service.yaml index c28bf2a154..d59cb1ad47 100644 --- a/openapi/golem-worker-service.yaml +++ b/openapi/golem-worker-service.yaml @@ -4114,6 +4114,7 @@ components: - $ref: '#/components/schemas/PublicOplogEntry_r#OplogProcessorCheckpointParams' - $ref: '#/components/schemas/PublicOplogEntry_r#SetRetryPolicyParams' - $ref: '#/components/schemas/PublicOplogEntry_r#RemoveRetryPolicyParams' + - $ref: '#/components/schemas/PublicOplogEntry_r#CardRevokedParams' discriminator: propertyName: type mapping: @@ -4158,6 +4159,7 @@ components: OplogProcessorCheckpoint: '#/components/schemas/PublicOplogEntry_r#OplogProcessorCheckpointParams' SetRetryPolicy: '#/components/schemas/PublicOplogEntry_r#SetRetryPolicyParams' RemoveRetryPolicy: '#/components/schemas/PublicOplogEntry_r#RemoveRetryPolicyParams' + CardRevoked: '#/components/schemas/PublicOplogEntry_r#CardRevokedParams' PublicOplogEntryWithIndex: type: object title: PublicOplogEntryWithIndex @@ -4254,6 +4256,18 @@ components: - Cancelled example: Cancelled - $ref: '#/components/schemas/r#CancelledParams' + PublicOplogEntry_r#CardRevokedParams: + allOf: + - type: object + required: + - type + properties: + type: + type: string + enum: + - CardRevoked + example: CardRevoked + - $ref: '#/components/schemas/r#CardRevokedParams' PublicOplogEntry_r#ChangePersistenceLevelParams: allOf: - type: object @@ -6183,6 +6197,19 @@ components: format: uint64 partial: $ref: '#/components/schemas/TypedSchemaValue' + r#CardRevokedParams: + type: object + title: r#CardRevokedParams + required: + - timestamp + - cardId + properties: + timestamp: + type: string + format: date-time + cardId: + type: string + format: uuid r#ChangePersistenceLevelParams: type: object title: r#ChangePersistenceLevelParams From f0b81196d9fce21fbc5cf3bbb7324471110dfa08 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Fri, 12 Jun 2026 10:46:06 +0200 Subject: [PATCH 07/13] cleanup wip --- .../wit/deps/golem-1.x/golem-oplog.wit | 14 ++- golem-common/src/base_model/oplog/mod.rs | 2 +- .../wit/deps/golem-1.x/golem-oplog.wit | 14 ++- golem-registry-service/src/repo/model/card.rs | 99 ++++++++++++++++++- golem-registry-service/src/services/card.rs | 14 +-- .../src/services/component/write.rs | 10 +- .../src/model/public_oplog/wit.rs | 10 +- .../wit/deps/golem-1.x/golem-oplog.wit | 14 ++- .../wit/deps/golem-1.x/golem-oplog.wit | 14 ++- sdks/scala/wit/deps/golem-1.x/golem-oplog.wit | 14 ++- sdks/ts/wit/deps/golem-1.x/golem-oplog.wit | 14 ++- wit/deps/golem-1.x/golem-oplog.wit | 14 ++- 12 files changed, 203 insertions(+), 30 deletions(-) diff --git a/cli/golem-cli/wit/deps/golem-1.x/golem-oplog.wit b/cli/golem-cli/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/cli/golem-cli/wit/deps/golem-1.x/golem-oplog.wit +++ b/cli/golem-cli/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/golem-common/src/base_model/oplog/mod.rs b/golem-common/src/base_model/oplog/mod.rs index df41438321..0d91d818f9 100644 --- a/golem-common/src/base_model/oplog/mod.rs +++ b/golem-common/src/base_model/oplog/mod.rs @@ -668,7 +668,7 @@ oplog_entry! { name: String, } }, - /// Records that a permission card used by the worker has been revoked. + /// Records that a permission card used by the agent has been revoked. CardRevoked { hint: true wit_raw_type: "card-revoked-parameters" diff --git a/golem-common/wit/deps/golem-1.x/golem-oplog.wit b/golem-common/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/golem-common/wit/deps/golem-1.x/golem-oplog.wit +++ b/golem-common/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/golem-registry-service/src/repo/model/card.rs b/golem-registry-service/src/repo/model/card.rs index 45095264fb..9fd9b96b6e 100644 --- a/golem-registry-service/src/repo/model/card.rs +++ b/golem-registry-service/src/repo/model/card.rs @@ -14,7 +14,9 @@ use chrono::{DateTime, Utc}; use golem_common::error_forwarding; -use golem_common::model::card::{Card, CardId, CardManagedBy, PermissionPattern}; +use golem_common::model::card::{ + Card, CardId, CardManagedBy, PermissionPattern, PolymorphicCard, PolymorphicPermissionPattern, +}; use golem_service_base::repo::{Blob, RepoError, SqlDateTime}; use sqlx::FromRow; use uuid::Uuid; @@ -40,6 +42,10 @@ pub struct CardDataRecord { pub lower_negative: Vec, pub upper_positive: Vec, pub upper_negative: Vec, + pub polymorphic_lower_positive: Vec, + pub polymorphic_lower_negative: Vec, + pub polymorphic_upper_positive: Vec, + pub polymorphic_upper_negative: Vec, } #[derive(FromRow, Debug, Clone, PartialEq)] @@ -73,6 +79,26 @@ impl TryFrom for Card { } } +impl TryFrom for PolymorphicCard { + type Error = RepoError; + + fn try_from(value: CardRecord) -> Result { + let data = value.data.into_value(); + + Ok(PolymorphicCard { + card_id: CardId(value.card_id), + parent_ids: data.parent_ids.into_iter().map(CardId).collect(), + lower_positive: data.polymorphic_lower_positive, + lower_negative: data.polymorphic_lower_negative, + upper_positive: data.polymorphic_upper_positive, + upper_negative: data.polymorphic_upper_negative, + created_at: value.created_at.into(), + expires_at: value.expires_at.map(Into::into), + system_card: value.system_card, + }) + } +} + impl CardRecord { pub fn creation( card_id: CardId, @@ -93,6 +119,41 @@ impl CardRecord { lower_negative, upper_positive, upper_negative, + polymorphic_lower_positive: Vec::new(), + polymorphic_lower_negative: Vec::new(), + polymorphic_upper_positive: Vec::new(), + polymorphic_upper_negative: Vec::new(), + }), + created_at: SqlDateTime::now(), + expires_at: expires_at.map(Into::into), + system_card, + managed_by: managed_by.map(Blob::new), + } + } + + pub fn polymorphic_creation( + card_id: CardId, + parent_ids: Vec, + lower_positive: Vec, + lower_negative: Vec, + upper_positive: Vec, + upper_negative: Vec, + expires_at: Option>, + system_card: bool, + managed_by: Option, + ) -> Self { + Self { + card_id: card_id.0, + data: Blob::new(CardDataRecord { + parent_ids: parent_ids.into_iter().map(|id| id.0).collect(), + lower_positive: Vec::new(), + lower_negative: Vec::new(), + upper_positive: Vec::new(), + upper_negative: Vec::new(), + polymorphic_lower_positive: lower_positive, + polymorphic_lower_negative: lower_negative, + polymorphic_upper_positive: upper_positive, + polymorphic_upper_negative: upper_negative, }), created_at: SqlDateTime::now(), expires_at: expires_at.map(Into::into), @@ -101,3 +162,39 @@ impl CardRecord { } } } + +#[cfg(test)] +mod tests { + use super::*; + use golem_common::model::component::ComponentName; + use golem_common::model::component_metadata::AgentInitialPermissionTemplate; + use golem_common::model::environment::EnvironmentName; + use test_r::test; + + #[test] + fn polymorphic_creation_persists_agent_initial_template_grants() { + let template = AgentInitialPermissionTemplate::default_for( + &EnvironmentName::try_from("prod").unwrap(), + &ComponentName("cart-svc".to_string()), + ); + + let record = CardRecord::polymorphic_creation( + template.card_id, + Vec::new(), + template.lower_positive.clone(), + template.lower_negative.clone(), + template.upper_positive.clone(), + template.upper_negative.clone(), + None, + false, + None, + ); + let card = PolymorphicCard::try_from(record).unwrap(); + + assert_eq!(card.card_id, template.card_id); + assert_eq!(card.lower_positive, template.lower_positive); + assert_eq!(card.lower_negative, template.lower_negative); + assert_eq!(card.upper_positive, template.upper_positive); + assert_eq!(card.upper_negative, template.upper_negative); + } +} diff --git a/golem-registry-service/src/services/card.rs b/golem-registry-service/src/services/card.rs index 59680e9741..8a70d18ddb 100644 --- a/golem-registry-service/src/services/card.rs +++ b/golem-registry-service/src/services/card.rs @@ -17,6 +17,7 @@ use crate::repo::model::card::{CardRecord, CardRepoError}; use golem_common::model::agent::AgentTypeName; use golem_common::model::card::{CardId, CardManagedBy}; use golem_common::model::component::{ComponentId, ComponentRevision}; +use golem_common::model::component_metadata::AgentInitialPermissionTemplate; use std::sync::Arc; pub struct CardService { @@ -33,16 +34,17 @@ impl CardService { component_id: ComponentId, component_revision: ComponentRevision, agent_type: AgentTypeName, + template: &AgentInitialPermissionTemplate, ) -> Result { - let card_id = CardId::new(); + let card_id = template.card_id; self.card_repo - .create(CardRecord::creation( + .create(CardRecord::polymorphic_creation( card_id, Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), - Vec::new(), + template.lower_positive.clone(), + template.lower_negative.clone(), + template.upper_positive.clone(), + template.upper_negative.clone(), None, false, Some(CardManagedBy::AgentInitial { diff --git a/golem-registry-service/src/services/component/write.rs b/golem-registry-service/src/services/component/write.rs index 0ed855bd92..19e3e16d4e 100644 --- a/golem-registry-service/src/services/component/write.rs +++ b/golem-registry-service/src/services/component/write.rs @@ -112,18 +112,16 @@ impl ComponentWriteService { let mut result = BTreeMap::new(); for agent_type in agent_types { - let card_id = self - .card_service + let template = + AgentInitialPermissionTemplate::default_for(environment_name, component_name); + self.card_service .create_agent_initial_card( component_id, component_revision, agent_type.type_name.clone(), + &template, ) .await?; - let template = AgentInitialPermissionTemplate { - card_id, - ..AgentInitialPermissionTemplate::default_for(environment_name, component_name) - }; result.insert(agent_type.type_name.clone(), template); } diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index 06336238f4..b8c773d23f 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -444,8 +444,10 @@ impl From for oplog::PublicOplogEntry { }) } PublicOplogEntry::CardRevoked(CardRevokedParams { timestamp, card_id }) => { - let _ = card_id; - Self::NoOp(timestamp.into()) + Self::CardRevoked(oplog::CardRevokedParameters { + timestamp: timestamp.into(), + card_id: card_id.into(), + }) } } } @@ -1191,6 +1193,10 @@ impl TryFrom for golem_common::model::oplog::OplogEntry { timestamp: timestamp_from_datetime(params.timestamp), name: params.name, }), + oplog::OplogEntry::CardRevoked(params) => Ok(Self::CardRevoked { + timestamp: timestamp_from_datetime(params.timestamp), + card_id: params.card_id.into(), + }), } } } diff --git a/sdks/moonbit/golem_sdk/wit/deps/golem-1.x/golem-oplog.wit b/sdks/moonbit/golem_sdk/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/sdks/moonbit/golem_sdk/wit/deps/golem-1.x/golem-oplog.wit +++ b/sdks/moonbit/golem_sdk/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/sdks/rust/golem-rust/wit/deps/golem-1.x/golem-oplog.wit b/sdks/rust/golem-rust/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/sdks/rust/golem-rust/wit/deps/golem-1.x/golem-oplog.wit +++ b/sdks/rust/golem-rust/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/sdks/scala/wit/deps/golem-1.x/golem-oplog.wit b/sdks/scala/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/sdks/scala/wit/deps/golem-1.x/golem-oplog.wit +++ b/sdks/scala/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/sdks/ts/wit/deps/golem-1.x/golem-oplog.wit b/sdks/ts/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/sdks/ts/wit/deps/golem-1.x/golem-oplog.wit +++ b/sdks/ts/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads diff --git a/wit/deps/golem-1.x/golem-oplog.wit b/wit/deps/golem-1.x/golem-oplog.wit index ee4a9cf0fe..7cd54815d3 100644 --- a/wit/deps/golem-1.x/golem-oplog.wit +++ b/wit/deps/golem-1.x/golem-oplog.wit @@ -204,6 +204,12 @@ interface oplog { name: string, } + /// Parameters for a card-revoked oplog entry. + record card-revoked-parameters { + timestamp: datetime, + card-id: uuid, + } + record end-atomic-region-parameters { timestamp: datetime, begin-index: oplog-index @@ -716,7 +722,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } variant public-oplog-entry { @@ -810,7 +818,9 @@ interface oplog { /// Sets or overwrites a named retry policy set-retry-policy(set-retry-policy-parameters), /// Removes a named retry policy by name - remove-retry-policy(remove-retry-policy-parameters) + remove-retry-policy(remove-retry-policy-parameters), + /// Records that a permission card used by the agent has been revoked + card-revoked(card-revoked-parameters) } /// Enriches raw oplog entries into public oplog entries by resolving oplog payloads From e6c36a1a5fa4d4216988bf4094a2f2164897ca0d Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Fri, 12 Jun 2026 11:03:30 +0200 Subject: [PATCH 08/13] store card as enum --- golem-common/src/base_model/card/types.rs | 52 +++++++ golem-registry-service/src/repo/card.rs | 23 ++- golem-registry-service/src/repo/model/card.rs | 145 ++++++++++-------- 3 files changed, 144 insertions(+), 76 deletions(-) diff --git a/golem-common/src/base_model/card/types.rs b/golem-common/src/base_model/card/types.rs index e3ae6feada..a15b2d7119 100644 --- a/golem-common/src/base_model/card/types.rs +++ b/golem-common/src/base_model/card/types.rs @@ -63,6 +63,7 @@ pub struct Card { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))] pub struct PolymorphicCard { pub card_id: CardId, pub parent_ids: Vec, @@ -75,6 +76,57 @@ pub struct PolymorphicCard { pub system_card: bool, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(desert_rust::BinaryCodec))] +pub enum StoredCard { + Concrete(Card), + Polymorphic(PolymorphicCard), +} + +impl StoredCard { + pub fn card_id(&self) -> CardId { + match self { + Self::Concrete(card) => card.card_id, + Self::Polymorphic(card) => card.card_id, + } + } + + pub fn parent_ids(&self) -> &[CardId] { + match self { + Self::Concrete(card) => &card.parent_ids, + Self::Polymorphic(card) => &card.parent_ids, + } + } + + pub fn expires_at(&self) -> Option> { + match self { + Self::Concrete(card) => card.expires_at, + Self::Polymorphic(card) => card.expires_at, + } + } + + pub fn system_card(&self) -> bool { + match self { + Self::Concrete(card) => card.system_card, + Self::Polymorphic(card) => card.system_card, + } + } + + pub fn into_concrete(self) -> Result { + match self { + Self::Concrete(card) => Ok(card), + other => Err(other), + } + } + + pub fn into_polymorphic(self) -> Result { + match self { + Self::Polymorphic(card) => Ok(card), + other => Err(other), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PolymorphicManifestCard { pub card_id: CardId, diff --git a/golem-registry-service/src/repo/card.rs b/golem-registry-service/src/repo/card.rs index 3e53795576..fd03bd55af 100644 --- a/golem-registry-service/src/repo/card.rs +++ b/golem-registry-service/src/repo/card.rs @@ -120,16 +120,16 @@ impl DbCardRepo { async fn insert_parent_links( tx: &mut PoolLabelledTransaction, card_id: Uuid, - parent_ids: &[Uuid], + parent_ids: &[CardId], ) -> Result<(), CardRepoError> { for parent_id in parent_ids { tx.execute( sqlx::query("INSERT INTO card_parents (card_id, parent_id) VALUES ($1, $2)") .bind(card_id) - .bind(parent_id), + .bind(parent_id.0), ) .await - .to_error_on_foreign_key_violation(CardRepoError::ParentNotFound(*parent_id))?; + .to_error_on_foreign_key_violation(CardRepoError::ParentNotFound(parent_id.0))?; } Ok(()) @@ -248,7 +248,7 @@ impl DbCardRepo { tx: &mut PoolLabelledTransaction, record: CardRecord, ) -> Result { - Self::lock_parent_cards_for_create(tx, record.data.value().parent_ids.as_slice()).await?; + Self::lock_parent_cards_for_create(tx, record.data.value().parent_ids()).await?; let inserted: CardRecord = tx .fetch_one_as( @@ -267,12 +267,7 @@ impl DbCardRepo { ) .await?; - Self::insert_parent_links( - tx, - inserted.card_id, - inserted.data.value().parent_ids.as_slice(), - ) - .await?; + Self::insert_parent_links(tx, inserted.card_id, inserted.data.value().parent_ids()).await?; Ok(inserted) } @@ -281,18 +276,18 @@ impl DbCardRepo { impl DbCardRepo { async fn lock_parent_cards_for_create( tx: &mut PoolLabelledTransaction, - parent_ids: &[Uuid], + parent_ids: &[CardId], ) -> Result<(), CardRepoError> { for parent_id in parent_ids { let row = tx .fetch_optional( sqlx::query("SELECT card_id FROM cards WHERE card_id = $1 FOR UPDATE") - .bind(*parent_id), + .bind(parent_id.0), ) .await?; if row.is_none() { - return Err(CardRepoError::ParentNotFound(*parent_id)); + return Err(CardRepoError::ParentNotFound(parent_id.0)); } } @@ -303,7 +298,7 @@ impl DbCardRepo { impl DbCardRepo { async fn lock_parent_cards_for_create( _tx: &mut PoolLabelledTransaction, - _parent_ids: &[Uuid], + _parent_ids: &[CardId], ) -> RepoResult<()> { // SQLite serializes write transactions, so there is no separate row-locking // primitive to use here. Missing parents are rejected by the card_parents FK. diff --git a/golem-registry-service/src/repo/model/card.rs b/golem-registry-service/src/repo/model/card.rs index 9fd9b96b6e..197d8ceb67 100644 --- a/golem-registry-service/src/repo/model/card.rs +++ b/golem-registry-service/src/repo/model/card.rs @@ -12,10 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. +use anyhow::anyhow; use chrono::{DateTime, Utc}; use golem_common::error_forwarding; use golem_common::model::card::{ Card, CardId, CardManagedBy, PermissionPattern, PolymorphicCard, PolymorphicPermissionPattern, + StoredCard, }; use golem_service_base::repo::{Blob, RepoError, SqlDateTime}; use sqlx::FromRow; @@ -37,15 +39,13 @@ error_forwarding!(CardRepoError, RepoError); #[derive(Debug, Clone, PartialEq, Eq, desert_rust::BinaryCodec)] pub struct CardDataRecord { - pub parent_ids: Vec, - pub lower_positive: Vec, - pub lower_negative: Vec, - pub upper_positive: Vec, - pub upper_negative: Vec, - pub polymorphic_lower_positive: Vec, - pub polymorphic_lower_negative: Vec, - pub polymorphic_upper_positive: Vec, - pub polymorphic_upper_negative: Vec, + pub card: StoredCard, +} + +impl CardDataRecord { + pub fn parent_ids(&self) -> &[CardId] { + self.card.parent_ids() + } } #[derive(FromRow, Debug, Clone, PartialEq)] @@ -62,19 +62,10 @@ impl TryFrom for Card { type Error = RepoError; fn try_from(value: CardRecord) -> Result { - let data = value.data.into_value(); - - Ok(Card { - card_id: CardId(value.card_id), - parent_ids: data.parent_ids.into_iter().map(CardId).collect(), - lower_positive: data.lower_positive, - lower_negative: data.lower_negative, - upper_positive: data.upper_positive, - upper_negative: data.upper_negative, - created_at: value.created_at.into(), - expires_at: value.expires_at.map(Into::into), - system_card: value.system_card, - managed_by: value.managed_by.map(Blob::into_value), + value.try_into_stored_card()?.into_concrete().map_err(|_| { + RepoError::InternalError(anyhow!( + "Cannot convert polymorphic card record to concrete card" + )) }) } } @@ -83,19 +74,22 @@ impl TryFrom for PolymorphicCard { type Error = RepoError; fn try_from(value: CardRecord) -> Result { - let data = value.data.into_value(); - - Ok(PolymorphicCard { - card_id: CardId(value.card_id), - parent_ids: data.parent_ids.into_iter().map(CardId).collect(), - lower_positive: data.polymorphic_lower_positive, - lower_negative: data.polymorphic_lower_negative, - upper_positive: data.polymorphic_upper_positive, - upper_negative: data.polymorphic_upper_negative, - created_at: value.created_at.into(), - expires_at: value.expires_at.map(Into::into), - system_card: value.system_card, - }) + value + .try_into_stored_card()? + .into_polymorphic() + .map_err(|_| { + RepoError::InternalError(anyhow!( + "Cannot convert concrete card record to polymorphic card" + )) + }) + } +} + +impl TryFrom for StoredCard { + type Error = RepoError; + + fn try_from(value: CardRecord) -> Result { + value.try_into_stored_card() } } @@ -111,23 +105,27 @@ impl CardRecord { system_card: bool, managed_by: Option, ) -> Self { + let card = Card { + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at: Utc::now(), + expires_at, + system_card, + managed_by, + }; Self { - card_id: card_id.0, + card_id: card.card_id.0, data: Blob::new(CardDataRecord { - parent_ids: parent_ids.into_iter().map(|id| id.0).collect(), - lower_positive, - lower_negative, - upper_positive, - upper_negative, - polymorphic_lower_positive: Vec::new(), - polymorphic_lower_negative: Vec::new(), - polymorphic_upper_positive: Vec::new(), - polymorphic_upper_negative: Vec::new(), + card: StoredCard::Concrete(card.clone()), }), - created_at: SqlDateTime::now(), - expires_at: expires_at.map(Into::into), - system_card, - managed_by: managed_by.map(Blob::new), + created_at: card.created_at.into(), + expires_at: card.expires_at.map(Into::into), + system_card: card.system_card, + managed_by: card.managed_by.map(Blob::new), } } @@ -142,25 +140,48 @@ impl CardRecord { system_card: bool, managed_by: Option, ) -> Self { + let card = PolymorphicCard { + card_id, + parent_ids, + lower_positive, + lower_negative, + upper_positive, + upper_negative, + created_at: Utc::now(), + expires_at, + system_card, + }; Self { - card_id: card_id.0, + card_id: card.card_id.0, data: Blob::new(CardDataRecord { - parent_ids: parent_ids.into_iter().map(|id| id.0).collect(), - lower_positive: Vec::new(), - lower_negative: Vec::new(), - upper_positive: Vec::new(), - upper_negative: Vec::new(), - polymorphic_lower_positive: lower_positive, - polymorphic_lower_negative: lower_negative, - polymorphic_upper_positive: upper_positive, - polymorphic_upper_negative: upper_negative, + card: StoredCard::Polymorphic(card.clone()), }), - created_at: SqlDateTime::now(), - expires_at: expires_at.map(Into::into), - system_card, + created_at: card.created_at.into(), + expires_at: card.expires_at.map(Into::into), + system_card: card.system_card, managed_by: managed_by.map(Blob::new), } } + + fn try_into_stored_card(self) -> Result { + let mut card = self.data.into_value().card; + match &mut card { + StoredCard::Concrete(card) => { + card.card_id = CardId(self.card_id); + card.created_at = self.created_at.into(); + card.expires_at = self.expires_at.map(Into::into); + card.system_card = self.system_card; + card.managed_by = self.managed_by.map(Blob::into_value); + } + StoredCard::Polymorphic(card) => { + card.card_id = CardId(self.card_id); + card.created_at = self.created_at.into(); + card.expires_at = self.expires_at.map(Into::into); + card.system_card = self.system_card; + } + } + Ok(card) + } } #[cfg(test)] From f6f2cc2d2bee540125bfc18cb89fbbce994d4514 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Fri, 12 Jun 2026 11:53:44 +0200 Subject: [PATCH 09/13] update dts --- sdks/scala/wit/dts/golem_api_1_5_0_oplog.d.ts | 17 +++++++++++++++++ .../types/golem_api_1_5_0_oplog.d.ts | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/sdks/scala/wit/dts/golem_api_1_5_0_oplog.d.ts b/sdks/scala/wit/dts/golem_api_1_5_0_oplog.d.ts index c74401347d..b74a935bc6 100644 --- a/sdks/scala/wit/dts/golem_api_1_5_0_oplog.d.ts +++ b/sdks/scala/wit/dts/golem_api_1_5_0_oplog.d.ts @@ -241,6 +241,13 @@ declare module 'golem:api/oplog@1.5.0' { timestamp: Datetime; name: string; }; + /** + * Parameters for a card-revoked oplog entry. + */ + export type CardRevokedParameters = { + timestamp: Datetime; + cardId: Uuid; + }; export type EndAtomicRegionParameters = { timestamp: Datetime; beginIndex: OplogIndex; @@ -925,6 +932,11 @@ declare module 'golem:api/oplog@1.5.0' { { tag: 'remove-retry-policy' val: RemoveRetryPolicyParameters + } | + /** Records that a permission card used by the agent has been revoked */ + { + tag: 'card-revoked' + val: CardRevokedParameters }; export type PublicOplogEntry = /** The initial agent oplog entry */ @@ -1152,6 +1164,11 @@ declare module 'golem:api/oplog@1.5.0' { { tag: 'remove-retry-policy' val: RemoveRetryPolicyParameters + } | + /** Records that a permission card used by the agent has been revoked */ + { + tag: 'card-revoked' + val: CardRevokedParameters }; export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; } diff --git a/sdks/ts/packages/golem-ts-sdk/types/golem_api_1_5_0_oplog.d.ts b/sdks/ts/packages/golem-ts-sdk/types/golem_api_1_5_0_oplog.d.ts index c74401347d..b74a935bc6 100644 --- a/sdks/ts/packages/golem-ts-sdk/types/golem_api_1_5_0_oplog.d.ts +++ b/sdks/ts/packages/golem-ts-sdk/types/golem_api_1_5_0_oplog.d.ts @@ -241,6 +241,13 @@ declare module 'golem:api/oplog@1.5.0' { timestamp: Datetime; name: string; }; + /** + * Parameters for a card-revoked oplog entry. + */ + export type CardRevokedParameters = { + timestamp: Datetime; + cardId: Uuid; + }; export type EndAtomicRegionParameters = { timestamp: Datetime; beginIndex: OplogIndex; @@ -925,6 +932,11 @@ declare module 'golem:api/oplog@1.5.0' { { tag: 'remove-retry-policy' val: RemoveRetryPolicyParameters + } | + /** Records that a permission card used by the agent has been revoked */ + { + tag: 'card-revoked' + val: CardRevokedParameters }; export type PublicOplogEntry = /** The initial agent oplog entry */ @@ -1152,6 +1164,11 @@ declare module 'golem:api/oplog@1.5.0' { { tag: 'remove-retry-policy' val: RemoveRetryPolicyParameters + } | + /** Records that a permission card used by the agent has been revoked */ + { + tag: 'card-revoked' + val: CardRevokedParameters }; export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; } From d697ff06b11444672c3330e078001b512d1463ca Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Wed, 17 Jun 2026 12:16:18 +0200 Subject: [PATCH 10/13] fix compilation --- golem-common/src/model/oplog/protobuf.rs | 13 +++++++++++++ .../src/model/public_oplog/mod.rs | 13 +++++++++++++ .../src/model/public_oplog/wit.rs | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/golem-common/src/model/oplog/protobuf.rs b/golem-common/src/model/oplog/protobuf.rs index 09804b882b..99a2e00319 100644 --- a/golem-common/src/model/oplog/protobuf.rs +++ b/golem-common/src/model/oplog/protobuf.rs @@ -48,6 +48,19 @@ use crate::model::oplog::public_oplog_entry::{ RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, CancelledParams }; +use crate::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, + CancelledParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + InterruptedParams, JumpParams, LogParams, NoOpParams, OplogProcessorCheckpointParams, + PendingAgentInvocationParams, PendingUpdateParams, PreCommitRemoteTransactionParams, + PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, RestartParams, RevertParams, + RolledBackRemoteTransactionParams, SetRetryPolicyParams, SetSpanAttributeParams, + SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, +}; use crate::model::oplog::{ AgentTerminatedByQuotaError, DurableFunctionType, EphemeralCannotSuspendError, EphemeralFuelExhaustedError, EphemeralSleepTooLongError, OplogEntry, PersistenceLevel, diff --git a/golem-worker-executor/src/model/public_oplog/mod.rs b/golem-worker-executor/src/model/public_oplog/mod.rs index 67180effdf..292428748b 100644 --- a/golem-worker-executor/src/model/public_oplog/mod.rs +++ b/golem-worker-executor/src/model/public_oplog/mod.rs @@ -35,6 +35,19 @@ use golem_common::model::oplog::public_oplog_entry::{ SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, CardRevokedParams, }; +use golem_common::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, + CancelledParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + InterruptedParams, JumpParams, LogParams, NoOpParams, OplogProcessorCheckpointParams, + PendingAgentInvocationParams, PendingUpdateParams, PreCommitRemoteTransactionParams, + PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, RestartParams, RevertParams, + RolledBackRemoteTransactionParams, SetRetryPolicyParams, SetSpanAttributeParams, + SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, +}; use golem_common::model::oplog::types::encode_span_data; use golem_common::model::oplog::{ AgentInitializationParameters, AgentInvocationOutputParameters, diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index b8c773d23f..e3ca37e056 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -30,6 +30,23 @@ use golem_common::model::oplog::public_oplog_entry::{ StringAttributeValue, SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, WriteRemoteTransactionParameters,CardRevokedParams }; +use golem_common::model::oplog::public_oplog_entry::{ + ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, + BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, + CancelledParams, CardRevokedParams, ChangePersistenceLevelParams, + CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, + DropResourceParams, EndAtomicRegionParams, EndParams, ErrorParams, ExitedParams, + FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, + InterruptedParams, JumpParams, LogParams, ManualUpdateParameters, NoOpParams, + OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, + PluginInstallationDescription, PreCommitRemoteTransactionParams, + PreRollbackRemoteTransactionParams, PublicAgentInvocation, PublicAgentInvocationResult, + PublicAttributeValue, PublicDurableFunctionType, PublicSpanData, RemoveRetryPolicyParams, + RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, + SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, StringAttributeValue, + SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, + WriteRemoteTransactionParameters, +}; use golem_common::model::oplog::{ AgentInvocationOutputParameters, AgentTerminatedByQuotaError, EphemeralCannotSuspendError, EphemeralFuelExhaustedError, EphemeralSleepTooLongError, FallibleResultParameters, From fdbfae104c721b6daf29972b3a2aa1db4d221cb2 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Thu, 18 Jun 2026 11:35:29 +0200 Subject: [PATCH 11/13] resolve conflicts --- golem-common/src/model/oplog/protobuf.rs | 13 ------------- .../src/model/public_oplog/mod.rs | 13 ------------- .../src/model/public_oplog/wit.rs | 16 ---------------- 3 files changed, 42 deletions(-) diff --git a/golem-common/src/model/oplog/protobuf.rs b/golem-common/src/model/oplog/protobuf.rs index 99a2e00319..4bb624946d 100644 --- a/golem-common/src/model/oplog/protobuf.rs +++ b/golem-common/src/model/oplog/protobuf.rs @@ -35,19 +35,6 @@ use crate::model::oplog::payload::OplogPayload; use crate::model::oplog::payload::host_functions::{ HostFunctionName, host_request_from_typed_schema_value, }; -use crate::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, - CancelPendingInvocationParams, CardRevokedParams, ChangePersistenceLevelParams, - CommittedRemoteTransactionParams, CreateParams, CreateResourceParams, DeactivatePluginParams, - DropResourceParams, EndAtomicRegionParams, EndParams, ErrorParams, ExitedParams, - FailedUpdateParams, FilesystemStorageUsageUpdateParams, FinishSpanParams, GrowMemoryParams, - StartParams, InterruptedParams, JumpParams, LogParams, NoOpParams, - OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, - PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, - RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartSpanParams, SuccessfulUpdateParams, SuspendParams, CancelledParams -}; use crate::model::oplog::public_oplog_entry::{ ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, diff --git a/golem-worker-executor/src/model/public_oplog/mod.rs b/golem-worker-executor/src/model/public_oplog/mod.rs index 292428748b..29c641f8d4 100644 --- a/golem-worker-executor/src/model/public_oplog/mod.rs +++ b/golem-worker-executor/src/model/public_oplog/mod.rs @@ -22,19 +22,6 @@ use golem_common::model::agent::{AgentMode, AgentTypeName, ParsedAgentId}; use golem_common::model::component::{ComponentRevision, InstalledPlugin}; use golem_common::model::invocation_context::InvocationContextStack; use golem_common::model::lucene::Query; -use golem_common::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, - CancelledParams, ChangePersistenceLevelParams, CommittedRemoteTransactionParams, CreateParams, - CreateResourceParams, DeactivatePluginParams, DropResourceParams, EndAtomicRegionParams, - EndParams, ErrorParams, ExitedParams, FailedUpdateParams, FilesystemStorageUsageUpdateParams, - FinishSpanParams, GrowMemoryParams, InterruptedParams, JumpParams, LogParams, NoOpParams, - OplogProcessorCheckpointParams, PendingAgentInvocationParams, PendingUpdateParams, - PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, RemoveRetryPolicyParams, - RestartParams, RevertParams, RolledBackRemoteTransactionParams, SetRetryPolicyParams, - SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, SuccessfulUpdateParams, - SuspendParams, CardRevokedParams, -}; use golem_common::model::oplog::public_oplog_entry::{ ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index e3ca37e056..52bf58a3e7 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -14,22 +14,6 @@ use crate::preview2::golem_api_1_x::oplog; use golem_common::model::environment::EnvironmentId; -use golem_common::model::oplog::public_oplog_entry::{ - ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, - BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, - CancelledParams, ChangePersistenceLevelParams, CommittedRemoteTransactionParams, CreateParams, - CreateResourceParams, DeactivatePluginParams, DropResourceParams, EndAtomicRegionParams, - EndParams, ErrorParams, ExitedParams, FailedUpdateParams, FilesystemStorageUsageUpdateParams, - FinishSpanParams, GrowMemoryParams, InterruptedParams, JumpParams, LogParams, - ManualUpdateParameters, NoOpParams, OplogProcessorCheckpointParams, - PendingAgentInvocationParams, PendingUpdateParams, PluginInstallationDescription, - PreCommitRemoteTransactionParams, PreRollbackRemoteTransactionParams, PublicAgentInvocation, - PublicAgentInvocationResult, PublicAttributeValue, PublicDurableFunctionType, PublicSpanData, - RemoveRetryPolicyParams, RestartParams, RevertParams, RolledBackRemoteTransactionParams, - SetRetryPolicyParams, SetSpanAttributeParams, SnapshotParams, StartParams, StartSpanParams, - StringAttributeValue, SuccessfulUpdateParams, SuspendParams, WriteRemoteBatchedParameters, - WriteRemoteTransactionParameters,CardRevokedParams -}; use golem_common::model::oplog::public_oplog_entry::{ ActivatePluginParams, AgentInvocationFinishedParams, AgentInvocationStartedParams, BeginAtomicRegionParams, BeginRemoteTransactionParams, CancelPendingInvocationParams, From d9c2598dc4ed576c1707bb5771665f295a4b4114 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Mon, 22 Jun 2026 18:52:34 +0200 Subject: [PATCH 12/13] fmt --- golem-registry-service/src/services/component/write.rs | 2 +- golem-worker-executor/src/durable_host/mod.rs | 2 +- golem-worker-executor/src/model/public_oplog/wit.rs | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/golem-registry-service/src/services/component/write.rs b/golem-registry-service/src/services/component/write.rs index 19e3e16d4e..2d6df11eac 100644 --- a/golem-registry-service/src/services/component/write.rs +++ b/golem-registry-service/src/services/component/write.rs @@ -105,7 +105,7 @@ impl ComponentWriteService { &self, component_id: ComponentId, component_revision: ComponentRevision, - agent_types: &[AgentType], + agent_types: &[AgentTypeSchema], environment_name: &EnvironmentName, component_name: &ComponentName, ) -> Result, ComponentError> { diff --git a/golem-worker-executor/src/durable_host/mod.rs b/golem-worker-executor/src/durable_host/mod.rs index c44e32ba79..7eb2ae51e9 100644 --- a/golem-worker-executor/src/durable_host/mod.rs +++ b/golem-worker-executor/src/durable_host/mod.rs @@ -261,7 +261,7 @@ pub enum InvocationStrictness { pub(crate) fn agent_effective_surface_from_component_metadata( component: &Component, owned_agent_id: &OwnedAgentId, - agent_id: &LegacyParsedAgentId, + agent_id: &ParsedAgentId, ) -> golem_common::model::card::EffectiveSurface { let template = component .metadata diff --git a/golem-worker-executor/src/model/public_oplog/wit.rs b/golem-worker-executor/src/model/public_oplog/wit.rs index 52bf58a3e7..478f457d38 100644 --- a/golem-worker-executor/src/model/public_oplog/wit.rs +++ b/golem-worker-executor/src/model/public_oplog/wit.rs @@ -1717,6 +1717,12 @@ impl TryFrom for oplog::OplogEntry { delta, }), ), + M::CardRevoked { timestamp, card_id } => { + Ok(Self::CardRevoked(oplog::CardRevokedParameters { + timestamp: timestamp.into(), + card_id: card_id.into(), + })) + } M::CreateResource { timestamp, id, From 2d07b8c866824bc356f2c1dbccb7d53ef72f1bb0 Mon Sep 17 00:00:00 2001 From: Maxim Schuwalow Date: Tue, 23 Jun 2026 00:45:41 +0200 Subject: [PATCH 13/13] rebuild plugin wasm --- plugins/otlp-exporter.wasm | Bin 481938 -> 482560 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/plugins/otlp-exporter.wasm b/plugins/otlp-exporter.wasm index 8e66fa64a69e426f3c4fa76be2116aa1c7e9cd85..1f630c3a1243bce94b82c964b114ff3513a9d5b7 100644 GIT binary patch delta 74070 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 OL(({|miZfu_`d=AoK{Q# delta 73824 zcmc${2Ygh;+6TNdb2fDs7E(wfIlCl~gcj)?PLyUr5IZWO;+04T-|Mv{bP$lT$U{>= zP8)k7dP0IBQGYMjMt`a3sn!)g^)&8ov#ZT}P5cPHd% z4wovj=753yyLam)9OY!WcNMP6Zs=rn3NO_D(pjr+*h89d#CiI>GVmXLdi4HB&Hml{ zz0!TaK)}jdw>A6p9oVB!?*Rb$FV|156BpIJfB!!HYd+uSh3?^V8Otp%i>;?Q?ofzB zCe`=1XpkrxXb$;$m?l9D{jWF3(;BgNXBNKK_;NYM{qM9(<;>w<+AcXwXo4q%RZ0|< z93m_%ObGnPxp3fDI2^)%x?R^eXBcBxSIUMdtjWHI%91;G7 z?d!YnW@QJ*pH{A%|EKmjf^p_gxwn(Q@4c1tHAfms;X>%#E6hLCsLB*5O;(Esl@MkD zkYD0t{mI%TlIkrhuS8Y;Y4)oU9`0bQoWw&@|K#h8`91g56TA6tf7|=gDxBNmh|YHy zJI|QclRx?YdEdR_OHqvmoeHa#?>Hh{&v$#SM^|)c&z^mrcP!8%Cp($LV9~pHr)Tee zTN}ZL@mcJs_N8`>4d>JNbp9Tn$+z--{3AY(FW@)$Eqv<6$rCk%( z#bE72zC-hCQ%qqa#a(u%b$+E#6wwp|;lE#%*8r}DMa+K<|K?I-O!?UMGhc2WCU zT-MHNKWOhd5A!qH2EN?+9vi8RW@Fe`_6~cSt#)o@``G8~AUni}?P5FFr|dBMj4g0} z#OARN*$TFt6|ym6DO;h*y{&NV#1_wbACXLgC5Vwc%*c7lD&PO}^ACcDiF_#plZ|CL|izwxvDJpYMb z^?TpOVc)5d6HwRf}y+WXopZMOD-Hb>j9 z9ncPHhqceOFSNPZhuTNlD(z!!wHDCUXlu1~+A?jqwnAH}&C@>7=4)SRUu(y-LhXb$ z%Xz?g!g<)a$~o7$(Rs>w$oaK%t#gJmf2Z?1=jTqp^AqPc&M%!`Ip;g~IKOosaR!{^ z-0eK+{KC23xx~5M`MvX)^SJY{7wr#PoMr#eSEr#s(sZqUBb-gh3? zPCI8iKXCq{4RwC#{Kz@axzPEJbFp)&bA@x6bEUJ;Io|oPbF*`kbE31zS?rw9*tx~I z)w#jB!@176%elt+sdJz6pz|~5ui7Z*56(^e19rxF);Th4RM^{Lqr=98jSY)hJvrvA zlRKzSLl*BZ?9hRYD&!Ad*Vq*QtM7te2(;c4Sg~^X3 zapZ3B7!*Tx;UiC^IV+vmQsBtbvkgAbm!6l&9o}Yo6=Ubr!qLxc5^*cH6d~1!*Bcl+ z&0CM&(myEwVgJhK2J$f*3NxSon6Y#IZ(c}@KPL;S>2P?fMpr?#1rDFn7=O<1?w-t! z`?I>IB3rxeIptNhEaC9h(pCD1?x`sXrsw3cHyR3uF7#Rr#S|B=PNV2}VR8308lSh# z|9ak|{F7~khw{9ff3eN~d(VgYm2HLh_j;f6`8WI*`n2W8Civ_3ZOd2RD158$FvdT< z;dl1$#82GtPaoLWKcRmD|Ko;#5g_l}^xx{=g)hG8?=qk%KXlVSWk45x;ik$~YhVH& zdQ067!tD%s+ljBbRd{jWsq*d{#flzDdR1fcmckQ5cd)nu8&t)ZX5J>~9YCT-3oDFp zGXCpk|Fh$&`|loEosTIh>^m|GL^ZQjLLs~CmpkE&O8%mE+WFmMqWHlgf8v;X`I#cT z+puuPn5)b^t4P%>UkViYXTQ_W%v8hQYFsKZ7Ct+!DQ74AGbSXnsfBAML~;Iev45+- zJHJxwuRf_V|GwCN@1)vQN%PM6c_B;(6f#ppLcI@pk(iwkE?@}pa6_5zWpRgt~e zadIMhF>f+@v2fJnFT#28rowLT59IupO@&|18phoI?FzqSFwfHMg*`rK&D@`wpvnN9 z-CnqU&UB59@IN;H5k6~2;nMll!@v+Ug5{B+m&|Sb2GSTHThDw?C*`w|f0*n1Lr*sLQ{8!#&J3abrTQXq9wCmTV

UOf=TCJT=^wSRDvOYUO%O#aXQ{|lulSQ1eix4vX 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