server: project Workspace prompts into Worker config bundles
This commit is contained in:
@@ -3496,6 +3496,7 @@ fn builtin_profile_config_bundle(
|
||||
label: embedded_profile_label(profile),
|
||||
}],
|
||||
declarations: Vec::new(),
|
||||
prompt_catalog: None,
|
||||
profile_source_archive,
|
||||
profile_source_archive_handle,
|
||||
}
|
||||
@@ -4436,6 +4437,7 @@ mod tests {
|
||||
name: "read".to_string(),
|
||||
reference: "capability:read".to_string(),
|
||||
}],
|
||||
prompt_catalog: None,
|
||||
profile_source_archive: None,
|
||||
profile_source_archive_handle: None,
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub mod memory_backend;
|
||||
pub mod memory_staging;
|
||||
pub mod observation;
|
||||
pub mod profile_settings;
|
||||
pub mod prompt_settings;
|
||||
pub mod records;
|
||||
#[cfg(feature = "typescript")]
|
||||
pub use records::ticket_api_typescript;
|
||||
|
||||
@@ -254,10 +254,13 @@ pub fn build_virtual_profile_config_bundle(
|
||||
workspace_created_at: &str,
|
||||
selector: &str,
|
||||
) -> Result<Option<ConfigBundle>> {
|
||||
let Some(entry) = projection.entries.get(selector) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let archive = build_virtual_profile_archive(selector, entry, &projection.sources, state)?;
|
||||
let archive = projection
|
||||
.entries
|
||||
.get(selector)
|
||||
.map(|entry| build_virtual_profile_archive(selector, entry, &projection.sources, state))
|
||||
.transpose()?;
|
||||
let profile_selector = selector_for_builtin_candidate(selector)
|
||||
.unwrap_or_else(|| worker_runtime::catalog::ProfileSelector::Named(selector.to_string()));
|
||||
let bundle = ConfigBundle {
|
||||
metadata: ConfigBundleMetadata {
|
||||
id: format!("workspace-config-profile-r{}", state.snapshot.revision),
|
||||
@@ -274,11 +277,12 @@ pub fn build_virtual_profile_config_bundle(
|
||||
},
|
||||
},
|
||||
profiles: vec![ConfigProfileDescriptor {
|
||||
selector: worker_runtime::catalog::ProfileSelector::Named(selector.to_string()),
|
||||
selector: profile_selector,
|
||||
label: Some(selector.to_string()),
|
||||
}],
|
||||
declarations: Vec::new(),
|
||||
profile_source_archive: Some(archive),
|
||||
prompt_catalog: Some(crate::prompt_settings::project_prompts_from_workspace_config(state)?),
|
||||
profile_source_archive: archive,
|
||||
profile_source_archive_handle: None,
|
||||
}
|
||||
.with_computed_digest();
|
||||
@@ -712,13 +716,26 @@ mod tests {
|
||||
|
||||
fn virtual_state(entries: Vec<config_source::ConfigEntry>) -> WorkspaceConfigState {
|
||||
let snapshot = config_source::ConfigTreeSnapshot::from_entries(7, entries).unwrap();
|
||||
let schema_bundle = config_source::WorkspaceConfigSchemaBundle::compose([
|
||||
ProfileConfigSchemaProvider.contribution().unwrap(),
|
||||
crate::prompt_settings::PromptConfigSchemaProvider
|
||||
.contribution()
|
||||
.unwrap(),
|
||||
])
|
||||
.unwrap();
|
||||
let contract = config_source::ToolchainContract::with_schema_bundle(
|
||||
config_source::DEFAULT_SCHEMA_VERSION,
|
||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
||||
schema_bundle,
|
||||
);
|
||||
let projection_digest = config_source::SnapshotEnvironment::new(snapshot.clone())
|
||||
.evaluate_contract(&contract)
|
||||
.unwrap()
|
||||
.projection_digest;
|
||||
WorkspaceConfigState {
|
||||
projection_digest: String::new(),
|
||||
contract: config_source::ToolchainContract::new(
|
||||
config_source::DEFAULT_SCHEMA_VERSION,
|
||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
||||
),
|
||||
projection_digest,
|
||||
contract,
|
||||
snapshot,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
use config_source::{ConfigProjectionValidator, ConfigSchemaContribution};
|
||||
use worker::{EffectivePromptCatalog, prompt_schema_source};
|
||||
|
||||
use crate::config_source::{
|
||||
WorkspaceConfigSchemaProvider, WorkspaceConfigState, evaluate_workspace_config_state,
|
||||
};
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PromptConfigSchemaProvider;
|
||||
|
||||
impl WorkspaceConfigSchemaProvider for PromptConfigSchemaProvider {
|
||||
fn contribution(&self) -> Result<ConfigSchemaContribution> {
|
||||
ConfigSchemaContribution::new(
|
||||
"builtin:prompts",
|
||||
"prompts",
|
||||
"1",
|
||||
prompt_schema_source().map_err(|error| Error::Config(error.to_string()))?,
|
||||
)
|
||||
.map(|contribution| {
|
||||
contribution.with_projection_validator(
|
||||
ConfigProjectionValidator::StaticTemplateCatalog {
|
||||
namespace: "prompts".to_string(),
|
||||
key_aliases: std::collections::BTreeMap::from([(
|
||||
"default_prompt".to_string(),
|
||||
"default".to_string(),
|
||||
)]),
|
||||
},
|
||||
)
|
||||
})
|
||||
.map_err(|error| Error::Config(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_evaluated_prompt_catalog(
|
||||
evaluation: &config_source::EvaluationResult,
|
||||
) -> Result<()> {
|
||||
let projection = evaluation.projections.first().ok_or_else(|| {
|
||||
Error::InvalidInput("Workspace config produced no active projection".to_string())
|
||||
})?;
|
||||
let prompts = projection.data_json.get("prompts").ok_or_else(|| {
|
||||
Error::InvalidInput("Workspace config projection has no prompts namespace".to_string())
|
||||
})?;
|
||||
EffectivePromptCatalog::from_projection(prompts, 0, "preview", "preview")
|
||||
.map(|_| ())
|
||||
.map_err(|error| Error::InvalidInput(format!("invalid Prompt catalog: {error}")))
|
||||
}
|
||||
|
||||
pub fn project_prompts_from_workspace_config(
|
||||
state: &WorkspaceConfigState,
|
||||
) -> Result<EffectivePromptCatalog> {
|
||||
let evaluation = evaluate_workspace_config_state(state, state.contract.schema_bundle.clone())?;
|
||||
if evaluation.projection_digest != state.projection_digest {
|
||||
return Err(Error::RegistryInconsistency(
|
||||
"Prompt projection digest does not match the active Workspace config revision"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
let projection = evaluation.projections.first().ok_or_else(|| {
|
||||
Error::RegistryInconsistency("Workspace config has no active projection".to_string())
|
||||
})?;
|
||||
let prompts = projection.data_json.get("prompts").ok_or_else(|| {
|
||||
Error::RegistryInconsistency(
|
||||
"active Workspace config projection has no prompts namespace".to_string(),
|
||||
)
|
||||
})?;
|
||||
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()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use config_source::{
|
||||
ConfigContentType, ConfigEntry, ConfigTreeSnapshot, SnapshotEnvironment, ToolchainContract,
|
||||
VirtualPath, WorkspaceConfigSchemaBundle,
|
||||
};
|
||||
|
||||
fn state(source: &str) -> WorkspaceConfigState {
|
||||
let schema = WorkspaceConfigSchemaBundle::compose([PromptConfigSchemaProvider
|
||||
.contribution()
|
||||
.unwrap()])
|
||||
.unwrap();
|
||||
let snapshot = ConfigTreeSnapshot::from_entries(
|
||||
7,
|
||||
[ConfigEntry::new(
|
||||
VirtualPath::parse("main.dcdl").unwrap(),
|
||||
ConfigContentType::Decodal,
|
||||
source,
|
||||
)
|
||||
.unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let contract = ToolchainContract::with_schema_bundle(
|
||||
config_source::DEFAULT_SCHEMA_VERSION,
|
||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
||||
schema,
|
||||
);
|
||||
let projection_digest = SnapshotEnvironment::new(snapshot.clone())
|
||||
.evaluate_contract(&contract)
|
||||
.unwrap()
|
||||
.projection_digest;
|
||||
WorkspaceConfigState {
|
||||
snapshot,
|
||||
contract,
|
||||
projection_digest,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_override_deep_patches_builtin_and_preserves_other_leaves() {
|
||||
let state = state(r#"{ prompts = { common = { language = "OVERRIDE"; }; }; }"#);
|
||||
let catalog = project_prompts_from_workspace_config(&state).unwrap();
|
||||
assert_eq!(catalog.config_revision, 7);
|
||||
assert_eq!(catalog.templates["common.language"], "OVERRIDE");
|
||||
assert!(!catalog.templates["common.workspace"].is_empty());
|
||||
assert!(catalog.templates["default"].contains("common.workspace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_commit_validator_rejects_dynamic_missing_and_cyclic_includes() {
|
||||
let schema = WorkspaceConfigSchemaBundle::compose([PromptConfigSchemaProvider
|
||||
.contribution()
|
||||
.unwrap()])
|
||||
.unwrap();
|
||||
for source in [
|
||||
r#"{ prompts = { common = { language = "{% include target %}"; }; }; }"#,
|
||||
r#"{ prompts = { common = { language = "{% include \"missing\" %}"; }; }; }"#,
|
||||
r#"{ prompts = { common = { language = "{% include \"common.workspace\" %}"; workspace = "{% include \"common.language\" %}"; }; }; }"#,
|
||||
] {
|
||||
let snapshot = ConfigTreeSnapshot::from_entries(
|
||||
0,
|
||||
[ConfigEntry::new(
|
||||
VirtualPath::parse("main.dcdl").unwrap(),
|
||||
ConfigContentType::Decodal,
|
||||
source,
|
||||
)
|
||||
.unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let contract = ToolchainContract::with_schema_bundle(
|
||||
config_source::DEFAULT_SCHEMA_VERSION,
|
||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
||||
schema.clone(),
|
||||
);
|
||||
assert!(
|
||||
SnapshotEnvironment::new(snapshot)
|
||||
.evaluate_contract(&contract)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_prompt_schema_rejects_unknown_and_non_string_leaves() {
|
||||
let schema = WorkspaceConfigSchemaBundle::compose([PromptConfigSchemaProvider
|
||||
.contribution()
|
||||
.unwrap()])
|
||||
.unwrap();
|
||||
for source in [
|
||||
"{ prompts = { common = { unknown = \"bad\"; }; }; }",
|
||||
"{ prompts = { common = { language = 42; }; }; }",
|
||||
] {
|
||||
let snapshot = ConfigTreeSnapshot::from_entries(
|
||||
0,
|
||||
[ConfigEntry::new(
|
||||
VirtualPath::parse("main.dcdl").unwrap(),
|
||||
ConfigContentType::Decodal,
|
||||
source,
|
||||
)
|
||||
.unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let contract = ToolchainContract::with_schema_bundle(
|
||||
config_source::DEFAULT_SCHEMA_VERSION,
|
||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
||||
schema.clone(),
|
||||
);
|
||||
assert!(
|
||||
SnapshotEnvironment::new(snapshot)
|
||||
.evaluate_contract(&contract)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -755,6 +755,7 @@ impl WorkspaceApi {
|
||||
.with_provider(Arc::new(
|
||||
crate::profile_settings::ProfileConfigSchemaProvider,
|
||||
))
|
||||
.with_provider(Arc::new(crate::prompt_settings::PromptConfigSchemaProvider))
|
||||
.with_provider(Arc::new(skills::SkillConfigSchemaProvider));
|
||||
config_store.ensure_workspace_config_materialized_with_schema(
|
||||
&config.workspace_id,
|
||||
@@ -2522,13 +2523,13 @@ async fn scoped_preview_workspace_config_tree(
|
||||
Json(request): Json<ConfigPreviewRequest>,
|
||||
) -> ApiResult<Json<crate::config_source::EvaluatedConfigCandidate>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
Ok(Json(
|
||||
api.config_store.preview_workspace_config_with_schema(
|
||||
&path.workspace_id,
|
||||
&request,
|
||||
api.config_schema_registry.compose()?,
|
||||
)?,
|
||||
))
|
||||
let candidate = api.config_store.preview_workspace_config_with_schema(
|
||||
&path.workspace_id,
|
||||
&request,
|
||||
api.config_schema_registry.compose()?,
|
||||
)?;
|
||||
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
|
||||
Ok(Json(candidate))
|
||||
}
|
||||
|
||||
async fn scoped_commit_workspace_config_tree(
|
||||
@@ -2544,6 +2545,7 @@ async fn scoped_commit_workspace_config_tree(
|
||||
&request,
|
||||
api.config_schema_registry.compose()?,
|
||||
)?;
|
||||
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
|
||||
let state = api
|
||||
.config_store
|
||||
.commit_evaluated_workspace_config(&path.workspace_id, &candidate)?;
|
||||
@@ -15256,6 +15258,7 @@ mod tests {
|
||||
label: Some("server-test".to_string()),
|
||||
}],
|
||||
declarations: Vec::new(),
|
||||
prompt_catalog: None,
|
||||
profile_source_archive: None,
|
||||
profile_source_archive_handle: None,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user