fix: remove config bundle restore dependency

This commit is contained in:
2026-08-19 04:23:37 +09:00
parent c97bde9ee0
commit b740b2d1e2
7 changed files with 331 additions and 70 deletions
+2 -1
View File
@@ -33,7 +33,8 @@ pub use manifest::{
};
pub use model_client::{ProviderError, build_client};
pub use prompt::catalog::{
CatalogError, EffectivePromptCatalog, PromptCatalog, WorkerPrompt, prompt_schema_source,
CatalogError, EffectivePromptCatalog, PromptCatalog, WorkerPrompt, WorkspacePromptProjection,
prompt_schema_source,
};
pub use prompt::source::PromptCatalogSource;
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
+97
View File
@@ -171,6 +171,77 @@ pub enum CatalogError {
DigestMismatch { expected: String, actual: String },
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkspacePromptProjection {
pub workspace_id: String,
pub config_revision: u64,
pub source_digest: String,
pub projection_digest: String,
pub schema_fingerprint: String,
pub toolchain_fingerprint: String,
pub catalog: EffectivePromptCatalog,
}
impl WorkspacePromptProjection {
pub fn new(
workspace_id: impl Into<String>,
source_digest: impl Into<String>,
projection_digest: impl Into<String>,
catalog: EffectivePromptCatalog,
) -> Result<Self, CatalogError> {
let workspace_id = workspace_id.into();
let source_digest = source_digest.into();
let projection_digest = projection_digest.into();
catalog.verify_digest()?;
if workspace_id.trim().is_empty() {
return Err(CatalogError::InvalidTemplateCatalog(
"Workspace Prompt projection workspace_id must not be empty".to_string(),
));
}
if source_digest.trim().is_empty() {
return Err(CatalogError::InvalidTemplateCatalog(
"Workspace Prompt projection source digest must not be empty".to_string(),
));
}
if projection_digest.trim().is_empty() {
return Err(CatalogError::InvalidTemplateCatalog(
"Workspace Prompt projection digest must not be empty".to_string(),
));
}
Ok(Self {
workspace_id,
config_revision: catalog.config_revision,
source_digest,
projection_digest,
schema_fingerprint: catalog.schema_fingerprint.clone(),
toolchain_fingerprint: catalog.toolchain_fingerprint.clone(),
catalog,
})
}
pub fn catalog(&self) -> &EffectivePromptCatalog {
&self.catalog
}
pub fn validate(&self) -> Result<(), CatalogError> {
let rebuilt = Self::new(
self.workspace_id.clone(),
self.source_digest.clone(),
self.projection_digest.clone(),
self.catalog.clone(),
)?;
if rebuilt.config_revision != self.config_revision
|| rebuilt.schema_fingerprint != self.schema_fingerprint
|| rebuilt.toolchain_fingerprint != self.toolchain_fingerprint
{
return Err(CatalogError::InvalidTemplateCatalog(
"Workspace Prompt projection metadata does not match its catalog".to_string(),
));
}
Ok(())
}
}
pub struct PromptCatalog {
env: Environment<'static>,
projection: EffectivePromptCatalog,
@@ -550,6 +621,32 @@ mod tests {
));
}
#[test]
fn workspace_prompt_projection_round_trips_and_rejects_tampered_metadata() {
let catalog = EffectivePromptCatalog::new(
BTreeMap::from([("default".to_string(), "PROMPT".to_string())]),
8,
"schema",
"toolchain",
)
.unwrap();
let projection = WorkspacePromptProjection::new(
"workspace-a",
"source-digest",
catalog.catalog_digest.clone(),
catalog,
)
.unwrap();
let serialized = serde_json::to_string(&projection).unwrap();
let restored: WorkspacePromptProjection = serde_json::from_str(&serialized).unwrap();
assert_eq!(restored, projection);
restored.validate().unwrap();
let mut tampered = restored;
tampered.config_revision += 1;
assert!(tampered.validate().is_err());
}
#[test]
fn catalog_source_preserves_workspace_projection_for_subworkers() {
let templates = BTreeMap::from([("template".to_string(), "OVERRIDE".to_string())]);