diff --git a/crates/worker-runtime/src/worker_backend.rs b/crates/worker-runtime/src/worker_backend.rs index cc53a20c..2fdcabe4 100644 --- a/crates/worker-runtime/src/worker_backend.rs +++ b/crates/worker-runtime/src/worker_backend.rs @@ -653,18 +653,22 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { }; if let Some(bundle) = request.config_bundle.as_ref() { if let Some(prompt_catalog) = bundle.prompt_catalog.clone() { - let source_digest = bundle - .metadata - .provenance - .detail - .as_deref() - .and_then(|detail| { - detail - .split(';') - .find_map(|part| part.strip_prefix("source_tree_digest=")) - }) - .unwrap_or(&prompt_catalog.catalog_digest) - .to_string(); + let source_digest = if prompt_catalog.source_digest.is_empty() { + bundle + .metadata + .provenance + .detail + .as_deref() + .and_then(|detail| { + detail + .split(';') + .find_map(|part| part.strip_prefix("source_tree_digest=")) + }) + .unwrap_or(&prompt_catalog.catalog_digest) + .to_string() + } else { + prompt_catalog.source_digest.clone() + }; let projection = worker::WorkspacePromptProjection::new( bundle.metadata.workspace_id.clone(), source_digest, diff --git a/crates/worker/src/prompt/catalog.rs b/crates/worker/src/prompt/catalog.rs index 28e748e9..9c5d187a 100644 --- a/crates/worker/src/prompt/catalog.rs +++ b/crates/worker/src/prompt/catalog.rs @@ -31,6 +31,8 @@ const BUILTIN_TOOLCHAIN_FINGERPRINT: &str = "builtin:prompts:decodal-0.4"; pub struct EffectivePromptCatalog { pub templates: BTreeMap, pub config_revision: u64, + #[serde(default, skip_serializing_if = "String::is_empty")] + pub source_digest: String, pub schema_fingerprint: String, pub toolchain_fingerprint: String, pub catalog_digest: String, @@ -48,6 +50,7 @@ impl EffectivePromptCatalog { Ok(Self { templates, config_revision, + source_digest: String::new(), schema_fingerprint: schema_fingerprint.into(), toolchain_fingerprint: toolchain_fingerprint.into(), catalog_digest, @@ -208,6 +211,11 @@ impl WorkspacePromptProjection { "Workspace Prompt projection digest must not be empty".to_string(), )); } + if !catalog.source_digest.is_empty() && catalog.source_digest != source_digest { + return Err(CatalogError::InvalidTemplateCatalog( + "Workspace Prompt projection source digest does not match its catalog".to_string(), + )); + } Ok(Self { workspace_id, config_revision: catalog.config_revision, diff --git a/crates/workspace-server/src/prompt_settings.rs b/crates/workspace-server/src/prompt_settings.rs index 376e53e1..81f1415b 100644 --- a/crates/workspace-server/src/prompt_settings.rs +++ b/crates/workspace-server/src/prompt_settings.rs @@ -215,13 +215,15 @@ pub fn project_prompts_from_workspace_config( "active Workspace config projection has no prompts namespace".to_string(), ) })?; - EffectivePromptCatalog::from_projection( + let mut catalog = EffectivePromptCatalog::from_projection( prompts, state.snapshot.revision, state.contract.schema_bundle.fingerprint.clone(), state.contract.fingerprint.clone(), ) - .map_err(|error| Error::RegistryInconsistency(error.to_string())) + .map_err(|error| Error::RegistryInconsistency(error.to_string()))?; + catalog.source_digest = state.snapshot.digest.clone(); + Ok(catalog) } pub fn project_workspace_prompt_projection(