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),
|
label: embedded_profile_label(profile),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
declarations: Vec::new(),
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive,
|
profile_source_archive,
|
||||||
profile_source_archive_handle,
|
profile_source_archive_handle,
|
||||||
}
|
}
|
||||||
@@ -4436,6 +4437,7 @@ mod tests {
|
|||||||
name: "read".to_string(),
|
name: "read".to_string(),
|
||||||
reference: "capability:read".to_string(),
|
reference: "capability:read".to_string(),
|
||||||
}],
|
}],
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub mod memory_backend;
|
|||||||
pub mod memory_staging;
|
pub mod memory_staging;
|
||||||
pub mod observation;
|
pub mod observation;
|
||||||
pub mod profile_settings;
|
pub mod profile_settings;
|
||||||
|
pub mod prompt_settings;
|
||||||
pub mod records;
|
pub mod records;
|
||||||
#[cfg(feature = "typescript")]
|
#[cfg(feature = "typescript")]
|
||||||
pub use records::ticket_api_typescript;
|
pub use records::ticket_api_typescript;
|
||||||
|
|||||||
@@ -254,10 +254,13 @@ pub fn build_virtual_profile_config_bundle(
|
|||||||
workspace_created_at: &str,
|
workspace_created_at: &str,
|
||||||
selector: &str,
|
selector: &str,
|
||||||
) -> Result<Option<ConfigBundle>> {
|
) -> Result<Option<ConfigBundle>> {
|
||||||
let Some(entry) = projection.entries.get(selector) else {
|
let archive = projection
|
||||||
return Ok(None);
|
.entries
|
||||||
};
|
.get(selector)
|
||||||
let archive = build_virtual_profile_archive(selector, entry, &projection.sources, state)?;
|
.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 {
|
let bundle = ConfigBundle {
|
||||||
metadata: ConfigBundleMetadata {
|
metadata: ConfigBundleMetadata {
|
||||||
id: format!("workspace-config-profile-r{}", state.snapshot.revision),
|
id: format!("workspace-config-profile-r{}", state.snapshot.revision),
|
||||||
@@ -274,11 +277,12 @@ pub fn build_virtual_profile_config_bundle(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
profiles: vec![ConfigProfileDescriptor {
|
profiles: vec![ConfigProfileDescriptor {
|
||||||
selector: worker_runtime::catalog::ProfileSelector::Named(selector.to_string()),
|
selector: profile_selector,
|
||||||
label: Some(selector.to_string()),
|
label: Some(selector.to_string()),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
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,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
.with_computed_digest();
|
.with_computed_digest();
|
||||||
@@ -712,13 +716,26 @@ mod tests {
|
|||||||
|
|
||||||
fn virtual_state(entries: Vec<config_source::ConfigEntry>) -> WorkspaceConfigState {
|
fn virtual_state(entries: Vec<config_source::ConfigEntry>) -> WorkspaceConfigState {
|
||||||
let snapshot = config_source::ConfigTreeSnapshot::from_entries(7, entries).unwrap();
|
let snapshot = config_source::ConfigTreeSnapshot::from_entries(7, entries).unwrap();
|
||||||
WorkspaceConfigState {
|
let schema_bundle = config_source::WorkspaceConfigSchemaBundle::compose([
|
||||||
projection_digest: String::new(),
|
ProfileConfigSchemaProvider.contribution().unwrap(),
|
||||||
contract: config_source::ToolchainContract::new(
|
crate::prompt_settings::PromptConfigSchemaProvider
|
||||||
|
.contribution()
|
||||||
|
.unwrap(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let contract = config_source::ToolchainContract::with_schema_bundle(
|
||||||
config_source::DEFAULT_SCHEMA_VERSION,
|
config_source::DEFAULT_SCHEMA_VERSION,
|
||||||
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
vec![VirtualPath::parse("main.dcdl").unwrap()],
|
||||||
config_source::DEFAULT_IMPORT_POLICY_VERSION,
|
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,
|
||||||
|
contract,
|
||||||
snapshot,
|
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(
|
.with_provider(Arc::new(
|
||||||
crate::profile_settings::ProfileConfigSchemaProvider,
|
crate::profile_settings::ProfileConfigSchemaProvider,
|
||||||
))
|
))
|
||||||
|
.with_provider(Arc::new(crate::prompt_settings::PromptConfigSchemaProvider))
|
||||||
.with_provider(Arc::new(skills::SkillConfigSchemaProvider));
|
.with_provider(Arc::new(skills::SkillConfigSchemaProvider));
|
||||||
config_store.ensure_workspace_config_materialized_with_schema(
|
config_store.ensure_workspace_config_materialized_with_schema(
|
||||||
&config.workspace_id,
|
&config.workspace_id,
|
||||||
@@ -2522,13 +2523,13 @@ async fn scoped_preview_workspace_config_tree(
|
|||||||
Json(request): Json<ConfigPreviewRequest>,
|
Json(request): Json<ConfigPreviewRequest>,
|
||||||
) -> ApiResult<Json<crate::config_source::EvaluatedConfigCandidate>> {
|
) -> ApiResult<Json<crate::config_source::EvaluatedConfigCandidate>> {
|
||||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||||
Ok(Json(
|
let candidate = api.config_store.preview_workspace_config_with_schema(
|
||||||
api.config_store.preview_workspace_config_with_schema(
|
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
&request,
|
&request,
|
||||||
api.config_schema_registry.compose()?,
|
api.config_schema_registry.compose()?,
|
||||||
)?,
|
)?;
|
||||||
))
|
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
|
||||||
|
Ok(Json(candidate))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scoped_commit_workspace_config_tree(
|
async fn scoped_commit_workspace_config_tree(
|
||||||
@@ -2544,6 +2545,7 @@ async fn scoped_commit_workspace_config_tree(
|
|||||||
&request,
|
&request,
|
||||||
api.config_schema_registry.compose()?,
|
api.config_schema_registry.compose()?,
|
||||||
)?;
|
)?;
|
||||||
|
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
|
||||||
let state = api
|
let state = api
|
||||||
.config_store
|
.config_store
|
||||||
.commit_evaluated_workspace_config(&path.workspace_id, &candidate)?;
|
.commit_evaluated_workspace_config(&path.workspace_id, &candidate)?;
|
||||||
@@ -15256,6 +15258,7 @@ mod tests {
|
|||||||
label: Some("server-test".to_string()),
|
label: Some("server-test".to_string()),
|
||||||
}],
|
}],
|
||||||
declarations: Vec::new(),
|
declarations: Vec::new(),
|
||||||
|
prompt_catalog: None,
|
||||||
profile_source_archive: None,
|
profile_source_archive: None,
|
||||||
profile_source_archive_handle: None,
|
profile_source_archive_handle: None,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user