feat: stage exact Plugin Tools through shared Worker composition
Extract the unchanged canonical plan model for typed Runtime snapshots without a Server dependency cycle. Bind Tool consumer identities to persisted Session/call IDs, use the owner-issued context and current publication authorization, and register through FeatureInstallPlan with cooperative shutdown. Validation so far: root cargo check --locked, portable model 6 tests/doctest, Worker actual Component adapter 7 focused tests, Server plan 9 tests via shared isolation. Full product launch owner/pin lifecycle and Remote/Embedded vertical integration remain a subsequent coherent change.
This commit is contained in:
@@ -37,6 +37,7 @@ flow = { path = "../flow" }
|
||||
fs4 = { workspace = true, optional = true }
|
||||
manifest.workspace = true
|
||||
yoi-plugin-runtime = { path = "../plugin-runtime" }
|
||||
plugin-plan = { path = "../plugin-plan" }
|
||||
libc.workspace = true
|
||||
protocol.workspace = true
|
||||
runtime-api.workspace = true
|
||||
|
||||
@@ -298,6 +298,8 @@ pub struct CreateWorkerRequest {
|
||||
/// Backend-authored immutable Workspace Memory settings snapshot.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub memory_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub plugin_execution: Option<runtime_api::PluginExecutionSnapshot>,
|
||||
/// Backend-attested subject or consolidation attachment. A reusable Profile
|
||||
/// may enable subjektiv policy, but only this host-owned bit activates it.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
|
||||
@@ -3057,6 +3057,7 @@ mod tests {
|
||||
let profile = ProfileSelector::Builtin("builtin:coder".to_string());
|
||||
let bundle = test_bundle(profile.clone());
|
||||
CreateWorkerRequest {
|
||||
plugin_execution: None,
|
||||
worker_id: WorkerId::now_v7(),
|
||||
create_fingerprint: "test-create".to_string(),
|
||||
profile,
|
||||
@@ -5024,6 +5025,7 @@ mod ws_tests {
|
||||
fn ws_create_request() -> CreateWorkerRequest {
|
||||
let bundle = ws_test_bundle(ProfileSelector::Builtin("builtin:companion".to_string()));
|
||||
CreateWorkerRequest {
|
||||
plugin_execution: None,
|
||||
worker_id: WorkerId::now_v7(),
|
||||
create_fingerprint: "test-create".to_string(),
|
||||
profile: ProfileSelector::Builtin("builtin:companion".to_string()),
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod identity;
|
||||
pub mod interaction;
|
||||
pub mod management;
|
||||
pub mod observation;
|
||||
pub mod plugin;
|
||||
pub mod profile_archive;
|
||||
pub mod resource;
|
||||
#[cfg(feature = "restore-checkpoints")]
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Process-owned bridge for exact Plugin launch inputs. Implementations compose
|
||||
//! the existing delivery/cache/executor and live owner protocols; they may not
|
||||
//! reselect a Profile, search an artifact inventory, or mint execution authority.
|
||||
use crate::identity::WorkerRef;
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait PluginToolRuntime: Send + Sync {
|
||||
/// Prepare unpublished contributions for this exact persisted plan. Both
|
||||
/// create and restore call this boundary before Feature/controller publication.
|
||||
/// Success requires current owner startup admission, not only verified bytes.
|
||||
async fn prepare(
|
||||
&self,
|
||||
worker: &WorkerRef,
|
||||
snapshot: &runtime_api::PluginExecutionSnapshot,
|
||||
session_id: &str,
|
||||
) -> Result<worker::plugin::PluginToolsFeature, String>;
|
||||
}
|
||||
|
||||
/// Materialize only the exact Server-authored pins. This prepares contributions,
|
||||
/// never publishes them or claims startup admission: the Host bridge must perform
|
||||
/// its live owner preflight before returning from `PluginToolRuntime::prepare`.
|
||||
/// Metadata from delivery becomes an expectation only after full immutable-pin
|
||||
/// comparison; byte equality never issues an execution grant.
|
||||
pub async fn materialize(
|
||||
worker: &WorkerRef,
|
||||
snapshot: &runtime_api::PluginExecutionSnapshot,
|
||||
session_id: &str,
|
||||
source: &dyn yoi_plugin_runtime::delivery::ArtifactSource,
|
||||
owner: std::sync::Arc<dyn yoi_plugin_runtime::owner::PluginHostOwner>,
|
||||
cache: &yoi_plugin_runtime::delivery::ArtifactCache,
|
||||
limits: yoi_plugin_runtime::Limits,
|
||||
) -> Result<worker::plugin::PluginToolsFeature, String> {
|
||||
use yoi_plugin_runtime::{delivery::ArtifactExpectation, host::Grants};
|
||||
let plan = &snapshot.plan;
|
||||
if plan.validate_canonical().is_err()
|
||||
|| plan.fingerprint().ok().as_ref() != Some(&snapshot.fingerprint)
|
||||
|| plan.execution().worker_id != worker.worker_id.to_string()
|
||||
|| snapshot.plan_id.is_empty()
|
||||
{
|
||||
return Err("Plugin launch snapshot mismatch".into());
|
||||
}
|
||||
let mut pins = Vec::new();
|
||||
for pin in plan.plugins() {
|
||||
let request = server_api::PluginArtifactDeliveryRequest {
|
||||
plan_id: snapshot.plan_id.clone(),
|
||||
plan_fingerprint: snapshot.fingerprint.clone(),
|
||||
installation_id: pin.installation_id().into(),
|
||||
artifact_digest: pin.artifact_digest().into(),
|
||||
};
|
||||
let metadata = source
|
||||
.authorize(&request)
|
||||
.await
|
||||
.map_err(|_| "Plugin artifact admission unavailable")?;
|
||||
if metadata.workspace_id != plan.workspace_id()
|
||||
|| metadata.request != request
|
||||
|| !pin
|
||||
.matches_artifact(&metadata.artifact)
|
||||
.map_err(|_| "Plugin artifact metadata mismatch")?
|
||||
{
|
||||
return Err("Plugin artifact metadata mismatch".into());
|
||||
}
|
||||
let mut grants = Grants::default();
|
||||
for grant in &pin.projection().grants {
|
||||
match grant {
|
||||
plugin_plan::PluginGrant::Logging => grants.logging = true,
|
||||
plugin_plan::PluginGrant::Https { slot } => {
|
||||
grants.connection_slots.insert(slot.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
pins.push(worker::plugin::PluginToolPin {
|
||||
workspace_id: plan.workspace_id().into(),
|
||||
installation_id: pin.installation_id().into(),
|
||||
plan_id: snapshot.plan_id.clone(),
|
||||
subject: plan.execution().principal_id.clone(),
|
||||
request,
|
||||
expectation: ArtifactExpectation::new(metadata.artifact),
|
||||
tools: pin.projection().tools.clone(),
|
||||
config: pin.projection().config.clone(),
|
||||
grants,
|
||||
});
|
||||
}
|
||||
worker::plugin::PluginToolsFeature::prepare(pins, source, cache, owner, session_id, limits)
|
||||
.await
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
@@ -1242,6 +1242,7 @@ impl Runtime {
|
||||
})?;
|
||||
if existing_request.create_fingerprint != request.create_fingerprint
|
||||
|| existing_request.backend_job != request.backend_job
|
||||
|| existing_request.plugin_execution != request.plugin_execution
|
||||
{
|
||||
return Err(RuntimeError::InvalidRequest(format!(
|
||||
"worker {} was already created with a different fingerprint",
|
||||
@@ -1427,6 +1428,7 @@ impl Runtime {
|
||||
})?;
|
||||
if existing_request.create_fingerprint != request.create_fingerprint
|
||||
|| existing_request.backend_job != request.backend_job
|
||||
|| existing_request.plugin_execution != request.plugin_execution
|
||||
{
|
||||
return Err(RuntimeError::InvalidRequest(format!(
|
||||
"worker {} was already created with a different fingerprint",
|
||||
@@ -7428,6 +7430,21 @@ fn validate_logical_workdir_attachments(
|
||||
}
|
||||
|
||||
fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), RuntimeError> {
|
||||
if let Some(snapshot) = &request.plugin_execution {
|
||||
let valid = snapshot.plan.validate_canonical().is_ok()
|
||||
&& snapshot.plan.fingerprint().ok().as_ref() == Some(&snapshot.fingerprint)
|
||||
&& !snapshot.plan_id.is_empty()
|
||||
&& snapshot.plan.execution().worker_id == request.worker_id.to_string()
|
||||
&& request
|
||||
.workspace_api
|
||||
.as_ref()
|
||||
.is_some_and(|api| api.workspace_id == snapshot.plan.workspace_id());
|
||||
if !valid {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"Plugin execution snapshot does not match this Worker launch".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(binding) = &request.backend_job {
|
||||
binding
|
||||
.validate()
|
||||
@@ -8239,6 +8256,7 @@ mod tests {
|
||||
let profile = ProfileSelector::Builtin("builtin:coder".to_string());
|
||||
let bundle = test_bundle_for_profile(profile.clone());
|
||||
CreateWorkerRequest {
|
||||
plugin_execution: None,
|
||||
worker_id: WorkerId::now_v7(),
|
||||
create_fingerprint: "test-create".to_string(),
|
||||
profile,
|
||||
|
||||
@@ -67,6 +67,7 @@ fn resolved() -> manifest::WorkerManifest {
|
||||
|
||||
fn request() -> CreateWorkerRequest {
|
||||
CreateWorkerRequest {
|
||||
plugin_execution: None,
|
||||
worker_id: worker_id(),
|
||||
create_fingerprint: "new-create-fingerprint".into(),
|
||||
profile: ProfileSelector::Builtin("default".into()),
|
||||
|
||||
@@ -825,6 +825,7 @@ type SpawnModelClientFactory =
|
||||
#[derive(Clone)]
|
||||
pub struct ProfileRuntimeWorkerFactory {
|
||||
spawn_model_client_factory: Option<SpawnModelClientFactory>,
|
||||
plugin_runtime: Option<Arc<dyn crate::plugin::PluginToolRuntime>>,
|
||||
observation_hub: Arc<RuntimeWorkerObservationHub>,
|
||||
profile_base_dir: PathBuf,
|
||||
worker_aggregate_root: Option<PathBuf>,
|
||||
@@ -949,6 +950,7 @@ impl ProfileRuntimeWorkerFactory {
|
||||
let profile_base_dir = profile_base_dir.into();
|
||||
Self {
|
||||
spawn_model_client_factory: None,
|
||||
plugin_runtime: None,
|
||||
observation_hub: Arc::new(RuntimeWorkerObservationHub::default()),
|
||||
profile_base_dir,
|
||||
worker_aggregate_root: None,
|
||||
@@ -991,6 +993,16 @@ impl ProfileRuntimeWorkerFactory {
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind process-owned Plugin composition. Profile selection without this
|
||||
/// service fails closed; no ambient or offline fallback is available.
|
||||
pub fn with_plugin_runtime(
|
||||
mut self,
|
||||
runtime: Arc<dyn crate::plugin::PluginToolRuntime>,
|
||||
) -> Self {
|
||||
self.plugin_runtime = Some(runtime);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "http-server")]
|
||||
pub fn with_workspace_request_client(mut self, client: RuntimeWorkspaceRequestClient) -> Self {
|
||||
self.runtime_id = Some(client.runtime_id().to_string());
|
||||
@@ -2125,6 +2137,23 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
}
|
||||
})?;
|
||||
let worker = prepared.worker_mut();
|
||||
if let Some(snapshot) = &request.request.plugin_execution {
|
||||
if self.runtime_id.as_deref() != Some(snapshot.plan.execution().runtime_id.as_str()) {
|
||||
return Err("Plugin plan Runtime identity mismatch".into());
|
||||
}
|
||||
let runtime = self
|
||||
.plugin_runtime
|
||||
.as_ref()
|
||||
.ok_or("Plugin runtime provider unavailable")?;
|
||||
let tools = runtime
|
||||
.prepare(
|
||||
&request.worker_ref,
|
||||
snapshot,
|
||||
&worker.session_id().to_string(),
|
||||
)
|
||||
.await?;
|
||||
worker.bind_plugin_tools(tools);
|
||||
}
|
||||
if let Some(binding) = request.request.backend_job.as_ref() {
|
||||
worker
|
||||
.bind_backend_job(binding.clone())
|
||||
@@ -2933,6 +2962,27 @@ impl ProfileRuntimeWorkerFactory {
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(_) => return Err("restore run artifacts cannot be inspected"),
|
||||
}
|
||||
if let Some(snapshot) = &request.request.plugin_execution {
|
||||
if self.runtime_id.as_deref() != Some(snapshot.plan.execution().runtime_id.as_str())
|
||||
{
|
||||
return Err("Plugin plan Runtime identity mismatch");
|
||||
}
|
||||
let runtime = self
|
||||
.plugin_runtime
|
||||
.as_ref()
|
||||
.ok_or("Plugin runtime provider unavailable")?;
|
||||
let tools = runtime
|
||||
.prepare(
|
||||
&request.worker_ref,
|
||||
snapshot,
|
||||
&candidate.session_id().to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "Plugin restore preparation refused or unavailable")?;
|
||||
candidate
|
||||
.bind_plugin_tools(tools)
|
||||
.map_err(|_| "restored Plugin binding failed")?;
|
||||
}
|
||||
let prepared = candidate
|
||||
.prepare_runtime_controller(
|
||||
&run_dir,
|
||||
@@ -10430,6 +10480,7 @@ mod tests {
|
||||
fn create_request(_name: &str) -> CreateWorkerRequest {
|
||||
let bundle = test_bundle();
|
||||
CreateWorkerRequest {
|
||||
plugin_execution: None,
|
||||
worker_id: WorkerId::now_v7(),
|
||||
create_fingerprint: "test-create".to_string(),
|
||||
profile: ProfileSelector::Builtin("builtin:companion".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user