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:
@@ -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
|
||||
@@ -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"}}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user