use super::{
PluginCapabilityShape, WitCapabilityRefError, WorkspacePathError, WorkspacePathGrant,
WorkspacePathRef, workspace::intersect_path_grants,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{Debug, Formatter};
#[derive(Clone, Default, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct PluginCapabilities {
#[serde(rename = "buffer.observe")]
pub buffer_observe: bool,
#[serde(rename = "buffer.propose_edit")]
pub buffer_propose_edit: bool,
#[serde(rename = "workspace.observe")]
pub workspace_observe: Vec<WorkspacePathGrant>,
#[serde(rename = "workspace.artifact_write")]
pub workspace_artifact_write: Vec<WorkspacePathGrant>,
#[serde(rename = "status.publish")]
pub status_publish: bool,
}
impl PluginCapabilities {
#[must_use]
pub fn intersection(&self, requested: &Self) -> Self {
CapabilitySet::from(self)
.intersection(&CapabilitySet::from(requested))
.into_capabilities()
}
#[must_use]
pub fn allows(&self, capability: PluginCapabilityRef<'_>) -> bool {
grants_allow_capability(
self.buffer_observe,
self.buffer_propose_edit,
&self.workspace_observe,
&self.workspace_artifact_write,
self.status_publish,
capability,
)
}
}
impl Debug for PluginCapabilities {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PluginCapabilities")
.field("buffer_observe", &self.buffer_observe)
.field("buffer_propose_edit", &self.buffer_propose_edit)
.field(
"workspace_observe",
&WorkspaceGrantListDebug(&self.workspace_observe),
)
.field(
"workspace_artifact_write",
&WorkspaceGrantListDebug(&self.workspace_artifact_write),
)
.field("status_publish", &self.status_publish)
.finish()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CapabilityAtom {
BufferObserve,
BufferProposeEdit,
WorkspaceObserve,
WorkspaceArtifactWrite,
StatusPublish,
}
impl CapabilityAtom {
#[must_use]
pub const fn all() -> [Self; 5] {
[
Self::BufferObserve,
Self::BufferProposeEdit,
Self::WorkspaceObserve,
Self::WorkspaceArtifactWrite,
Self::StatusPublish,
]
}
#[must_use]
pub const fn names() -> [&'static str; 5] {
[
Self::BufferObserve.as_str(),
Self::BufferProposeEdit.as_str(),
Self::WorkspaceObserve.as_str(),
Self::WorkspaceArtifactWrite.as_str(),
Self::StatusPublish.as_str(),
]
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::BufferObserve => "buffer.observe",
Self::BufferProposeEdit => "buffer.propose_edit",
Self::WorkspaceObserve => "workspace.observe",
Self::WorkspaceArtifactWrite => "workspace.artifact_write",
Self::StatusPublish => "status.publish",
}
}
#[must_use]
pub const fn shape(self) -> PluginCapabilityShape {
match self {
Self::BufferObserve => PluginCapabilityShape::BufferObserve,
Self::BufferProposeEdit => PluginCapabilityShape::BufferProposeEdit,
Self::WorkspaceObserve => PluginCapabilityShape::WorkspaceObserve,
Self::WorkspaceArtifactWrite => PluginCapabilityShape::WorkspaceArtifactWrite,
Self::StatusPublish => PluginCapabilityShape::StatusPublish,
}
}
pub fn authorization_ref(
self,
path: Option<&str>,
) -> Result<PluginCapabilityRef<'_>, WitCapabilityRefError> {
match self {
Self::BufferObserve => {
scalar_capability_ref(path, self, PluginCapabilityRef::BufferObserve)
}
Self::BufferProposeEdit => {
scalar_capability_ref(path, self, PluginCapabilityRef::BufferProposeEdit)
}
Self::WorkspaceObserve => {
let path = path.ok_or_else(|| WitCapabilityRefError::MissingPath {
capability: self.shape(),
})?;
PluginCapabilityRef::workspace_observe(path).map_err(|source| {
WitCapabilityRefError::InvalidPath {
capability: self.shape(),
source,
}
})
}
Self::WorkspaceArtifactWrite => {
let path = path.ok_or_else(|| WitCapabilityRefError::MissingPath {
capability: self.shape(),
})?;
PluginCapabilityRef::workspace_artifact_write(path).map_err(|source| {
WitCapabilityRefError::InvalidPath {
capability: self.shape(),
source,
}
})
}
Self::StatusPublish => {
scalar_capability_ref(path, self, PluginCapabilityRef::StatusPublish)
}
}
}
}
impl TryFrom<&str> for CapabilityAtom {
type Error = ();
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"buffer.observe" => Ok(Self::BufferObserve),
"buffer.propose_edit" => Ok(Self::BufferProposeEdit),
"workspace.observe" => Ok(Self::WorkspaceObserve),
"workspace.artifact_write" => Ok(Self::WorkspaceArtifactWrite),
"status.publish" => Ok(Self::StatusPublish),
_unknown => Err(()),
}
}
}
impl From<CapabilityAtom> for PluginCapabilityShape {
fn from(atom: CapabilityAtom) -> Self {
atom.shape()
}
}
const fn scalar_capability_ref<'path>(
path: Option<&'path str>,
atom: CapabilityAtom,
reference: PluginCapabilityRef<'path>,
) -> Result<PluginCapabilityRef<'path>, WitCapabilityRefError> {
if path.is_some() {
return Err(WitCapabilityRefError::UnexpectedPath {
capability: atom.shape(),
});
}
Ok(reference)
}
#[derive(Clone, Default, Eq, PartialEq)]
pub struct CapabilitySet {
buffer_observe: bool,
buffer_propose_edit: bool,
workspace_observe: Vec<WorkspacePathGrant>,
workspace_artifact_write: Vec<WorkspacePathGrant>,
status_publish: bool,
}
impl CapabilitySet {
#[must_use]
pub fn intersection(&self, requested: &Self) -> Self {
Self {
buffer_observe: self.buffer_observe && requested.buffer_observe,
buffer_propose_edit: self.buffer_propose_edit && requested.buffer_propose_edit,
workspace_observe: intersect_path_grants(
&self.workspace_observe,
&requested.workspace_observe,
),
workspace_artifact_write: intersect_path_grants(
&self.workspace_artifact_write,
&requested.workspace_artifact_write,
),
status_publish: self.status_publish && requested.status_publish,
}
}
#[must_use]
pub fn allows(&self, capability: PluginCapabilityRef<'_>) -> bool {
grants_allow_capability(
self.buffer_observe,
self.buffer_propose_edit,
&self.workspace_observe,
&self.workspace_artifact_write,
self.status_publish,
capability,
)
}
#[must_use]
pub fn is_subset_of(&self, other: &Self) -> bool {
(!self.buffer_observe || other.buffer_observe)
&& (!self.buffer_propose_edit || other.buffer_propose_edit)
&& (!self.status_publish || other.status_publish)
&& path_grants_are_subset(&self.workspace_observe, &other.workspace_observe)
&& path_grants_are_subset(
&self.workspace_artifact_write,
&other.workspace_artifact_write,
)
}
#[must_use]
pub fn into_capabilities(self) -> PluginCapabilities {
PluginCapabilities {
buffer_observe: self.buffer_observe,
buffer_propose_edit: self.buffer_propose_edit,
workspace_observe: self.workspace_observe,
workspace_artifact_write: self.workspace_artifact_write,
status_publish: self.status_publish,
}
}
}
impl Debug for CapabilitySet {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("CapabilitySet")
.field("buffer_observe", &self.buffer_observe)
.field("buffer_propose_edit", &self.buffer_propose_edit)
.field(
"workspace_observe",
&WorkspaceGrantListDebug(&self.workspace_observe),
)
.field(
"workspace_artifact_write",
&WorkspaceGrantListDebug(&self.workspace_artifact_write),
)
.field("status_publish", &self.status_publish)
.finish()
}
}
impl From<&PluginCapabilities> for CapabilitySet {
fn from(capabilities: &PluginCapabilities) -> Self {
Self {
buffer_observe: capabilities.buffer_observe,
buffer_propose_edit: capabilities.buffer_propose_edit,
workspace_observe: capabilities.workspace_observe.clone(),
workspace_artifact_write: capabilities.workspace_artifact_write.clone(),
status_publish: capabilities.status_publish,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PluginCapabilityRef<'path> {
BufferObserve,
BufferProposeEdit,
WorkspaceObserve(WorkspacePathRef<'path>),
WorkspaceArtifactWrite(WorkspacePathRef<'path>),
StatusPublish,
}
impl<'path> PluginCapabilityRef<'path> {
#[must_use]
pub const fn atom(self) -> CapabilityAtom {
match self {
Self::BufferObserve => CapabilityAtom::BufferObserve,
Self::BufferProposeEdit => CapabilityAtom::BufferProposeEdit,
Self::WorkspaceObserve(_path) => CapabilityAtom::WorkspaceObserve,
Self::WorkspaceArtifactWrite(_path) => CapabilityAtom::WorkspaceArtifactWrite,
Self::StatusPublish => CapabilityAtom::StatusPublish,
}
}
#[must_use]
pub const fn shape(self) -> PluginCapabilityShape {
self.atom().shape()
}
pub fn workspace_observe(path: &'path str) -> Result<Self, WorkspacePathError> {
Ok(Self::WorkspaceObserve(WorkspacePathRef::try_from(path)?))
}
pub fn workspace_artifact_write(path: &'path str) -> Result<Self, WorkspacePathError> {
Ok(Self::WorkspaceArtifactWrite(WorkspacePathRef::try_from(
path,
)?))
}
}
impl From<PluginCapabilityRef<'_>> for PluginCapabilityShape {
fn from(capability: PluginCapabilityRef<'_>) -> Self {
capability.shape()
}
}
fn path_grants_are_subset(left: &[WorkspacePathGrant], right: &[WorkspacePathGrant]) -> bool {
left.iter().all(|grant| {
right
.iter()
.any(|candidate| candidate.covers(grant.prefix()))
})
}
fn grants_allow_capability(
buffer_observe: bool,
buffer_propose_edit: bool,
workspace_observe: &[WorkspacePathGrant],
workspace_artifact_write: &[WorkspacePathGrant],
status_publish: bool,
capability: PluginCapabilityRef<'_>,
) -> bool {
match capability {
PluginCapabilityRef::BufferObserve => buffer_observe,
PluginCapabilityRef::BufferProposeEdit => buffer_propose_edit,
PluginCapabilityRef::WorkspaceObserve(path) => {
workspace_observe.iter().any(|grant| grant.allows(path))
}
PluginCapabilityRef::WorkspaceArtifactWrite(path) => workspace_artifact_write
.iter()
.any(|grant| grant.allows(path)),
PluginCapabilityRef::StatusPublish => status_publish,
}
}
struct WorkspaceGrantListDebug<'grants>(&'grants [WorkspacePathGrant]);
impl Debug for WorkspaceGrantListDebug<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_list()
.entries(self.0.iter().map(WorkspaceGrantDebug::from))
.finish()
}
}
struct WorkspaceGrantDebug {
all_workspace: bool,
prefix_byte_len: usize,
}
impl From<&WorkspacePathGrant> for WorkspaceGrantDebug {
fn from(grant: &WorkspacePathGrant) -> Self {
Self {
all_workspace: grant.is_all_workspace(),
prefix_byte_len: grant.prefix().len(),
}
}
}
impl Debug for WorkspaceGrantDebug {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("WorkspacePathGrant")
.field("all_workspace", &self.all_workspace)
.field("prefix_byte_len", &self.prefix_byte_len)
.finish()
}
}
#[cfg(test)]
pub mod tests {
use super::{CapabilityAtom, CapabilitySet, PluginCapabilities, PluginCapabilityRef};
use crate::plugin::{
PluginCapabilityShape, WitCapability, WitCapabilityRefError, WorkspacePathError,
WorkspacePathGrant,
policy::workspace::tests::{grant_witness_paths, path_grant_strategy},
};
use proptest::prelude::*;
use serde_json::json;
pub fn capabilities_strategy() -> impl Strategy<Value = PluginCapabilities> {
(
any::<bool>(),
any::<bool>(),
prop::collection::vec(path_grant_strategy(), 0..8),
prop::collection::vec(path_grant_strategy(), 0..8),
any::<bool>(),
)
.prop_map(
|(
buffer_observe,
buffer_propose_edit,
workspace_observe,
workspace_artifact_write,
status_publish,
)| {
PluginCapabilities {
buffer_observe,
buffer_propose_edit,
workspace_observe,
workspace_artifact_write,
status_publish,
}
},
)
}
fn scalar_subset(left: &PluginCapabilities, right: &PluginCapabilities) -> bool {
(!left.buffer_observe || right.buffer_observe)
&& (!left.buffer_propose_edit || right.buffer_propose_edit)
&& (!left.status_publish || right.status_publish)
}
fn path_grant_subset(left: &[WorkspacePathGrant], right: &[WorkspacePathGrant]) -> bool {
left.iter()
.all(|grant| public_grant_is_authorized_by(grant, right))
}
fn capability_subset(left: &PluginCapabilities, right: &PluginCapabilities) -> bool {
scalar_subset(left, right)
&& path_grant_subset(&left.workspace_observe, &right.workspace_observe)
&& path_grant_subset(
&left.workspace_artifact_write,
&right.workspace_artifact_write,
)
}
fn public_grant_is_authorized_by(
grant: &WorkspacePathGrant,
candidates: &[WorkspacePathGrant],
) -> bool {
let grants = PluginCapabilities {
workspace_observe: candidates.to_vec(),
workspace_artifact_write: candidates.to_vec(),
..PluginCapabilities::default()
};
grant_witness_paths(grant).into_iter().all(|path| {
let read = PluginCapabilityRef::workspace_observe(&path)
.expect("generated witness path should be valid");
let write = PluginCapabilityRef::workspace_artifact_write(&path)
.expect("generated witness path should be valid");
grants.allows(read) && grants.allows(write)
})
}
#[test]
fn capability_atoms_own_names_and_shapes() {
let cases = [
(
CapabilityAtom::BufferObserve,
"buffer.observe",
PluginCapabilityShape::BufferObserve,
),
(
CapabilityAtom::BufferProposeEdit,
"buffer.propose_edit",
PluginCapabilityShape::BufferProposeEdit,
),
(
CapabilityAtom::WorkspaceObserve,
"workspace.observe",
PluginCapabilityShape::WorkspaceObserve,
),
(
CapabilityAtom::WorkspaceArtifactWrite,
"workspace.artifact_write",
PluginCapabilityShape::WorkspaceArtifactWrite,
),
(
CapabilityAtom::StatusPublish,
"status.publish",
PluginCapabilityShape::StatusPublish,
),
];
assert_eq!(
CapabilityAtom::names(),
cases.map(|(_atom, name, _shape)| name)
);
for (atom, name, shape) in cases {
assert_eq!(atom.as_str(), name);
assert_eq!(atom.shape(), shape);
assert_eq!(PluginCapabilityShape::from(atom), shape);
assert_eq!(CapabilityAtom::try_from(name), Ok(atom));
}
assert!(CapabilityAtom::try_from("process.exec").is_err());
}
#[test]
fn workspace_capability_ref_debug_redacts_path_payload() {
let path = "docs/secret-capability-path.txt";
let read = PluginCapabilityRef::workspace_observe(path).expect("path should validate");
let write =
PluginCapabilityRef::workspace_artifact_write(path).expect("path should validate");
for debug in [format!("{read:?}"), format!("{write:?}")] {
assert!(debug.contains("Workspace"));
assert!(debug.contains("path_byte_len"));
assert!(!debug.contains(path));
assert!(!debug.contains("secret-capability-path"));
}
}
#[test]
fn capability_debug_redacts_workspace_grant_prefixes() {
let observe_prefix = "docs/secret-capability-grant.txt";
let artifact_prefix = "generated/secret-artifact-grant.txt";
let capabilities = PluginCapabilities {
buffer_observe: true,
workspace_observe: vec![
WorkspacePathGrant::new(observe_prefix),
WorkspacePathGrant::all_workspace(),
],
workspace_artifact_write: vec![WorkspacePathGrant::new(artifact_prefix)],
..PluginCapabilities::default()
};
for (debug, type_name) in [
(format!("{capabilities:?}"), "PluginCapabilities"),
(
format!("{:?}", CapabilitySet::from(&capabilities)),
"CapabilitySet",
),
] {
assert!(debug.contains(type_name));
assert!(debug.contains("workspace_observe"));
assert!(debug.contains("workspace_artifact_write"));
assert!(debug.contains("all_workspace"));
assert!(debug.contains("prefix_byte_len"));
assert!(!debug.contains(observe_prefix));
assert!(!debug.contains(artifact_prefix));
assert!(!debug.contains("secret-capability-grant"));
assert!(!debug.contains("secret-artifact-grant"));
}
}
#[test]
fn capability_atoms_build_authorization_refs_with_path_arity() {
assert_eq!(
CapabilityAtom::BufferObserve.authorization_ref(None),
Ok(PluginCapabilityRef::BufferObserve)
);
assert_eq!(
CapabilityAtom::BufferObserve.authorization_ref(Some("docs/input.txt")),
Err(WitCapabilityRefError::UnexpectedPath {
capability: PluginCapabilityShape::BufferObserve,
})
);
assert_eq!(
CapabilityAtom::WorkspaceObserve.authorization_ref(None),
Err(WitCapabilityRefError::MissingPath {
capability: PluginCapabilityShape::WorkspaceObserve,
})
);
assert_eq!(
CapabilityAtom::WorkspaceObserve.authorization_ref(Some("docs/input.txt")),
Ok(PluginCapabilityRef::WorkspaceObserve(
crate::plugin::WorkspacePathRef::try_from("docs/input.txt")
.expect("path should validate")
))
);
assert_eq!(
CapabilityAtom::WorkspaceArtifactWrite.authorization_ref(Some("docs/../secret.txt")),
Err(WitCapabilityRefError::InvalidPath {
capability: PluginCapabilityShape::WorkspaceArtifactWrite,
source: WorkspacePathError::DotComponent,
})
);
}
#[test]
fn capability_refs_project_atom_and_redacted_shape() {
for (reference, atom, shape) in [
(
PluginCapabilityRef::BufferObserve,
CapabilityAtom::BufferObserve,
PluginCapabilityShape::BufferObserve,
),
(
PluginCapabilityRef::BufferProposeEdit,
CapabilityAtom::BufferProposeEdit,
PluginCapabilityShape::BufferProposeEdit,
),
(
PluginCapabilityRef::workspace_observe("docs/private")
.expect("path should be valid"),
CapabilityAtom::WorkspaceObserve,
PluginCapabilityShape::WorkspaceObserve,
),
(
PluginCapabilityRef::workspace_artifact_write("docs/generated/private")
.expect("path should be valid"),
CapabilityAtom::WorkspaceArtifactWrite,
PluginCapabilityShape::WorkspaceArtifactWrite,
),
(
PluginCapabilityRef::StatusPublish,
CapabilityAtom::StatusPublish,
PluginCapabilityShape::StatusPublish,
),
] {
assert_eq!(reference.atom(), atom);
assert_eq!(reference.shape(), shape);
assert_eq!(PluginCapabilityShape::from(reference), shape);
}
}
#[test]
fn workspace_path_grants_are_prefix_scoped() {
let grants = PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs/generated")],
..PluginCapabilities::default()
};
assert!(grants.allows(
PluginCapabilityRef::workspace_observe("docs/arch.md").expect("path should be valid")
));
assert!(
grants.allows(
PluginCapabilityRef::workspace_observe("docs").expect("path should be valid")
)
);
assert!(!grants.allows(
PluginCapabilityRef::workspace_observe("src/docs.rs").expect("path should be valid")
));
assert!(
grants.allows(
PluginCapabilityRef::workspace_artifact_write("docs/generated/plugin.md")
.expect("path should be valid")
)
);
assert!(
!grants.allows(
PluginCapabilityRef::workspace_artifact_write("docs/arch.md")
.expect("path should be valid")
)
);
}
#[test]
fn scalar_capabilities_default_deny() {
let grants = PluginCapabilities::default();
assert!(!grants.allows(PluginCapabilityRef::BufferObserve));
assert!(!grants.allows(PluginCapabilityRef::BufferProposeEdit));
assert!(!grants.allows(PluginCapabilityRef::StatusPublish));
}
#[test]
fn capability_json_uses_documented_names() {
let grants = PluginCapabilities {
buffer_observe: true,
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
};
let value = serde_json::to_value(grants).expect("capabilities should serialize");
assert_eq!(
value,
json!({
"buffer.observe": true,
"buffer.propose_edit": false,
"workspace.observe": [],
"workspace.artifact_write": [{"prefix": "docs"}],
"status.publish": false,
})
);
}
#[test]
fn capability_json_rejects_unknown_fields_and_invalid_grants() {
let unknown_field = json!({
"buffer.observe": false,
"buffer.propose_edit": false,
"workspace.observe": [],
"workspace.artifact_write": [],
"status.publish": false,
"process.exec": true,
});
let invalid_grant = json!({
"workspace.observe": [{"prefix": "docs/../secrets"}],
});
let missing_prefix_grant = json!({
"workspace.observe": [{}],
});
let implicit_all_workspace_grant = json!({
"workspace.observe": [{"prefix": ""}],
});
let old_names = json!({
"buffer.read": true,
"buffer.edit": true,
"workspace.read": [],
"workspace.write": [],
"status.write": true,
});
assert!(serde_json::from_value::<PluginCapabilities>(unknown_field).is_err());
assert!(serde_json::from_value::<PluginCapabilities>(invalid_grant).is_err());
assert!(serde_json::from_value::<PluginCapabilities>(missing_prefix_grant).is_err());
assert!(
serde_json::from_value::<PluginCapabilities>(implicit_all_workspace_grant).is_err()
);
assert!(serde_json::from_value::<PluginCapabilities>(old_names).is_err());
}
#[test]
fn wit_capability_names_match_json_capability_names() {
let grants = PluginCapabilities::default();
let value = serde_json::to_value(grants).expect("capabilities should serialize");
let object = value.as_object().expect("capabilities should be an object");
for capability in WitCapability::all() {
assert!(object.contains_key(capability.as_str()));
}
}
proptest! {
#[test]
fn capability_intersection_is_subset_of_both_inputs(
configured in capabilities_strategy(),
requested in capabilities_strategy(),
) {
let intersection = configured.intersection(&requested);
prop_assert!(capability_subset(&intersection, &configured));
prop_assert!(capability_subset(&intersection, &requested));
let intersection_set = CapabilitySet::from(&intersection);
prop_assert!(intersection_set.is_subset_of(&CapabilitySet::from(&configured)));
prop_assert!(intersection_set.is_subset_of(&CapabilitySet::from(&requested)));
}
#[test]
fn capability_intersection_authorizes_only_public_effective_grants(
configured in capabilities_strategy(),
requested in capabilities_strategy(),
) {
let intersection = configured.intersection(&requested);
for grant in &intersection.workspace_observe {
for path in grant_witness_paths(grant) {
let reference = PluginCapabilityRef::workspace_observe(&path)
.expect("generated witness path should be valid");
prop_assert!(intersection.allows(reference));
prop_assert!(configured.allows(reference));
prop_assert!(requested.allows(reference));
}
}
for grant in &intersection.workspace_artifact_write {
for path in grant_witness_paths(grant) {
let reference = PluginCapabilityRef::workspace_artifact_write(&path)
.expect("generated witness path should be valid");
prop_assert!(intersection.allows(reference));
prop_assert!(configured.allows(reference));
prop_assert!(requested.allows(reference));
}
}
}
}
}