feat: bind memory language to workspace settings
This commit is contained in:
@@ -174,6 +174,9 @@ pub struct CreateWorkerRequest {
|
||||
pub worker_observation_grants: Vec<RuntimeWorkerRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub workspace_api: Option<WorkspaceApiRef>,
|
||||
/// Backend-authored immutable Workspace Memory settings snapshot.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
|
||||
}
|
||||
|
||||
/// Worker lifecycle status for the in-memory embedded runtime.
|
||||
|
||||
@@ -1882,6 +1882,11 @@ mod tests {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
base_url: format!("https://workspace.example/{workspace_id}"),
|
||||
});
|
||||
request.memory_settings = Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
@@ -2193,6 +2198,11 @@ mod tests {
|
||||
worker_observation_enabled: false,
|
||||
worker_observation_grants: Vec::new(),
|
||||
workspace_api: None,
|
||||
memory_settings: Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "local".to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2828,6 +2838,11 @@ mod ws_tests {
|
||||
worker_observation_enabled: false,
|
||||
worker_observation_grants: Vec::new(),
|
||||
workspace_api: None,
|
||||
memory_settings: Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "local".to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2794,6 +2794,27 @@ fn validate_create_workspace_scope(
|
||||
)));
|
||||
}
|
||||
}
|
||||
let snapshot = request.memory_settings.as_ref().ok_or_else(|| {
|
||||
RuntimeError::InvalidRequest(
|
||||
"Workspace-scoped Worker create requires a bound Memory settings snapshot".to_string(),
|
||||
)
|
||||
})?;
|
||||
if snapshot.workspace_id != workspace_id {
|
||||
return Err(RuntimeError::InvalidRequest(format!(
|
||||
"Memory settings workspace_id {} does not match Runtime auth workspace_id {workspace_id}",
|
||||
snapshot.workspace_id
|
||||
)));
|
||||
}
|
||||
if snapshot.settings_revision == 0 {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"Memory settings revision must be at least 1".to_string(),
|
||||
));
|
||||
}
|
||||
if !matches!(snapshot.language.as_str(), "English" | "Japanese") {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"Memory settings language must be a normalized supported value".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3086,6 +3107,11 @@ mod tests {
|
||||
worker_observation_enabled: false,
|
||||
worker_observation_grants: Vec::new(),
|
||||
workspace_api: None,
|
||||
memory_settings: Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "local".to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3095,9 +3121,37 @@ mod tests {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
base_url: format!("https://workspace.example/{workspace_id}"),
|
||||
});
|
||||
request.memory_settings = Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_create_requires_matching_normalized_memory_settings_snapshot() {
|
||||
let mut request = scoped_task_request("memory-snapshot", "workspace-a");
|
||||
assert!(validate_create_workspace_scope(&request, Some("workspace-a")).is_ok());
|
||||
|
||||
request.memory_settings = None;
|
||||
assert!(validate_create_workspace_scope(&request, Some("workspace-a")).is_err());
|
||||
|
||||
request.memory_settings = Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "workspace-b".to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
});
|
||||
assert!(validate_create_workspace_scope(&request, Some("workspace-a")).is_err());
|
||||
|
||||
request.memory_settings = Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "workspace-a".to_string(),
|
||||
settings_revision: 2,
|
||||
language: "english".to_string(),
|
||||
});
|
||||
assert!(validate_create_workspace_scope(&request, Some("workspace-a")).is_err());
|
||||
}
|
||||
|
||||
fn scope(workspace_id: &str, server_id: &str) -> RuntimeWorkspaceScope {
|
||||
RuntimeWorkspaceScope::new(workspace_id, server_id)
|
||||
}
|
||||
@@ -4484,7 +4538,7 @@ mod tests {
|
||||
let worker = runtime
|
||||
.create_worker_scoped(
|
||||
&RuntimeWorkspaceScope::new("workspace-a", "server"),
|
||||
task_request("legacy"),
|
||||
scoped_task_request("legacy", "workspace-a"),
|
||||
)
|
||||
.unwrap();
|
||||
drop(runtime);
|
||||
|
||||
@@ -642,6 +642,60 @@ fn runtime_local_workdir_session(
|
||||
))
|
||||
}
|
||||
|
||||
fn bind_workspace_memory_settings(
|
||||
manifest: &mut manifest::WorkerManifest,
|
||||
request: &CreateWorkerRequest,
|
||||
) -> Result<(), String> {
|
||||
let Some(snapshot) = request.memory_settings.as_ref() else {
|
||||
if request.workspace_api.is_some() {
|
||||
return Err(
|
||||
"Workspace Worker request is missing its bound Memory settings snapshot"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(workspace_api) = request.workspace_api.as_ref()
|
||||
&& snapshot.workspace_id != workspace_api.workspace_id
|
||||
{
|
||||
return Err(format!(
|
||||
"Memory settings workspace {} does not match Workspace API scope {}",
|
||||
snapshot.workspace_id, workspace_api.workspace_id
|
||||
));
|
||||
}
|
||||
manifest
|
||||
.memory
|
||||
.get_or_insert_with(manifest::MemoryConfig::default)
|
||||
.bind_workspace_settings(snapshot);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_worker_memory_settings(
|
||||
manifest: &manifest::WorkerManifest,
|
||||
request: &CreateWorkerRequest,
|
||||
) -> Result<(), String> {
|
||||
let Some(expected) = request.memory_settings.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
let actual = manifest
|
||||
.memory
|
||||
.as_ref()
|
||||
.and_then(manifest::MemoryConfig::workspace_settings)
|
||||
.ok_or_else(|| {
|
||||
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
|
||||
})?;
|
||||
if &actual != expected {
|
||||
return Err(format!(
|
||||
"Workspace Worker Memory settings snapshot mismatch: expected {} revision {}, restored {} revision {}",
|
||||
expected.workspace_id,
|
||||
expected.settings_revision,
|
||||
actual.workspace_id,
|
||||
actual.settings_revision
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
fn observe_workspace_prompt_projection(
|
||||
@@ -696,7 +750,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
let archive = self
|
||||
.resolve_profile_source_archive(&request.request.profile_source)
|
||||
.await?;
|
||||
let (manifest, mut loader) = {
|
||||
let (mut manifest, mut loader) = {
|
||||
let manifest = archive
|
||||
.resolve_profile(selector, &worker_root, &worker_name)
|
||||
.map_err(|err| format!("failed to resolve profile source archive: {err}"))?;
|
||||
@@ -714,6 +768,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
)?
|
||||
}
|
||||
};
|
||||
bind_workspace_memory_settings(&mut manifest, &request.request)?;
|
||||
if let Some(bundle) = request.config_bundle.as_ref()
|
||||
&& let Some(resolution) =
|
||||
self.observe_bundle_prompt_projection(bundle, observation_workspace_id.as_deref())?
|
||||
@@ -750,6 +805,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
)
|
||||
.await
|
||||
.map_err(|err| format!("failed to create Worker from profile: {err}"))?;
|
||||
validate_worker_memory_settings(worker.manifest(), &request.request)?;
|
||||
if let Some(binding) = request.working_directory.as_ref() {
|
||||
worker.bind_workdir_session(Some(runtime_local_workdir_session(
|
||||
&binding.working_directory.id,
|
||||
@@ -856,7 +912,8 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
self.embedded_worker_mutation_dispatcher.as_ref(),
|
||||
Some(self.prompt_projection_cache.clone()),
|
||||
);
|
||||
let (manifest, loader) = Self::restore_fallback_manifest(&worker_name)?;
|
||||
let (mut manifest, loader) = Self::restore_fallback_manifest(&worker_name)?;
|
||||
bind_workspace_memory_settings(&mut manifest, &request.request)?;
|
||||
|
||||
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
|
||||
let session_dir = worker_aggregate_dir.join("session");
|
||||
@@ -924,6 +981,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
}
|
||||
Err(err) => return Err(format!("failed to restore Worker from metadata: {err}")),
|
||||
};
|
||||
validate_worker_memory_settings(worker.manifest(), &request.request)?;
|
||||
let flow_transition_enabled = worker.manifest().feature.flow.enabled;
|
||||
if let Some(binding) = request.working_directory.as_ref() {
|
||||
worker.bind_workdir_session(Some(runtime_local_workdir_session(
|
||||
@@ -2504,6 +2562,7 @@ mod tests {
|
||||
worker_observation_enabled: false,
|
||||
worker_observation_grants: Vec::new(),
|
||||
workspace_api: None,
|
||||
memory_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2816,6 +2875,11 @@ mod tests {
|
||||
workspace_id: "workspace-restore".to_string(),
|
||||
base_url: "http://workspace.invalid".to_string(),
|
||||
});
|
||||
request.memory_settings = Some(manifest::WorkspaceMemorySettingsSnapshot {
|
||||
workspace_id: "workspace-restore".to_string(),
|
||||
settings_revision: 1,
|
||||
language: "English".to_string(),
|
||||
});
|
||||
let identity = RuntimeIdentityMaterial::generate("runtime-restore").unwrap();
|
||||
let error = match ProfileRuntimeWorkerFactory::new(root.path())
|
||||
.with_runtime_store_dir(&runtime_store_dir)
|
||||
|
||||
Reference in New Issue
Block a user