feat: bind memory language to workspace settings

This commit is contained in:
2026-08-21 21:50:12 +09:00
parent 19f506f8bc
commit c4814115de
14 changed files with 927 additions and 72 deletions
+3
View File
@@ -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.
+15
View File
@@ -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(),
}),
}
}
+55 -1
View File
@@ -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);
+66 -2
View File
@@ -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)