feat: add authorized Plugin Ticket data capabilities

This commit is contained in:
2026-10-30 21:45:03 +09:00
parent 0f82d6fa8f
commit 4f7dcfda61
72 changed files with 8847 additions and 65 deletions
+37 -2
View File
@@ -91,12 +91,25 @@ pub enum PluginSurface {
Tool,
}
/// Only capabilities implemented by the initial Tool Host are representable.
/// Exact grant identities; new operations are never implied by old pins.
#[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 },
Https {
slot: String,
},
TicketRead,
TicketDataRead {
collection: String,
schema_id: String,
schema_version: String,
},
TicketDataWrite {
collection: String,
schema_id: String,
schema_version: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
@@ -164,6 +177,7 @@ impl WorkerPluginPin {
| plugin::PLUGIN_COMPONENT_HTTPS_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_LOGGED_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_HTTPS_LOGGED_TOOL_WORLD
| plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD
)
|| !valid_digest(&artifact.config_schema.digest)
|| !valid_identity(&artifact.package_id)
@@ -201,6 +215,27 @@ impl WorkerPluginPin {
.any(|slot| *slot == binding.slot)
})
|| projection.grants.iter().any(|grant| match grant {
PluginGrant::TicketRead
| PluginGrant::TicketDataRead { .. }
| PluginGrant::TicketDataWrite { .. } => {
artifact.world != plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD
|| !artifact.requested_grants.contains(grant)
|| match grant {
PluginGrant::TicketDataRead {
collection,
schema_id,
schema_version,
}
| PluginGrant::TicketDataWrite {
collection,
schema_id,
schema_version,
} => [collection, schema_id, schema_version]
.iter()
.any(|v| !valid_identity(v) || v.len() > 128),
_ => false,
}
}
PluginGrant::Logging => !artifact.requested_grants.contains(&PluginGrant::Logging),
PluginGrant::Https { slot } => {
!valid_identity(slot)
+36
View File
@@ -202,3 +202,39 @@ fn deserialized_unknown_fields_versions_and_noncanonical_order_are_rejected() {
unsorted.input.plugins.push(second);
assert_eq!(unsorted.validate_canonical(), Err(PlanError::Invalid));
}
#[test]
fn ticket_grants_require_exact_world_request_and_schema_namespace() {
let mut a = artifact();
let write = PluginGrant::TicketDataWrite {
collection: "notes".into(),
schema_id: "note".into(),
schema_version: "1".into(),
};
let read = PluginGrant::TicketDataRead {
collection: "notes".into(),
schema_id: "note".into(),
schema_version: "1".into(),
};
a.requested_grants
.extend([PluginGrant::TicketRead, write.clone()]);
let mut selected = projection();
selected.grants.push(write.clone());
// Even a fabricated metadata request cannot add Ticket operations to v2.0.
assert_eq!(pin(&a, selected.clone()), Err(PlanError::Invalid));
a.world = plugin::PLUGIN_COMPONENT_TICKET_TOOL_WORLD.into();
let exact = pin(&a, selected.clone()).unwrap();
assert!(exact.projection().grants.contains(&write));
assert!(!exact.projection().grants.contains(&read)); // write is not read
selected.grants.push(read);
assert_eq!(pin(&a, selected), Err(PlanError::Invalid));
let mut selected = projection();
selected.grants.push(PluginGrant::TicketDataWrite {
collection: "notes".into(),
schema_id: "note".into(),
schema_version: "2".into(),
});
assert_eq!(pin(&a, selected), Err(PlanError::Invalid));
a.requested_grants.retain(|g| g != &write);
assert_eq!(exact.matches_artifact(&a), Err(PlanError::Invalid));
}