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
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "plugin-plan"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
publish = false
[dependencies]
manifest.workspace = true
schemars.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
thiserror.workspace = true
+5
View File
@@ -0,0 +1,5 @@
# Portable Plugin execution plan
This crate owns the immutable [`WorkerPluginExecutionPlan`] model, validation and canonical fingerprint shared by Server persistence and Runtime launch snapshots. [`ArtifactDescriptor`] projects only the artifact metadata needed to construct or compare exact pins without depending on Server API types.
It does not own catalog admission, persistence, configuration resolution, artifact delivery or authorization. A deserialized plan or owner-supplied artifact projection is not evidence of trust or live execution authority; callers must enforce those boundaries with their owners.
@@ -0,0 +1 @@
{"format_version":1,"input":{"config":{"content_digest":"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","toolchain_fingerprint":"toolchain"},"execution":{"delegation_id":"delegation","principal_id":"principal","runtime_id":"runtime","worker_id":"worker"},"plugins":[{"artifact_digest":"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","artifact_size":42,"component_world":"yoi:plugin/https-logged-tool-plugin@2.0.0","config_digest":"sha256:4bf0a2da94b0c881343726db532ddbfeeed37790af7da44e790cc0bba708654e","config_schema":{"digest":"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","identity":"config","namespace":"example","version":"1"},"expected_last_operation":"operation","installation_id":"installation","package_digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","package_id":"package","package_schema_version":2,"package_version":"1.0.0","projection":{"bindings":[{"binding_id":"binding","policy_digest":"sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","slot":"api"}],"config":{"a":[{"a":false,"z":0}],"z":{"a":1,"b":2}},"grants":[{"kind":"logging"},{"kind":"https","slot":"api"}],"surfaces":["tool"],"tools":["echo","other"]},"required_imports":["https","logging"]}],"profile":{"identity":"profile:coder","projection_digest":"sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"},"workspace_id":"workspace"}}
+446
View File
@@ -0,0 +1,446 @@
#![doc = include_str!("../README.md")]
use manifest::plugin;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum PlanError {
#[error("invalid Plugin execution plan")]
Invalid,
#[error("Plugin execution plan conflict")]
Conflict,
}
pub type Result<T> = std::result::Result<T, PlanError>;
/// Neutral metadata used to validate and reproduce an exact pin. This excludes
/// descriptions, Tool schemas, schema source, credentials and artifact locations.
/// A projection is not proof of catalog admission or current authorization.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactIdentityProjection {
pub package_id: String,
pub version: String,
pub package_digest: String,
pub artifact_digest: String,
pub artifact_size: u32,
pub schema_version: u32,
pub world: String,
pub required_imports: Vec<String>,
pub tools: Vec<String>,
pub connections: Vec<String>,
pub requested_grants: Vec<PluginGrant>,
pub config_schema: PluginSchemaIdentity,
pub revoked: bool,
}
/// Adapts an owner's artifact metadata without coupling the portable model to
/// that owner's API. Callers must still perform admission with the owner; an
/// implementation of this trait does not establish metadata trust.
pub trait ArtifactDescriptor {
fn artifact_identity(&self) -> ArtifactIdentityProjection;
}
impl ArtifactDescriptor for ArtifactIdentityProjection {
fn artifact_identity(&self) -> ArtifactIdentityProjection {
self.clone()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConfigProvenance {
pub content_digest: String,
pub toolchain_fingerprint: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ProfileProvenance {
pub identity: String,
pub projection_digest: String,
}
/// Supplied by the authenticated Host launch/delegation boundary, never by guest
/// or model input. These are identities only, not credentials or provider handles.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HostExecutionIdentity {
pub runtime_id: String,
pub worker_id: String,
pub principal_id: String,
pub delegation_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct BindingPolicyReference {
pub slot: String,
pub binding_id: String,
pub policy_digest: String,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
pub enum PluginSurface {
Tool,
}
/// Only capabilities implemented by the initial Tool Host are representable.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum PluginGrant {
Logging,
Https { slot: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PluginSchemaIdentity {
pub identity: String,
pub version: String,
pub digest: String,
pub namespace: String,
}
/// The upstream config/schema authority must validate config values as non-secret
/// and intersect grants/surfaces with Workspace, Profile and delegation bounds.
/// Binding policies remain with their owner: only identities and digests travel.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct PluginPinProjection {
pub config: Value,
pub surfaces: Vec<PluginSurface>,
pub tools: Vec<String>,
pub grants: Vec<PluginGrant>,
pub bindings: Vec<BindingPolicyReference>,
}
/// All fields are private: construct from admitted artifact metadata, not a
/// hand-built wire snapshot. Deserialization alone does not establish admission.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkerPluginPin {
installation_id: String,
expected_last_operation: String,
package_id: String,
package_version: String,
package_digest: String,
artifact_digest: String,
artifact_size: u32,
package_schema_version: u32,
component_world: String,
required_imports: Vec<String>,
config_schema: PluginSchemaIdentity,
config_digest: String,
projection: PluginPinProjection,
}
impl WorkerPluginPin {
pub fn from_artifact(
installation_id: String,
expected_last_operation: String,
artifact: &(impl ArtifactDescriptor + ?Sized),
mut projection: PluginPinProjection,
) -> Result<Self> {
let artifact = artifact.artifact_identity();
if artifact.revoked {
return conflict();
}
if !valid_identity(&installation_id)
|| !valid_identity(&expected_last_operation)
|| !valid_digest(&artifact.package_digest)
|| !valid_digest(&artifact.artifact_digest)
|| artifact.artifact_size == 0
|| artifact.schema_version != plugin::SUPPORTED_PLUGIN_API_VERSION
|| !matches!(
artifact.world.as_str(),
plugin::PLUGIN_COMPONENT_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_HTTPS_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_LOGGED_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_HTTPS_LOGGED_TOOL_WORLD
)
|| !valid_digest(&artifact.config_schema.digest)
|| !valid_identity(&artifact.package_id)
|| !valid_identity(&artifact.version)
|| !valid_identity(&artifact.config_schema.identity)
|| !valid_identity(&artifact.config_schema.version)
|| !valid_identity(&artifact.config_schema.namespace)
|| !projection.config.is_object()
{
return invalid();
}
projection.surfaces.sort();
projection.surfaces.dedup();
projection.tools.sort();
projection.tools.dedup();
projection.grants.sort();
projection.grants.dedup();
projection.bindings.sort();
projection.config = canonical_value(&projection.config);
if (!projection.tools.is_empty() && !projection.surfaces.contains(&PluginSurface::Tool))
|| projection.tools.iter().any(|name| {
!valid_identity(name) || !artifact.tools.iter().any(|tool| *tool == *name)
})
|| projection
.bindings
.windows(2)
.any(|b| b[0].slot == b[1].slot)
|| projection.bindings.iter().any(|binding| {
!valid_identity(&binding.slot)
|| !valid_identity(&binding.binding_id)
|| !valid_digest(&binding.policy_digest)
|| !artifact
.connections
.iter()
.any(|slot| *slot == binding.slot)
})
|| projection.grants.iter().any(|grant| match grant {
PluginGrant::Logging => !artifact.requested_grants.contains(&PluginGrant::Logging),
PluginGrant::Https { slot } => {
!valid_identity(slot)
|| !artifact
.requested_grants
.contains(&PluginGrant::Https { slot: slot.clone() })
|| !projection
.bindings
.iter()
.any(|binding| binding.slot == *slot)
}
})
|| projection.bindings.iter().any(|binding| {
!projection.grants.contains(&PluginGrant::Https {
slot: binding.slot.clone(),
})
})
{
return invalid();
}
let mut required_imports = artifact.required_imports.clone();
required_imports.sort();
required_imports.dedup();
Ok(Self {
installation_id,
expected_last_operation,
package_id: artifact.package_id.clone(),
package_version: artifact.version.clone(),
package_digest: artifact.package_digest.clone(),
artifact_digest: artifact.artifact_digest.clone(),
artifact_size: artifact.artifact_size,
package_schema_version: artifact.schema_version,
component_world: artifact.world.clone(),
required_imports,
config_schema: PluginSchemaIdentity {
identity: artifact.config_schema.identity.clone(),
version: artifact.config_schema.version.clone(),
digest: artifact.config_schema.digest.clone(),
namespace: artifact.config_schema.namespace.clone(),
},
config_digest: digest_json(&projection.config)?,
projection,
})
}
pub fn installation_id(&self) -> &str {
&self.installation_id
}
pub fn expected_last_operation(&self) -> &str {
&self.expected_last_operation
}
pub fn package_id(&self) -> &str {
&self.package_id
}
pub fn package_version(&self) -> &str {
&self.package_version
}
pub fn package_schema_version(&self) -> u32 {
self.package_schema_version
}
pub fn component_world(&self) -> &str {
&self.component_world
}
pub fn required_imports(&self) -> &[String] {
&self.required_imports
}
pub fn package_digest(&self) -> &str {
&self.package_digest
}
pub fn artifact_digest(&self) -> &str {
&self.artifact_digest
}
pub fn artifact_size(&self) -> u32 {
self.artifact_size
}
pub fn config_schema(&self) -> &PluginSchemaIdentity {
&self.config_schema
}
pub fn config_digest(&self) -> &str {
&self.config_digest
}
pub fn projection(&self) -> &PluginPinProjection {
&self.projection
}
pub fn matches_artifact(&self, artifact: &(impl ArtifactDescriptor + ?Sized)) -> Result<bool> {
Ok(Self::from_artifact(
self.installation_id.clone(),
self.expected_last_operation.clone(),
artifact,
self.projection.clone(),
)? == *self)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkerPluginPlanInput {
pub workspace_id: String,
pub config: ConfigProvenance,
pub profile: ProfileProvenance,
pub execution: HostExecutionIdentity,
pub plugins: Vec<WorkerPluginPin>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkerPluginExecutionPlan {
format_version: u32,
input: WorkerPluginPlanInput,
}
impl WorkerPluginExecutionPlan {
pub fn canonicalize(mut input: WorkerPluginPlanInput) -> Result<Self> {
if !valid_identity(&input.workspace_id)
|| !valid_digest(&input.config.content_digest)
|| !valid_identity(&input.config.toolchain_fingerprint)
|| !valid_identity(&input.profile.identity)
|| !valid_digest(&input.profile.projection_digest)
|| !valid_identity(&input.execution.runtime_id)
|| !valid_identity(&input.execution.worker_id)
|| !valid_identity(&input.execution.principal_id)
|| input
.execution
.delegation_id
.as_ref()
.is_some_and(|id| !valid_identity(id))
|| input.plugins.len() > 128
{
return invalid();
}
input
.plugins
.sort_by(|a, b| a.installation_id.cmp(&b.installation_id));
// One selection per installation; two aliases of one package instead use
// distinct installation identities, as required by the catalog contract.
if input
.plugins
.windows(2)
.any(|pins| pins[0].installation_id == pins[1].installation_id)
{
return invalid();
}
// Public Tool names use immutable installation identities, never aliases.
let mut names = BTreeSet::new();
for pin in &input.plugins {
for tool in &pin.projection.tools {
let name = format!("{}__{}", pin.installation_id, tool);
if name.len() > 128 || !names.insert(name) {
return invalid();
}
}
}
Ok(Self {
format_version: 1,
input,
})
}
pub fn input(&self) -> &WorkerPluginPlanInput {
&self.input
}
pub fn workspace_id(&self) -> &str {
&self.input.workspace_id
}
pub fn config(&self) -> &ConfigProvenance {
&self.input.config
}
pub fn profile(&self) -> &ProfileProvenance {
&self.input.profile
}
pub fn execution(&self) -> &HostExecutionIdentity {
&self.input.execution
}
pub fn plugins(&self) -> &[WorkerPluginPin] {
&self.input.plugins
}
pub fn fingerprint(&self) -> Result<String> {
digest_json(self)
}
pub fn canonical_json(&self) -> Result<String> {
encode_canonical(self)
}
pub fn validate_canonical(&self) -> Result<()> {
if self.format_version != 1 || Self::canonicalize(self.input.clone())? != *self {
return invalid();
}
Ok(())
}
}
fn invalid<T>() -> Result<T> {
Err(PlanError::Invalid)
}
fn conflict<T>() -> Result<T> {
Err(PlanError::Conflict)
}
fn valid_identity(value: &str) -> bool {
!value.is_empty() && value.len() <= 512 && !value.chars().any(char::is_control)
}
fn valid_digest(value: &str) -> bool {
value.strip_prefix("sha256:").is_some_and(|hex| {
hex.len() == 64
&& hex
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
})
}
fn canonical_value(value: &Value) -> Value {
match value {
Value::Object(fields) => {
let mut keys: Vec<_> = fields.keys().collect();
keys.sort();
Value::Object(
keys.into_iter()
.map(|key| (key.clone(), canonical_value(&fields[key])))
.collect(),
)
}
Value::Array(values) => Value::Array(values.iter().map(canonical_value).collect()),
_ => value.clone(),
}
}
fn encode_canonical(value: &impl Serialize) -> Result<String> {
let value = serde_json::to_value(value).map_err(|_| PlanError::Invalid)?;
serde_json::to_string(&canonical_value(&value)).map_err(|_| PlanError::Invalid)
}
fn digest_json(value: &impl Serialize) -> Result<String> {
let bytes = encode_canonical(value)?;
Ok(hash_bytes(bytes.as_bytes()))
}
fn hash_bytes(bytes: &[u8]) -> String {
let hex: String = Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect();
format!("sha256:{hex}")
}
#[cfg(test)]
mod tests;
+204
View File
@@ -0,0 +1,204 @@
use super::*;
use serde_json::json;
fn digest(hex: char) -> String {
format!("sha256:{}", hex.to_string().repeat(64))
}
fn artifact() -> ArtifactIdentityProjection {
ArtifactIdentityProjection {
package_id: "package".into(),
version: "1.0.0".into(),
package_digest: digest('a'),
artifact_digest: digest('b'),
artifact_size: 42,
schema_version: 2,
world: plugin::PLUGIN_COMPONENT_HTTPS_LOGGED_TOOL_WORLD.into(),
required_imports: vec!["logging".into(), "https".into(), "logging".into()],
tools: vec!["other".into(), "echo".into()],
connections: vec!["api".into()],
requested_grants: vec![
PluginGrant::Https { slot: "api".into() },
PluginGrant::Logging,
],
config_schema: PluginSchemaIdentity {
identity: "config".into(),
version: "1".into(),
digest: digest('c'),
namespace: "example".into(),
},
revoked: false,
}
}
fn projection() -> PluginPinProjection {
PluginPinProjection {
config: serde_json::from_str(r#"{"z":{"b":2,"a":1},"a":[{"z":0,"a":false}]}"#).unwrap(),
surfaces: vec![PluginSurface::Tool, PluginSurface::Tool],
tools: vec!["other".into(), "echo".into(), "other".into()],
grants: vec![
PluginGrant::Https { slot: "api".into() },
PluginGrant::Logging,
],
bindings: vec![BindingPolicyReference {
slot: "api".into(),
binding_id: "binding".into(),
policy_digest: digest('f'),
}],
}
}
fn pin(
artifact: &impl ArtifactDescriptor,
projection: PluginPinProjection,
) -> Result<WorkerPluginPin> {
WorkerPluginPin::from_artifact(
"installation".into(),
"operation".into(),
artifact,
projection,
)
}
fn input() -> WorkerPluginPlanInput {
WorkerPluginPlanInput {
workspace_id: "workspace".into(),
config: ConfigProvenance {
content_digest: digest('d'),
toolchain_fingerprint: "toolchain".into(),
},
profile: ProfileProvenance {
identity: "profile:coder".into(),
projection_digest: digest('e'),
},
execution: HostExecutionIdentity {
runtime_id: "runtime".into(),
worker_id: "worker".into(),
principal_id: "principal".into(),
delegation_id: Some("delegation".into()),
},
plugins: vec![pin(&artifact(), projection()).unwrap()],
}
}
#[test]
fn canonical_wire_and_fingerprint_preserve_persisted_format() {
let plan = WorkerPluginExecutionPlan::canonicalize(input()).unwrap();
let expected = include_str!("fixtures/canonical-plan.json").trim_end();
assert_eq!(plan.canonical_json().unwrap(), expected);
assert_eq!(
plan.fingerprint().unwrap(),
"sha256:00ee15d79098f4afa137da5d4b0b9b78acde48a6157fdda0f3327d8c122bd889"
);
let restored: WorkerPluginExecutionPlan = serde_json::from_str(expected).unwrap();
restored.validate_canonical().unwrap();
assert_eq!(restored, plan);
assert!(restored.plugins()[0].matches_artifact(&artifact()).unwrap());
}
#[test]
fn artifact_identity_changes_are_not_hidden_by_projection_adapter() {
let original = pin(&artifact(), projection()).unwrap();
let mut changed = artifact();
changed.artifact_size += 1;
assert!(!original.matches_artifact(&changed).unwrap());
changed = artifact();
changed.config_schema.digest = digest('d');
assert!(!original.matches_artifact(&changed).unwrap());
changed = artifact();
changed.revoked = true;
assert_eq!(
original.matches_artifact(&changed),
Err(PlanError::Conflict)
);
}
#[test]
fn unsupported_or_malformed_artifact_identity_is_invalid() {
for change in [
|a: &mut ArtifactIdentityProjection| a.package_digest = "sha256:ABC".into(),
|a: &mut ArtifactIdentityProjection| a.artifact_digest = "digest".into(),
|a: &mut ArtifactIdentityProjection| a.artifact_size = 0,
|a: &mut ArtifactIdentityProjection| a.schema_version = 1,
|a: &mut ArtifactIdentityProjection| a.world = "unsupported".into(),
|a: &mut ArtifactIdentityProjection| a.package_id.clear(),
|a: &mut ArtifactIdentityProjection| a.version = "bad\nversion".into(),
|a: &mut ArtifactIdentityProjection| a.config_schema.identity.clear(),
|a: &mut ArtifactIdentityProjection| a.config_schema.version.clear(),
|a: &mut ArtifactIdentityProjection| a.config_schema.digest.clear(),
|a: &mut ArtifactIdentityProjection| a.config_schema.namespace.clear(),
] {
let mut value = artifact();
change(&mut value);
assert_eq!(pin(&value, projection()), Err(PlanError::Invalid));
}
}
#[test]
fn selected_tools_grants_and_bindings_cannot_expand_artifact_metadata() {
for change in [
|p: &mut PluginPinProjection| p.config = json!([]),
|p: &mut PluginPinProjection| p.surfaces.clear(),
|p: &mut PluginPinProjection| p.tools.push("absent".into()),
|p: &mut PluginPinProjection| p.bindings.push(p.bindings[0].clone()),
|p: &mut PluginPinProjection| p.bindings[0].slot = "absent".into(),
|p: &mut PluginPinProjection| p.bindings[0].binding_id.clear(),
|p: &mut PluginPinProjection| p.bindings[0].policy_digest.clear(),
|p: &mut PluginPinProjection| p.bindings.clear(),
|p: &mut PluginPinProjection| p.grants.retain(|g| *g == PluginGrant::Logging),
] {
let mut value = projection();
change(&mut value);
assert_eq!(pin(&artifact(), value), Err(PlanError::Invalid));
}
for change in [
|a: &mut ArtifactIdentityProjection| a.tools.clear(),
|a: &mut ArtifactIdentityProjection| a.connections.clear(),
|a: &mut ArtifactIdentityProjection| a.requested_grants.clear(),
] {
let mut value = artifact();
change(&mut value);
assert_eq!(pin(&value, projection()), Err(PlanError::Invalid));
}
}
#[test]
fn malformed_provenance_and_duplicate_installations_are_invalid() {
for change in [
|i: &mut WorkerPluginPlanInput| i.workspace_id.clear(),
|i: &mut WorkerPluginPlanInput| i.config.content_digest.clear(),
|i: &mut WorkerPluginPlanInput| i.config.toolchain_fingerprint.clear(),
|i: &mut WorkerPluginPlanInput| i.profile.identity.clear(),
|i: &mut WorkerPluginPlanInput| i.profile.projection_digest.clear(),
|i: &mut WorkerPluginPlanInput| i.execution.runtime_id.clear(),
|i: &mut WorkerPluginPlanInput| i.execution.worker_id.clear(),
|i: &mut WorkerPluginPlanInput| i.execution.principal_id.clear(),
|i: &mut WorkerPluginPlanInput| i.execution.delegation_id = Some(String::new()),
|i: &mut WorkerPluginPlanInput| i.plugins.push(i.plugins[0].clone()),
|i: &mut WorkerPluginPlanInput| i.plugins = vec![i.plugins[0].clone(); 129],
] {
let mut value = input();
change(&mut value);
assert_eq!(
WorkerPluginExecutionPlan::canonicalize(value),
Err(PlanError::Invalid)
);
}
}
#[test]
fn deserialized_unknown_fields_versions_and_noncanonical_order_are_rejected() {
let plan = WorkerPluginExecutionPlan::canonicalize(input()).unwrap();
let mut wire = serde_json::to_value(&plan).unwrap();
wire["input"]["execution"]["token"] = json!("credential");
assert!(serde_json::from_value::<WorkerPluginExecutionPlan>(wire).is_err());
let mut wire = serde_json::to_value(&plan).unwrap();
wire["format_version"] = json!(2);
let unsupported: WorkerPluginExecutionPlan = serde_json::from_value(wire).unwrap();
assert_eq!(unsupported.validate_canonical(), Err(PlanError::Invalid));
let mut unsorted = plan.clone();
let mut second = unsorted.input.plugins[0].clone();
second.installation_id = "aaa".into();
unsorted.input.plugins.push(second);
assert_eq!(unsorted.validate_canonical(), Err(PlanError::Invalid));
}