feat: deliver committed Ticket events to plugin services

This commit is contained in:
2026-10-31 04:58:19 +09:00
parent 4f7dcfda61
commit b508fef698
94 changed files with 11852 additions and 247 deletions
+28 -2
View File
@@ -31,6 +31,7 @@ pub struct ArtifactIdentityProjection {
pub world: String,
pub required_imports: Vec<String>,
pub tools: Vec<String>,
pub services: Vec<String>,
pub connections: Vec<String>,
pub requested_grants: Vec<PluginGrant>,
pub config_schema: PluginSchemaIdentity,
@@ -89,6 +90,7 @@ pub struct BindingPolicyReference {
#[serde(rename_all = "snake_case")]
pub enum PluginSurface {
Tool,
Service,
}
/// Exact grant identities; new operations are never implied by old pins.
@@ -100,6 +102,7 @@ pub enum PluginGrant {
slot: String,
},
TicketRead,
TicketEvents,
TicketDataRead {
collection: String,
schema_id: String,
@@ -130,6 +133,8 @@ pub struct PluginPinProjection {
pub config: Value,
pub surfaces: Vec<PluginSurface>,
pub tools: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub services: Vec<String>,
pub grants: Vec<PluginGrant>,
pub bindings: Vec<BindingPolicyReference>,
}
@@ -178,6 +183,7 @@ impl WorkerPluginPin {
| plugin::PLUGIN_COMPONENT_LOGGED_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_HTTPS_LOGGED_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_SERVICE_WORLD
)
|| !valid_digest(&artifact.config_schema.digest)
|| !valid_identity(&artifact.package_id)
@@ -193,6 +199,8 @@ impl WorkerPluginPin {
projection.surfaces.dedup();
projection.tools.sort();
projection.tools.dedup();
projection.services.sort();
projection.services.dedup();
projection.grants.sort();
projection.grants.dedup();
projection.bindings.sort();
@@ -201,6 +209,16 @@ impl WorkerPluginPin {
|| projection.tools.iter().any(|name| {
!valid_identity(name) || !artifact.tools.iter().any(|tool| *tool == *name)
})
|| (!projection.services.is_empty()
&& !projection.surfaces.contains(&PluginSurface::Service))
|| (projection.surfaces.contains(&PluginSurface::Service)
&& (projection.services.is_empty()
|| !projection.grants.contains(&PluginGrant::TicketEvents)))
|| projection.services.iter().any(|name| {
!valid_identity(name)
|| !artifact.services.contains(name)
|| artifact.world != plugin::PLUGIN_COMPONENT_SERVICE_WORLD
})
|| projection
.bindings
.windows(2)
@@ -218,8 +236,11 @@ impl WorkerPluginPin {
PluginGrant::TicketRead
| PluginGrant::TicketDataRead { .. }
| PluginGrant::TicketDataWrite { .. } => {
artifact.world != plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD
|| !artifact.requested_grants.contains(grant)
!matches!(
artifact.world.as_str(),
plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_SERVICE_WORLD
) || !artifact.requested_grants.contains(grant)
|| match grant {
PluginGrant::TicketDataRead {
collection,
@@ -236,6 +257,11 @@ impl WorkerPluginPin {
_ => false,
}
}
PluginGrant::TicketEvents => {
artifact.world != plugin::PLUGIN_COMPONENT_SERVICE_WORLD
|| !artifact.requested_grants.contains(grant)
|| projection.services.is_empty()
}
PluginGrant::Logging => !artifact.requested_grants.contains(&PluginGrant::Logging),
PluginGrant::Https { slot } => {
!valid_identity(slot)
+46
View File
@@ -15,6 +15,7 @@ fn artifact() -> ArtifactIdentityProjection {
schema_version: 2,
world: plugin::PLUGIN_COMPONENT_HTTPS_LOGGED_TOOL_WORLD.into(),
required_imports: vec!["logging".into(), "https".into(), "logging".into()],
services: vec![],
tools: vec!["other".into(), "echo".into()],
connections: vec!["api".into()],
requested_grants: vec![
@@ -35,6 +36,7 @@ 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],
services: vec![],
tools: vec!["other".into(), "echo".into(), "other".into()],
grants: vec![
PluginGrant::Https { slot: "api".into() },
@@ -238,3 +240,47 @@ fn ticket_grants_require_exact_world_request_and_schema_namespace() {
a.requested_grants.retain(|g| g != &write);
assert_eq!(exact.matches_artifact(&a), Err(PlanError::Invalid));
}
#[test]
fn service_selection_needs_exact_new_world_declared_name_and_explicit_event_grant() {
let mut artifact = artifact();
let mut selected = projection();
selected.tools.clear();
selected.bindings.clear();
selected.grants = vec![PluginGrant::TicketEvents];
selected.surfaces = vec![PluginSurface::Service];
selected.services = vec!["watch".into()];
artifact.services = vec!["watch".into()];
artifact.requested_grants.push(PluginGrant::TicketEvents);
assert_eq!(pin(&artifact, selected.clone()), Err(PlanError::Invalid));
artifact.world = plugin::PLUGIN_COMPONENT_SERVICE_WORLD.into();
let exact = pin(&artifact, selected.clone()).unwrap();
selected.services.push("undeclared".into());
assert_eq!(pin(&artifact, selected), Err(PlanError::Invalid));
artifact.services.clear();
assert_eq!(exact.matches_artifact(&artifact), Err(PlanError::Invalid));
}
#[test]
fn old_serialized_pin_does_not_acquire_service_selection_or_event_grant() {
let plan: WorkerPluginExecutionPlan =
serde_json::from_str(include_str!("fixtures/canonical-plan.json")).unwrap();
assert!(plan.plugins()[0].projection().services.is_empty());
assert!(
!plan.plugins()[0]
.projection()
.surfaces
.contains(&PluginSurface::Service)
);
assert!(
!plan.plugins()[0]
.projection()
.grants
.contains(&PluginGrant::TicketEvents)
);
// Additive default-off fields cannot mutate the exact old plan fingerprint.
assert_eq!(
plan.fingerprint().unwrap(),
"sha256:00ee15d79098f4afa137da5d4b0b9b78acde48a6157fdda0f3327d8c122bd889"
);
}