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:
2026-10-30 04:42:53 +09:00
parent 3a13301f75
commit 165153c63c
42 changed files with 2123 additions and 424 deletions
+1
View File
@@ -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
+2
View File
@@ -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")]
+2
View File
@@ -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()),
+1
View File
@@ -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")]
+87
View File
@@ -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())
}
+18
View File
@@ -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()),