Skip to content

Commit c1cad6c

Browse files
authored
policy: add the policy crate and it's tests into the repo (#48)
* policy: add the policy crate and it's tests into the repo Signed-off-by: Jiaxiao Zhou <duibao55328@gmail.com>
1 parent 320b27e commit c1cad6c

20 files changed

+1513
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ mcp-sdk = "0.0.3"
2121
mcp-server = { path = "crates/mcp-server" }
2222
oci-client = "0.15"
2323
oci-wasm = "0.3"
24-
policy-mcp = { git = "https://github.com/semcp/policy-mcp-rs", branch = "main" }
24+
policy = { path = "crates/policy" }
2525
reqwest = "0.12"
2626
rmcp = "0.2"
2727
serde = "1.0"

crates/policy/Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "policy"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license.workspace = true
6+
7+
8+
[dependencies]
9+
serde = { version = "1.0", features = ["derive"] }
10+
anyhow = "1.0"
11+
serde_yaml = "0.9.33"
12+
13+
[dev-dependencies]
14+
tempfile = "3.8"

crates/policy/src/lib.rs

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
//! Capability Policy for Local MCP Servers
2+
//!
3+
//! Parser for MCP server policy files. Supports storage, network, environment
4+
//! and runtime permissions.
5+
6+
use anyhow::{bail, Context, Result};
7+
use serde::{Deserialize, Serialize};
8+
9+
pub mod parser;
10+
pub mod types;
11+
12+
pub use parser::PolicyParser;
13+
pub use types::*;
14+
15+
/// Policy document structure
16+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
17+
pub struct PolicyDocument {
18+
/// Policy format version
19+
pub version: String,
20+
21+
/// Human-readable description of the policy
22+
pub description: Option<String>,
23+
24+
/// Permission definitions
25+
pub permissions: Permissions,
26+
}
27+
28+
impl PolicyDocument {
29+
/// Validate the policy document
30+
pub fn validate(&self) -> Result<()> {
31+
// Only supporting v1.x for now - will add v2 when we know what it looks like
32+
if !self.version.starts_with("1.") {
33+
bail!("Unsupported version: {}", self.version);
34+
}
35+
self.permissions
36+
.validate()
37+
.context("Permission validation failed")?;
38+
Ok(())
39+
}
40+
41+
/// Create a new policy document with default permissions
42+
pub fn new(version: impl Into<String>, description: Option<String>) -> Self {
43+
Self {
44+
version: version.into(),
45+
description,
46+
..Default::default()
47+
}
48+
}
49+
}
50+
51+
pub type PolicyResult<T> = Result<T>;
52+
53+
#[cfg(test)]
54+
mod tests {
55+
use super::*;
56+
57+
#[test]
58+
fn test_policy_validation() {
59+
let policy = PolicyDocument {
60+
version: "1.0".to_string(),
61+
description: Some("Test policy".to_string()),
62+
permissions: Permissions::default(),
63+
};
64+
65+
assert!(policy.validate().is_ok());
66+
}
67+
68+
#[test]
69+
fn test_policy_new_constructor() {
70+
let policy = PolicyDocument::new("1.0", Some("Test policy".to_string()));
71+
assert_eq!(policy.version, "1.0");
72+
assert_eq!(policy.description, Some("Test policy".to_string()));
73+
assert!(policy.validate().is_ok());
74+
75+
let policy2 = PolicyDocument::new("1.1".to_string(), None);
76+
assert_eq!(policy2.version, "1.1");
77+
assert_eq!(policy2.description, None);
78+
}
79+
80+
#[test]
81+
fn test_invalid_version() {
82+
let policy = PolicyDocument {
83+
version: "2.0".to_string(),
84+
description: None,
85+
permissions: Permissions::default(),
86+
};
87+
88+
let result = policy.validate();
89+
assert!(result.is_err());
90+
let error_message = result.unwrap_err().to_string();
91+
assert!(error_message.contains("Unsupported version: 2.0"));
92+
}
93+
94+
#[test]
95+
fn test_parse_docker_yaml() {
96+
let policy = PolicyParser::parse_file("testdata/docker.yaml").unwrap();
97+
policy.validate().unwrap();
98+
99+
assert_eq!(policy.version, "1.0");
100+
assert_eq!(
101+
policy.description,
102+
Some("Permission policy for docker container".to_string())
103+
);
104+
105+
let storage = policy.permissions.storage.as_ref().unwrap();
106+
let storage_allow = storage.allow.as_ref().unwrap();
107+
assert_eq!(storage_allow.len(), 2);
108+
assert_eq!(storage_allow[0].uri, "fs://work/agent/**");
109+
assert_eq!(
110+
storage_allow[0].access,
111+
vec![AccessType::Read, AccessType::Write]
112+
);
113+
114+
assert_eq!(storage_allow[1].uri, "fs://work/agent/config.yaml");
115+
assert_eq!(storage_allow[1].access, vec![AccessType::Read]);
116+
117+
let network = policy.permissions.network.as_ref().unwrap();
118+
let network_allow = network.allow.as_ref().unwrap();
119+
assert_eq!(network_allow.len(), 3);
120+
121+
match &network_allow[0] {
122+
NetworkPermission::Host(host) => assert_eq!(host.host, "api.openai.com"),
123+
_ => panic!("Expected host permission"),
124+
}
125+
126+
match &network_allow[1] {
127+
NetworkPermission::Host(host) => assert_eq!(host.host, "*.internal.myorg.com"),
128+
_ => panic!("Expected host permission"),
129+
}
130+
131+
match &network_allow[2] {
132+
NetworkPermission::Cidr(cidr) => assert_eq!(cidr.cidr, "10.0.0.0/8"),
133+
_ => panic!("Expected CIDR permission"),
134+
}
135+
136+
let env = policy.permissions.environment.as_ref().unwrap();
137+
let env_allow = env.allow.as_ref().unwrap();
138+
assert_eq!(env_allow.len(), 2);
139+
assert_eq!(env_allow[0].key, "PATH");
140+
assert_eq!(env_allow[1].key, "HOME");
141+
142+
let runtime = policy.permissions.runtime.as_ref().unwrap();
143+
let docker_runtime = runtime.docker.as_ref().unwrap();
144+
let docker_security = docker_runtime.security.as_ref().unwrap();
145+
146+
assert_eq!(docker_security.privileged, Some(false));
147+
assert_eq!(docker_security.no_new_privileges, Some(true));
148+
149+
let capabilities = docker_security.capabilities.as_ref().unwrap();
150+
assert_eq!(capabilities.drop, Some(vec![CapabilityAction::All]));
151+
assert_eq!(
152+
capabilities.add,
153+
Some(vec![CapabilityAction::NetBindService])
154+
);
155+
156+
assert!(
157+
runtime.hyperlight.is_none(),
158+
"Hyperlight should be None since it's just a comment"
159+
);
160+
}
161+
162+
#[test]
163+
fn test_round_trip_docker_yaml() {
164+
let original_policy = PolicyParser::parse_file("testdata/docker.yaml").unwrap();
165+
let yaml_string = PolicyParser::to_yaml(&original_policy).unwrap();
166+
let reparsed_policy = PolicyParser::parse_str(&yaml_string).unwrap();
167+
168+
assert_eq!(original_policy, reparsed_policy);
169+
}
170+
}

0 commit comments

Comments
 (0)