use serde::{Deserialize, Serialize};
use crate::UseResult;
use super::plan::{
PlanAuthority, PlanEnforcementProfile, PlanQualifiedSurfaceRef, PlanScope,
PlannedOperationImpact, PlannedPackageTransition, PlannedProviderEvidence,
PlannedStateEvidence, PlannedWorkspaceImpact, PluginOperationAction, PluginOperationPlan,
};
use super::plan_validation::{planned_okf_changes, planned_secret_changes};
use super::{
parse_contract, plan::plan_error, PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3,
PLUGIN_OPERATION_PLAN_SCHEMA_V4,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PluginOperationPlanDraft {
pub schema: String,
pub action: PluginOperationAction,
pub package_id: String,
pub component_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub package_lock_digest: Option<String>,
pub packages: Vec<PlannedPackageTransition>,
pub providers: Vec<PlannedProviderEvidence>,
pub workspace_impacts: Vec<PlannedWorkspaceImpact>,
pub impact: PlannedOperationImpact,
pub state: PlannedStateEvidence,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginOperationPlanBinding {
pub operation_id: String,
pub created_at_ms: u64,
pub expires_at_ms: u64,
pub scope: PlanScope,
pub authority: PlanAuthority,
}
impl PluginOperationPlanDraft {
#[allow(clippy::too_many_arguments)]
pub fn new(
action: PluginOperationAction,
package_id: impl Into<String>,
component_id: impl Into<String>,
packages: Vec<PlannedPackageTransition>,
providers: Vec<PlannedProviderEvidence>,
workspace_impacts: Vec<PlannedWorkspaceImpact>,
impact: PlannedOperationImpact,
state: PlannedStateEvidence,
) -> UseResult<Self> {
let mut impact = impact;
impact.okf_changes = planned_okf_changes(action, &packages)?;
let draft = Self {
schema: PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3.to_string(),
action,
package_id: package_id.into(),
component_id: component_id.into(),
package_lock_digest: None,
packages,
providers,
workspace_impacts,
impact,
state,
};
draft.validate()?;
Ok(draft)
}
#[allow(clippy::too_many_arguments)]
pub fn new_unbound(
action: PluginOperationAction,
package_id: impl Into<String>,
component_id: impl Into<String>,
packages: Vec<PlannedPackageTransition>,
workspace_impacts: Vec<PlannedWorkspaceImpact>,
impact: PlannedOperationImpact,
state: PlannedStateEvidence,
) -> UseResult<Self> {
let mut impact = impact;
impact.okf_changes = planned_okf_changes(action, &packages)?;
let draft = Self {
schema: PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3.to_string(),
action,
package_id: package_id.into(),
component_id: component_id.into(),
package_lock_digest: None,
packages,
providers: Vec::new(),
workspace_impacts,
impact,
state,
};
draft.validate_unbound()?;
Ok(draft)
}
pub fn from_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"plugin operation plan draft",
super::plan::PLAN_ERROR,
Self::validate,
)
}
pub fn from_unbound_json(input: &[u8]) -> UseResult<Self> {
parse_contract(
input,
"unbound plugin operation plan draft",
super::plan::PLAN_ERROR,
Self::validate_unbound,
)
}
pub fn validate(&self) -> UseResult<()> {
if self.schema != PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3 {
return Err(plan_error(
"The plugin operation plan draft schema is unsupported.",
));
}
self.clone().bind_unchecked(validation_binding()).map(drop)
}
pub fn validate_unbound(&self) -> UseResult<()> {
if self.schema != PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3 || !self.providers.is_empty() {
return Err(plan_error(
"An unbound plugin operation draft must use the current schema and contain no provider evidence.",
));
}
let mut validation = self.clone();
validation.providers =
unbound_validation_providers(validation.action, &validation.packages);
validation.bind_unchecked(validation_binding()).map(drop)
}
pub fn bind(self, binding: PluginOperationPlanBinding) -> UseResult<PluginOperationPlan> {
if self.schema != PLUGIN_OPERATION_PLAN_DRAFT_SCHEMA_V3 {
return Err(plan_error(
"The plugin operation plan draft schema is unsupported.",
));
}
self.bind_unchecked(binding)
}
fn bind_unchecked(self, binding: PluginOperationPlanBinding) -> UseResult<PluginOperationPlan> {
let secret_changes = planned_secret_changes(self.action, &self.packages);
let plan = PluginOperationPlan {
schema: PLUGIN_OPERATION_PLAN_SCHEMA_V4.to_string(),
operation_id: binding.operation_id,
created_at_ms: binding.created_at_ms,
expires_at_ms: binding.expires_at_ms,
action: self.action,
package_id: self.package_id,
component_id: self.component_id,
scope: binding.scope,
package_lock_digest: self.package_lock_digest,
prior_package_lock_digest: None,
packages: self.packages,
secret_changes,
providers: self.providers,
workspace_impacts: self.workspace_impacts,
impact: self.impact,
authority: binding.authority,
state: self.state,
};
plan.validate()?;
Ok(plan)
}
}
fn unbound_validation_providers(
action: PluginOperationAction,
packages: &[PlannedPackageTransition],
) -> Vec<PlannedProviderEvidence> {
if matches!(
action,
PluginOperationAction::Uninstall | PluginOperationAction::Disable
) {
return Vec::new();
}
let mut providers = packages
.iter()
.filter_map(|package| {
package
.after
.as_ref()
.map(|state| (package.package_id.as_str(), state))
})
.flat_map(|(package_id, state)| {
state.release.surfaces.iter().filter_map(move |surface| {
if !matches!(
surface.kind,
super::PluginSurfaceKind::Tool | super::PluginSurfaceKind::Mcp
) {
return None;
}
let reference = surface.reference();
let permission = state
.permissions
.surfaces
.iter()
.find(|permission| permission.surface == reference)?;
Some(PlannedProviderEvidence {
surface: PlanQualifiedSurfaceRef {
package_id: package_id.to_owned(),
surface: reference,
},
provider_id: "a3s-use-unbound-provider".to_owned(),
provider_build_id: "host-preflight-pending".to_owned(),
capability_digest:
"sha256:0000000000000000000000000000000000000000000000000000000000000000"
.to_owned(),
semantics_profile_digest:
"sha256:0000000000000000000000000000000000000000000000000000000000000000"
.to_owned(),
enforcement: if permission.native_execution {
PlanEnforcementProfile::Sandbox
} else {
PlanEnforcementProfile::Container
},
})
})
})
.collect::<Vec<_>>();
providers.sort_by(|left, right| left.surface.cmp(&right.surface));
providers
}
fn validation_binding() -> PluginOperationPlanBinding {
PluginOperationPlanBinding {
operation_id: "draft:validation".to_string(),
created_at_ms: 1,
expires_at_ms: 2,
scope: PlanScope {
kind: super::PlanScopeKind::User,
id: "current".to_string(),
},
authority: PlanAuthority {
actor: super::PlanActor::User,
decision: super::PlanPolicyDecision::Ask,
policy_digest:
"sha256:0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
confirmation_required: true,
},
}
}