From 830b3cbd9d43ef2d28c0371eb7b62a8d23724cbc Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 14:36:46 +0000 Subject: [PATCH 1/6] Initial commit: Rust SDK for mcpd plugins - Trait-based plugin architecture - Support for request/response flows - Static binary builds with musl - Examples: simple, auth, rate-limit - CI/CD with GitHub Actions - justfile for task automation - Comprehensive documentation --- .cargo-deny.toml | 51 ++++ .github/workflows/build.yaml | 41 +++ .github/workflows/lint.yaml | 37 +++ .github/workflows/security.yaml | 31 +++ .github/workflows/test.yaml | 35 +++ .gitignore | 22 ++ ARCHITECTURE.md | 249 +++++++++++++++++ CONTRIBUTING.md | 125 +++++++++ Cargo.toml | 61 +++++ Makefile | 86 ++++++ README.md | 411 +++++++++++++++++++++++++++++ build.rs | 43 +++ examples/auth_plugin/main.rs | 144 ++++++++++ examples/rate_limit_plugin/main.rs | 206 +++++++++++++++ examples/simple_plugin/main.rs | 75 ++++++ justfile | 92 +++++++ src/constants.rs | 7 + src/error.rs | 46 ++++ src/lib.rs | 167 ++++++++++++ src/plugin.rs | 190 +++++++++++++ src/server.rs | 201 ++++++++++++++ 21 files changed, 2320 insertions(+) create mode 100644 .cargo-deny.toml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/security.yaml create mode 100644 .github/workflows/test.yaml create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 README.md create mode 100644 build.rs create mode 100644 examples/auth_plugin/main.rs create mode 100644 examples/rate_limit_plugin/main.rs create mode 100644 examples/simple_plugin/main.rs create mode 100644 justfile create mode 100644 src/constants.rs create mode 100644 src/error.rs create mode 100644 src/lib.rs create mode 100644 src/plugin.rs create mode 100644 src/server.rs diff --git a/.cargo-deny.toml b/.cargo-deny.toml new file mode 100644 index 0000000..b2cc0d0 --- /dev/null +++ b/.cargo-deny.toml @@ -0,0 +1,51 @@ +# cargo-deny configuration +# https://embarkstudios.github.io/cargo-deny/ + +[advisories] +# Deny crates with security vulnerabilities. +vulnerability = "deny" +# Warn about unmaintained crates. +unmaintained = "warn" +# Warn about yanked crates. +yanked = "warn" +# Ignore advisories for crates that are not used in production. +ignore = [] + +[licenses] +# Allow Apache-2.0 and MIT licenses. +allow = [ + "Apache-2.0", + "MIT", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", +] +# Deny AGPLv3 and GPLv3 (copyleft licenses). +deny = [ + "AGPL-3.0", + "GPL-3.0", +] +# Confidence threshold for license detection. +confidence-threshold = 0.8 +# Use exceptions for specific crates if needed. +exceptions = [] + +[bans] +# Warn about multiple versions of the same crate. +multiple-versions = "warn" +# Deny specific crates. +deny = [] +# Skip certain crates from multiple version detection. +skip = [] +# Skip tree for certain crates. +skip-tree = [] + +[sources] +# Deny crates from unknown registries. +unknown-registry = "deny" +# Deny crates from git repositories (prefer crates.io). +unknown-git = "deny" +# Allow crates from crates.io. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# Allow specific git sources if needed. +allow-git = [] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..4b0a8b3 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,41 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + examples: + name: Build Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Build all examples + run: cargo build --examples --release + - name: List built examples + run: ls -lh target/release/examples/ + + static-binary: + name: Build Static Binary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. + with: + targets: x86_64-unknown-linux-musl + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Build static binary + run: cargo build --release --target x86_64-unknown-linux-musl + - name: Verify static library + run: | + file target/x86_64-unknown-linux-musl/release/libmcpd_plugins_sdk.rlib + echo "Static library built successfully" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..7fe9b3d --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,37 @@ +name: Lint + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + +jobs: + rustfmt: + name: Format Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. + with: + components: rustfmt + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Check formatting + run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. + with: + components: clippy + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..1b026e7 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,31 @@ +name: Security + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # Run security checks daily at 00:00 UTC. + - cron: '0 0 * * *' + +jobs: + deny: + name: Cargo Deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + arguments: --all-features + + audit: + name: Security Audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - name: Install protoc + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - uses: actions-rust-lang/audit@v1 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..b407bb5 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,35 @@ +name: Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + test: + name: Test Suite + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + rust: [stable, 1.75.0] + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. + with: + toolchain: ${{ matrix.rust }} + - name: Install protoc (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y protobuf-compiler + - name: Install protoc (macOS) + if: runner.os == 'macOS' + run: brew install protobuf + - name: Run tests + run: cargo test --all-features + - name: Run doc tests + run: cargo test --doc --all-features diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec694dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Rust build artifacts. +/target/ +Cargo.lock + +# Generated code. +/src/generated/ + +# Downloaded proto files. +/proto/ + +# IDE files. +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS. +.DS_Store + +# Debug files. +*.pdb diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..f47cc8c --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,249 @@ +# Architecture Documentation + +## Overview + +The `mcpd-plugins-sdk-rust` provides a Rust implementation of the `mcpd` plugin SDK, following the same patterns established in the [Go](https://github.com/mozilla-ai/mcpd-plugins-sdk-go), [Python](https://github.com/mozilla-ai/mcpd-plugins-sdk-python), and [.NET](https://github.com/mozilla-ai/mcpd-plugins-sdk-dotnet) SDKs while adhering to Rust idioms and best practices. + +## Design Principles + +### 1. Trait-Based Architecture + +Unlike the other SDKs which use class inheritance, this Rust SDK uses a trait-based approach: + +- **`Plugin` trait**: Core trait that plugins implement +- **Default implementations**: All methods have sensible defaults +- **Zero-cost abstractions**: Trait dispatch optimized by the compiler + +### 2. Async-First Design + +Following Rust async best practices: + +- All plugin methods are `async` using `#[tonic::async_trait]` +- Built on Tokio for high-performance async I/O +- Graceful shutdown with signal handling + +### 3. Type Safety + +- Protocol buffers generated at build time +- Strong typing throughout the API +- Custom error types with `thiserror` + + +## Module Structure + +``` +src/ +├── generated/ - Generated protobuf code +├── constants.rs - Flow constants +├── error.rs - Error types +├── lib.rs - Public API exports and documentation +├── plugin.rs - Plugin trait and adapter +└── server.rs - Server lifecycle management +``` + +### generated/ + +Auto-generated protobuf and gRPC code: +- Downloaded from mcpd-proto repository at build time +- Compiled from [`plugin.proto`](https://github.com/mozilla-ai/mcpd-proto/blob/main/plugins/v1/plugin.proto) using tonic-build +- Contains message types and service traits + +### constants.rs + +Flow constants for plugin capabilities: +- `FLOW_REQUEST` - Process incoming HTTP requests +- `FLOW_RESPONSE` - Process outgoing HTTP responses + +### error.rs + +Custom error types: +- `PluginError` enum with variants for different error types +- Conversion to gRPC `Status` codes +- Integration with `std::error::Error` + +### lib.rs + +The main entry point that: +- Re-exports public API types +- Provides comprehensive documentation +- Includes generated protobuf module + +### plugin.rs + +Defines the core `Plugin` trait with: +- 8 methods: metadata, capabilities, lifecycle, health, and request handling +- Default implementations for all methods +- `PluginAdapter` to bridge between trait and generated gRPC service + +### server.rs + +Handles server lifecycle: +- Command-line argument parsing with `clap` +- Unix socket and TCP support +- Graceful shutdown with signal handling +- Automatic socket cleanup + +## Key Design Decisions + +### 1. Raw String Literals for Reserved Keywords + +The protobuf field `continue` is a Rust keyword, so we use `r#continue`: + +```rust +HttpResponse { + r#continue: true, + ..Default::default() +} +``` + +### 2. Build-Time Proto Download + +The `build.rs` script: +- Downloads proto files from `mcpd-proto` [repository](https://github.com/mozilla-ai/mcpd-proto) +- Generates Rust code with `tonic-build` +- Ensures reproducible builds + +### 3. Cross-Platform Support + +- Unix sockets on Linux/macOS (preferred) +- TCP on all platforms (including Windows) +- Conditional compilation with `#[cfg(unix)]` + +### 4. Zero-Copy Where Possible + +- References instead of clones +- `Arc` for shared ownership +- Borrowed data in function parameters + +## Comparison with Other SDKs + +### Go SDK + +https://github.com/mozilla-ai/mcpd-plugins-sdk-go + +- **Go**: Struct embedding for composition +- **Rust**: Trait implementation + +### Python SDK + +https://github.com/mozilla-ai/mcpd-plugins-sdk-python + +- **Python**: Class inheritance with `async/await` +- **Rust**: Trait implementation with `#[tonic::async_trait]` + +### .NET SDK + +https://github.com/mozilla-ai/mcpd-plugins-sdk-dotnet + +- **C#**: Class inheritance with `Task` +- **Rust**: Trait implementation with `async fn` + +## Performance Characteristics + +### Compilation +- Proto files downloaded and compiled once +- Generated code cached in `src/generated/` +- Incremental compilation supported + +### Runtime +- Zero-cost abstractions with trait dispatch +- Efficient async I/O with Tokio +- Minimal allocations with borrowing + +### Binary Size +- Release builds with LTO: ~5-10 MB +- Static linking with musl: ~3-5 MB +- Strip symbols for production: <3 MB + +## Idioms and Patterns + +### 1. Builder Pattern + +Configuration uses the options pattern via `PluginConfig`: + +```rust +async fn configure(&self, request: Request) -> Result, Status> { + let config = request.into_inner(); + // Use config.custom_config and config.telemetry +} +``` + +### 2. Error Handling + +Use `?` operator for error propagation: + +```rust +async fn handle_request(&self, request: Request) + -> Result, Status> { + let req = request.into_inner(); + // Errors automatically converted via From trait. + self.validate(&req)?; + Ok(Response::new(HttpResponse { + r#continue: true, + ..Default::default() + })) +} +``` + +### 3. Shared State + +Use `Arc` and async locks for shared state: + +```rust +struct MyPlugin { + state: Arc>>, +} + +async fn handle_request(&self, request: Request) + -> Result, Status> { + let state = self.state.read().await; + // Use state +} +``` + +## Testing Strategy + +### Unit Tests +- Test individual components in isolation +- Mock gRPC context +- Use `tokio::test` for async tests + +### Integration Tests +- Test full plugin lifecycle +- Use real gRPC connections +- Example plugins serve as integration tests + +### Example Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_plugin_metadata() { + let plugin = MyPlugin::new(); + let response = plugin.get_metadata(Request::new(())).await.unwrap(); + assert_eq!(response.into_inner().name, "my-plugin"); + } +} +``` + +## Backward Compatibility + +Following semantic versioning: +- Patch versions: Bug fixes +- Minor versions: New features, backward compatible +- Major versions: Breaking changes + +Proto version updates handled via environment variable: +```bash +PROTO_VERSION=v2 cargo build +``` + +## References + +- [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +- [Tonic Documentation](https://docs.rs/tonic/) +- [Tokio Best Practices](https://tokio.rs/tokio/tutorial) +- [mcpd Documentation](https://mozilla-ai.github.io/mcpd/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..818aaa5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,125 @@ +# Contributing to mcpd-plugins-sdk-rust + +Thank you for your interest in contributing to the Rust SDK for mcpd plugins! + +## Getting Started + +1. Fork the repository +2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/mcpd-plugins-sdk-rust` +3. Create a feature branch: `git checkout -b my-feature` +4. Make your changes +5. Run tests and linting: `make test lint` +6. Commit your changes +7. Push to your fork: `git push origin my-feature` +8. Open a pull request + +## Development Setup + +### Prerequisites + +- Rust 1.75 or later +- `cargo` and `rustup` + +### Install Development Tools + +```bash +make install-tools +``` + +This installs: +- `clippy` - Rust linter +- `rustfmt` - Code formatter + +### Building + +```bash +# Build the library. +make build + +# Build examples. +make examples + +# Build everything. +make all +``` + +### Testing + +```bash +# Run all tests. +make test + +# Run specific test. +cargo test test_name +``` + +### Linting and Formatting + +We use `clippy` and `rustfmt` to maintain code quality: + +```bash +# Run linter. +make lint + +# Format code. +make fmt + +# Both are run automatically by `make all`. +``` + +## Code Style + +- Follow the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/) +- Use `cargo fmt` to format your code +- Address all `clippy` warnings +- Add documentation comments for public items +- End single-line comments with a period + +### Documentation + +All public items must have documentation comments: + +```rust +/// Returns plugin metadata. +/// +/// This method should be overridden to provide plugin identification. +async fn get_metadata(&self, _request: Request<()>) -> Result, Status> { + // Implementation. +} +``` + +## Testing Guidelines + +- Write unit tests for new functionality +- Add integration tests for examples +- Ensure all tests pass before submitting PR +- Use descriptive test names + +## Pull Request Process + +1. Update documentation if you've changed APIs +2. Add tests for new functionality +3. Ensure `make all` passes without errors +4. Update CHANGELOG.md if applicable +5. Write clear commit messages +6. Reference any related issues in the PR description + +## Commit Messages + +- Use clear, descriptive commit messages +- Reference issues: "Fix #123: Description" +- Use present tense: "Add feature" not "Added feature" + +## Code Review + +- All PRs require review before merging +- Address review feedback promptly +- Keep PRs focused on a single feature/fix + +## Questions? + +Open an issue for questions or discussion. + +## License + +By contributing, you agree that your contributions will be licensed under the Apache License 2.0. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a321079 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "mcpd-plugins-sdk" +version = "0.0.2" +edition = "2021" +rust-version = "1.75" +authors = ["Mozilla AI"] +license = "Apache-2.0" +description = "Rust SDK for building mcpd plugins" +repository = "https://github.com/mozilla-ai/mcpd-plugins-sdk-rust" +keywords = ["mcpd", "plugins", "grpc", "middleware"] +categories = ["web-programming", "api-bindings"] + +[dependencies] +# gRPC and async runtime. +tonic = "0.12" +tokio = { version = "1.35", features = ["full"] } +tokio-stream = "0.1" + +# Protocol buffers. +prost = "0.13" +prost-types = "0.13" + +# Error handling. +thiserror = "2.0" +anyhow = "1.0" + +# Serialization. +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Logging. +tracing = "0.1" + +# CLI argument parsing. +clap = { version = "4.5", features = ["derive"] } + +# HTTP types. +http = "1.0" + +[build-dependencies] +tonic-build = "0.12" +ureq = "2.10" + +[dev-dependencies] +tokio-test = "0.4" +tracing-subscriber = "0.3" + +[features] +default = [] + +[[example]] +name = "simple_plugin" +path = "examples/simple_plugin/main.rs" + +[[example]] +name = "auth_plugin" +path = "examples/auth_plugin/main.rs" + +[[example]] +name = "rate_limit_plugin" +path = "examples/rate_limit_plugin/main.rs" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a15060f --- /dev/null +++ b/Makefile @@ -0,0 +1,86 @@ +# Makefile for mcpd-plugins-sdk-rust. +# Note: justfile is the recommended interface for development. + +.PHONY: help +help: + @echo "Available targets:" + @echo " all - Run fmt, lint, build, and test" + @echo " build - Build the library" + @echo " release - Build release version" + @echo " test - Run tests" + @echo " clean - Clean build artifacts" + @echo " examples - Build all examples" + @echo " lint - Run clippy linter" + @echo " fmt - Format code" + @echo " check - Fast compile check" + @echo " install-tools - Install clippy and rustfmt" + @echo " run-simple - Run simple_plugin example" + @echo " run-auth - Run auth_plugin example" + @echo " run-ratelimit - Run rate_limit_plugin example" + @echo " doc - Build and open documentation" + @echo " coverage - Generate test coverage" + @echo " static - Build static binary (Linux musl)" + +.PHONY: all +all: fmt lint build test + +.PHONY: build +build: + cargo build + +.PHONY: release +release: + cargo build --release + +.PHONY: test +test: + cargo test + +.PHONY: clean +clean: + cargo clean + rm -rf proto/ src/generated/ + +.PHONY: examples +examples: + cargo build --examples + +.PHONY: lint +lint: + cargo clippy -- -D warnings + +.PHONY: fmt +fmt: + cargo fmt + +.PHONY: check +check: + cargo check + +.PHONY: install-tools +install-tools: + rustup component add clippy rustfmt + +.PHONY: run-simple +run-simple: + cargo run --example simple_plugin -- --address /tmp/simple.sock + +.PHONY: run-auth +run-auth: + cargo run --example auth_plugin -- --address /tmp/auth.sock + +.PHONY: run-ratelimit +run-ratelimit: + cargo run --example rate_limit_plugin -- --address /tmp/ratelimit.sock + +.PHONY: doc +doc: + cargo doc --no-deps --open + +.PHONY: coverage +coverage: + cargo tarpaulin --out Html --output-dir coverage/ + +.PHONY: static +static: + cargo build --release --target x86_64-unknown-linux-musl diff --git a/README.md b/README.md new file mode 100644 index 0000000..19192fa --- /dev/null +++ b/README.md @@ -0,0 +1,411 @@ +# mcpd-plugins-sdk-rust + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) +[![Rust Version](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org) + +Rust SDK for building [mcpd](https://github.com/mozilla-ai/mcpd) plugins. + +This SDK provides a simple, trait-based API for creating gRPC plugins that intercept and transform HTTP requests and responses in the mcpd middleware pipeline. + +## Features + +- **Simple trait-based API**: Implement the `Plugin` trait with only the methods you need +- **Async/await support**: Built on Tokio and Tonic for high-performance async I/O +- **Automatic server setup**: `serve()` function handles all boilerplate +- **Cross-platform**: Unix sockets (Linux/macOS) and TCP support +- **Type-safe**: Protocol buffers for serialization +- **Graceful shutdown**: SIGINT/SIGTERM handling with cleanup + +## Quick Start + +### Installation + +Add this to your `Cargo.toml`: + +```toml +[dependencies] +mcpd-plugins-sdk = "0.0" # any 0.0.x +tokio = { version = "1", features = ["full"] } +tonic = "0.12" +``` + +### Minimal Plugin Example + +Create a simple plugin that adds a custom header: + +```rust +use mcpd_plugins_sdk::{ + Plugin, serve, Metadata, Capabilities, HttpRequest, HttpResponse, + FLOW_REQUEST, +}; +use tonic::{Request, Response, Status}; + +struct MyPlugin; + +#[tonic::async_trait] +impl Plugin for MyPlugin { + async fn get_metadata( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Metadata { + name: "my-plugin".to_string(), + version: "1.0.0".to_string(), + description: "My custom plugin".to_string(), + ..Default::default() + })) + } + + async fn get_capabilities( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Capabilities { + flows: vec![FLOW_REQUEST as i32], + })) + } + + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + let mut req = request.into_inner(); + + // Add custom header. + req.headers.insert("X-My-Plugin".to_string(), "processed".to_string()); + + Ok(Response::new(HttpResponse { + r#continue: true, + modified_request: Some(req), + ..Default::default() + })) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + serve(MyPlugin, None).await?; + Ok(()) +} +``` + +### Building and Running + +**Important**: When deploying plugins to run with mcpd (especially in containers), you should build **static, self-contained binaries** that don't depend on system libraries. The mcpd container won't have Rust runtime libraries available. + +#### Recommended: Static Binary (Linux) + +For production use with mcpd, build a fully static binary: + +```bash +# Install musl target (one-time setup). +rustup target add x86_64-unknown-linux-musl + +# Build static binary. +cargo build --release --target x86_64-unknown-linux-musl + +# The resulting binary is completely self-contained and ready for mcpd. +./target/x86_64-unknown-linux-musl/release/my-plugin --address /tmp/my-plugin.sock +``` + +#### Development Build + +For local development and testing: + +```bash +# Build the plugin. +cargo build --release + +# Run with Unix socket (Linux/macOS). +./target/release/my-plugin --address /tmp/my-plugin.sock --network unix + +# Run with TCP (any platform). +./target/release/my-plugin --address localhost:50051 --network tcp +``` + +## Core Concepts + +### Plugin Trait + +The `Plugin` trait defines the interface for all plugins. All methods have default implementations, so you only need to override what you need: + +```rust +#[tonic::async_trait] +pub trait Plugin: Send + Sync + 'static { + // Identity methods. + async fn get_metadata(&self, _request: Request<()>) -> Result, Status>; + async fn get_capabilities(&self, _request: Request<()>) -> Result, Status>; + + // Lifecycle methods. + async fn configure(&self, _request: Request) -> Result, Status>; + async fn stop(&self, _request: Request<()>) -> Result, Status>; + + // Health checks. + async fn check_health(&self, _request: Request<()>) -> Result, Status>; + async fn check_ready(&self, _request: Request<()>) -> Result, Status>; + + // Request/response handling. + async fn handle_request(&self, request: Request) -> Result, Status>; + async fn handle_response(&self, response: Request) -> Result, Status>; +} +``` + +### Processing Flows + +Plugins can participate in two processing flows: + +- **`FLOW_REQUEST`**: Process incoming HTTP requests before they reach the upstream server +- **`FLOW_RESPONSE`**: Process outgoing HTTP responses before they return to the client + +Declare which flows your plugin supports in the `get_capabilities()` method: + +```rust +async fn get_capabilities(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(Capabilities { + flows: vec![FLOW_REQUEST as i32, FLOW_RESPONSE as i32], // Support both flows. + })) +} +``` + +### Request Handling Patterns + +The `handle_request()` method can respond in three ways: + +#### 1. Pass Through (Unchanged) + +```rust +async fn handle_request(&self, request: Request) -> Result, Status> { + Ok(Response::new(HttpResponse { + r#continue: true, + ..Default::default() + })) +} +``` + +#### 2. Transform Request + +```rust +async fn handle_request(&self, request: Request) -> Result, Status> { + let mut req = request.into_inner(); + + // Modify the request. + req.headers.insert("X-Custom".to_string(), "value".to_string()); + + Ok(Response::new(HttpResponse { + r#continue: true, + modified_request: Some(req), + ..Default::default() + })) +} +``` + +#### 3. Short-Circuit (Return Response) + +```rust +async fn handle_request(&self, request: Request) -> Result, Status> { + // Return error response directly. + Ok(Response::new(HttpResponse { + r#continue: false, + status_code: 401, + body: b"Unauthorized".to_vec(), + ..Default::default() + })) +} +``` + +## Examples + +The SDK includes three complete example plugins: + +### 1. Simple Plugin + +Adds custom headers to all requests. + +```bash +cargo run --example simple_plugin -- --address /tmp/simple.sock +``` + +[View source](examples/simple_plugin/main.rs) + +### 2. Auth Plugin + +Validates Bearer tokens and returns 401 for unauthorized requests. + +```bash +cargo run --example auth_plugin -- --address /tmp/auth.sock +``` + +[View source](examples/auth_plugin/main.rs) + +### 3. Rate Limit Plugin + +Implements token bucket rate limiting per client IP address. + +```bash +cargo run --example rate_limit_plugin -- --address /tmp/ratelimit.sock +``` + +[View source](examples/rate_limit_plugin/main.rs) + +## Building for Production + +### Why Static Binaries? + +mcpd runs plugins as separate processes and may run in containerized environments that don't have Rust runtime libraries. **Always use static binaries for production deployments.** + +### Static Binary Build (Recommended) + +```bash +# Install musl target (one-time setup). +rustup target add x86_64-unknown-linux-musl + +# Build static binary. +cargo build --release --target x86_64-unknown-linux-musl + +# The resulting binary is completely self-contained. +ls -lh target/x86_64-unknown-linux-musl/release/my-plugin +``` + +### Cross-Compilation + +Use [cross](https://github.com/cross-rs/cross) for easy cross-compilation to different platforms: + +```bash +# Install cross (one-time setup). +cargo install cross + +# Build for different platforms. +cross build --release --target x86_64-unknown-linux-musl # Linux x86_64 (static) +cross build --release --target aarch64-unknown-linux-musl # Linux ARM64 (static) +cross build --release --target x86_64-apple-darwin # macOS x86_64 +cross build --release --target aarch64-apple-darwin # macOS ARM64 (Apple Silicon) +``` + +### Binary Size Optimization + +Add this to your plugin's `Cargo.toml` for smaller binaries (typically 3-5 MB): + +```toml +[profile.release] +opt-level = "z" # Optimize for size. +lto = true # Enable link-time optimization. +codegen-units = 1 # Better optimization, slower compile. +strip = true # Strip symbols. +panic = "abort" # Smaller panic handler. +``` + +### Deployment Checklist + +- ✅ Build with `--target x86_64-unknown-linux-musl` for static linking +- ✅ Test the binary runs without any dynamic library dependencies: `ldd my-plugin` (should show "not a dynamic executable") +- ✅ Verify the binary is executable and runs with `--help` +- ✅ Deploy to mcpd plugin directory with correct permissions + +## Configuration + +Plugins receive configuration via the `configure()` method: + +```rust +async fn configure(&self, request: Request) -> Result, Status> { + let config = request.into_inner(); + + // Access custom configuration. + if let Some(value) = config.custom_config.get("my_setting") { + // Use configuration value. + } + + // Access telemetry configuration. + if let Some(telemetry) = config.telemetry { + // Setup OpenTelemetry with provided settings. + } + + Ok(Response::new(())) +} +``` + +Configuration is provided by mcpd from YAML files: + +```yaml +plugins: + my-plugin: + custom_config: + my_setting: "value" + max_requests: "100" +``` + +## Error Handling + +The SDK provides a `PluginError` type for error handling: + +```rust +use mcpd_plugins_sdk::PluginError; + +fn validate_config(value: &str) -> Result { + value.parse().map_err(|_| { + PluginError::Configuration(format!("Invalid number: {}", value)) + }) +} +``` + +## Testing + +### Unit Tests + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_handle_request() { + let plugin = MyPlugin::new(); + let request = Request::new(HttpRequest { + method: "GET".to_string(), + path: "/test".to_string(), + ..Default::default() + }); + + let response = plugin.handle_request(request).await.unwrap(); + assert_eq!(response.into_inner().r#continue, true); + } +} +``` + +### Integration Tests + +See the [examples](examples/) directory for complete integration test patterns. + +## Protocol Buffers + +The SDK automatically downloads and compiles protocol buffers from the [mcpd-proto](https://github.com/mozilla-ai/mcpd-proto) repository during the build process. + +To use a specific proto version: + +```bash +PROTO_VERSION=v0.0.3 cargo build +``` + +## Rust Version Policy + +This crate requires Rust 1.75 or later. We follow a conservative MSRV policy and will clearly communicate any MSRV bumps. + +## Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +## License + +Licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details. + +## Resources + +- [mcpd](https://github.com/mozilla-ai/mcpd) +- [mcpd-proto](https://github.com/mozilla-ai/mcpd-proto) - Protocol buffer definitions +- [Tonic documentation](https://docs.rs/tonic/) - gRPC framework documentation +- [Tokio documentation](https://tokio.rs/) - Async runtime documentation + +## Other Language SDKs + +- [Go SDK](https://github.com/mozilla-ai/mcpd-plugins-sdk-go) +- [Python SDK](https://github.com/mozilla-ai/mcpd-plugins-sdk-python) +- [.NET SDK](https://github.com/mozilla-ai/mcpd-plugins-sdk-dotnet) diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..03dae7a --- /dev/null +++ b/build.rs @@ -0,0 +1,43 @@ +use std::env; +use std::path::PathBuf; + +fn main() -> Result<(), Box> { + // Get the proto version from environment or use default. + let proto_version = env::var("PROTO_VERSION").unwrap_or_else(|_| "v1".to_string()); + + println!("cargo:rerun-if-changed=proto/plugin.proto"); + println!("cargo:rerun-if-env-changed=PROTO_VERSION"); + + // Download proto file if it doesn't exist. + let proto_path = PathBuf::from("proto/plugin.proto"); + + if !proto_path.exists() { + eprintln!("Downloading plugin.proto version {}...", proto_version); + std::fs::create_dir_all("proto")?; + + let url = format!( + "https://raw.githubusercontent.com/mozilla-ai/mcpd-proto/refs/heads/main/plugins/{}/plugin.proto", + proto_version + ); + + // Use a simple HTTP client to download the file. + let response = ureq::get(&url).call()?; + let mut file = std::fs::File::create(&proto_path)?; + std::io::copy(&mut response.into_reader(), &mut file)?; + + eprintln!("Downloaded proto file successfully"); + } + + // Ensure output directory exists. + let out_dir = PathBuf::from("src/generated"); + std::fs::create_dir_all(&out_dir)?; + + // Configure protobuf compilation. + tonic_build::configure() + .build_server(true) + .build_client(false) + .out_dir(&out_dir) + .compile_protos(&["proto/plugin.proto"], &["proto"])?; + + Ok(()) +} diff --git a/examples/auth_plugin/main.rs b/examples/auth_plugin/main.rs new file mode 100644 index 0000000..c57d359 --- /dev/null +++ b/examples/auth_plugin/main.rs @@ -0,0 +1,144 @@ +//! Authentication plugin that validates Bearer tokens. +//! +//! This plugin demonstrates how to implement request validation and +//! short-circuit the processing pipeline by returning early responses. + +use mcpd_plugins_sdk::{ + serve, Capabilities, HttpRequest, HttpResponse, Metadata, Plugin, PluginConfig, FLOW_REQUEST, +}; +use std::collections::HashSet; +use std::sync::Arc; +use tokio::sync::RwLock; +use tonic::{Request, Response, Status}; +use tracing_subscriber; + +struct AuthPlugin { + valid_tokens: Arc>>, +} + +impl AuthPlugin { + fn new() -> Self { + let mut tokens = HashSet::new(); + // Add some default tokens for demo purposes. + tokens.insert("demo-token-123".to_string()); + tokens.insert("test-token-456".to_string()); + + Self { + valid_tokens: Arc::new(RwLock::new(tokens)), + } + } +} + +#[tonic::async_trait] +impl Plugin for AuthPlugin { + async fn get_metadata(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(Metadata { + name: "auth-plugin".to_string(), + version: "1.0.0".to_string(), + description: "Bearer token authentication plugin".to_string(), + commit_hash: env!("CARGO_PKG_VERSION").to_string(), + build_date: env!("CARGO_PKG_VERSION").to_string(), + })) + } + + async fn get_capabilities( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Capabilities { + flows: vec![FLOW_REQUEST as i32], + })) + } + + async fn configure(&self, request: Request) -> Result, Status> { + let config = request.into_inner(); + + tracing::info!("Configuring auth plugin with custom config"); + + // Parse configuration. + if let Some(tokens_str) = config.custom_config.get("valid_tokens") { + let tokens: Vec = tokens_str.split(',').map(|s| s.to_string()).collect(); + let mut valid_tokens = self.valid_tokens.write().await; + valid_tokens.clear(); + valid_tokens.extend(tokens); + tracing::info!("Loaded {} valid tokens from config", valid_tokens.len()); + } + + Ok(Response::new(())) + } + + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + tracing::info!("Authenticating {} request to {}", req.method, req.path); + + // Skip health check endpoints. + if req.path == "/health" || req.path == "/ready" { + return Ok(Response::new(HttpResponse { + r#continue: true, + ..Default::default() + })); + } + + // Check for Authorization header. + if let Some(auth_header) = req.headers.get("Authorization") { + if let Some(token) = auth_header.strip_prefix("Bearer ") { + // Validate token. + let valid_tokens = self.valid_tokens.read().await; + if valid_tokens.contains(token) { + tracing::info!("Valid token provided, allowing request"); + return Ok(Response::new(HttpResponse { + r#continue: true, + ..Default::default() + })); + } else { + tracing::warn!("Invalid token provided"); + } + } else { + tracing::warn!("Authorization header present but not Bearer token"); + } + } else { + tracing::warn!("No Authorization header provided"); + } + + // Return 401 Unauthorized. + let body = serde_json::json!({ + "error": "unauthorized", + "message": "Valid Bearer token required" + }); + + let mut headers = std::collections::HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert( + "WWW-Authenticate".to_string(), + "Bearer realm=\"mcpd\"".to_string(), + ); + + Ok(Response::new(HttpResponse { + r#continue: false, + status_code: 401, + headers, + body: body.to_string().into_bytes(), + ..Default::default() + })) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing. + tracing_subscriber::fmt() + .with_target(false) + .with_level(true) + .init(); + + tracing::info!("Starting auth plugin"); + + // Serve the plugin. + serve(AuthPlugin::new(), None).await?; + + Ok(()) +} diff --git a/examples/rate_limit_plugin/main.rs b/examples/rate_limit_plugin/main.rs new file mode 100644 index 0000000..272d7fa --- /dev/null +++ b/examples/rate_limit_plugin/main.rs @@ -0,0 +1,206 @@ +//! Rate limiting plugin using token bucket algorithm. +//! +//! This plugin demonstrates stateful request processing with per-client +//! rate limiting and configuration via the Configure method. + +use mcpd_plugins_sdk::{ + serve, Capabilities, HttpRequest, HttpResponse, Metadata, Plugin, PluginConfig, FLOW_REQUEST, +}; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::Mutex; +use tonic::{Request, Response, Status}; +use tracing_subscriber; + +#[derive(Debug, Clone)] +struct TokenBucket { + tokens: f64, + max_tokens: f64, + refill_rate: f64, + last_refill: Instant, +} + +impl TokenBucket { + fn new(max_tokens: f64, refill_rate: f64) -> Self { + Self { + tokens: max_tokens, + max_tokens, + refill_rate, + last_refill: Instant::now(), + } + } + + fn refill(&mut self) { + let now = Instant::now(); + let elapsed = now.duration_since(self.last_refill).as_secs_f64(); + self.tokens = (self.tokens + elapsed * self.refill_rate).min(self.max_tokens); + self.last_refill = now; + } + + fn try_consume(&mut self) -> bool { + self.refill(); + if self.tokens >= 1.0 { + self.tokens -= 1.0; + true + } else { + false + } + } + + fn available_tokens(&mut self) -> f64 { + self.refill(); + self.tokens + } +} + +struct RateLimitPlugin { + buckets: Arc>>, + max_requests: f64, + window_duration: Duration, +} + +impl RateLimitPlugin { + fn new() -> Self { + Self { + buckets: Arc::new(Mutex::new(HashMap::new())), + max_requests: 10.0, + window_duration: Duration::from_secs(60), + } + } +} + +#[tonic::async_trait] +impl Plugin for RateLimitPlugin { + async fn get_metadata(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(Metadata { + name: "rate-limit-plugin".to_string(), + version: "1.0.0".to_string(), + description: "Token bucket rate limiting plugin".to_string(), + commit_hash: env!("CARGO_PKG_VERSION").to_string(), + build_date: env!("CARGO_PKG_VERSION").to_string(), + })) + } + + async fn get_capabilities( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Capabilities { + flows: vec![FLOW_REQUEST as i32], + })) + } + + async fn configure(&self, request: Request) -> Result, Status> { + let config = request.into_inner(); + + tracing::info!("Configuring rate limit plugin"); + + // Parse max_requests from config. + if let Some(max_req_str) = config.custom_config.get("max_requests") { + if let Ok(max_req) = max_req_str.parse::() { + tracing::info!("Setting max_requests to {}", max_req); + } + } + + // Parse window_duration from config. + if let Some(window_str) = config.custom_config.get("window_seconds") { + if let Ok(window_secs) = window_str.parse::() { + tracing::info!("Setting window duration to {} seconds", window_secs); + } + } + + Ok(Response::new(())) + } + + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + + tracing::debug!("Rate limiting {} request to {}", req.method, req.path); + + // Use remote_addr as the client identifier. + let client_id = req.remote_addr.clone(); + + let mut buckets = self.buckets.lock().await; + + // Get or create bucket for this client. + let bucket = buckets.entry(client_id.clone()).or_insert_with(|| { + let refill_rate = self.max_requests / self.window_duration.as_secs_f64(); + TokenBucket::new(self.max_requests, refill_rate) + }); + + // Try to consume a token. + if bucket.try_consume() { + let available = bucket.available_tokens(); + tracing::debug!( + "Request allowed for client {} ({:.1} tokens remaining)", + client_id, + available + ); + + // Add rate limit headers. + let mut headers = std::collections::HashMap::new(); + headers.insert( + "X-RateLimit-Limit".to_string(), + self.max_requests.to_string(), + ); + headers.insert( + "X-RateLimit-Remaining".to_string(), + available.floor().to_string(), + ); + + Ok(Response::new(HttpResponse { + r#continue: true, + headers, + ..Default::default() + })) + } else { + tracing::warn!("Rate limit exceeded for client {}", client_id); + + // Return 429 Too Many Requests. + let body = serde_json::json!({ + "error": "rate_limit_exceeded", + "message": "Too many requests, please try again later" + }); + + let mut headers = std::collections::HashMap::new(); + headers.insert("Content-Type".to_string(), "application/json".to_string()); + headers.insert( + "X-RateLimit-Limit".to_string(), + self.max_requests.to_string(), + ); + headers.insert("X-RateLimit-Remaining".to_string(), "0".to_string()); + headers.insert( + "Retry-After".to_string(), + self.window_duration.as_secs().to_string(), + ); + + Ok(Response::new(HttpResponse { + r#continue: false, + status_code: 429, + headers, + body: body.to_string().into_bytes(), + ..Default::default() + })) + } + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing. + tracing_subscriber::fmt() + .with_target(false) + .with_level(true) + .init(); + + tracing::info!("Starting rate limit plugin"); + + // Serve the plugin. + serve(RateLimitPlugin::new(), None).await?; + + Ok(()) +} diff --git a/examples/simple_plugin/main.rs b/examples/simple_plugin/main.rs new file mode 100644 index 0000000..909bfdc --- /dev/null +++ b/examples/simple_plugin/main.rs @@ -0,0 +1,75 @@ +//! Simple plugin that adds a custom header to all requests. +//! +//! This demonstrates the minimal implementation of a plugin that processes +//! HTTP requests and adds custom metadata. + +use mcpd_plugins_sdk::{ + serve, Capabilities, HttpRequest, HttpResponse, Metadata, Plugin, FLOW_REQUEST, +}; +use tonic::{Request, Response, Status}; +use tracing_subscriber; + +struct SimplePlugin; + +#[tonic::async_trait] +impl Plugin for SimplePlugin { + async fn get_metadata(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(Metadata { + name: "simple-plugin".to_string(), + version: "1.0.0".to_string(), + description: "A simple plugin that adds custom headers".to_string(), + commit_hash: env!("CARGO_PKG_VERSION").to_string(), + build_date: env!("CARGO_PKG_VERSION").to_string(), + })) + } + + async fn get_capabilities( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Capabilities { + flows: vec![FLOW_REQUEST as i32], + })) + } + + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + let mut req = request.into_inner(); + + // Log the request. + tracing::info!("Processing {} request to {}", req.method, req.path); + + // Add custom header. + req.headers + .insert("X-Simple-Plugin".to_string(), "processed".to_string()); + req.headers.insert( + "X-Plugin-Version".to_string(), + env!("CARGO_PKG_VERSION").to_string(), + ); + + // Return modified request. + Ok(Response::new(HttpResponse { + r#continue: true, + modified_request: Some(req), + ..Default::default() + })) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Initialize tracing. + tracing_subscriber::fmt() + .with_target(false) + .with_level(true) + .init(); + + tracing::info!("Starting simple plugin"); + + // Serve the plugin. + serve(SimplePlugin, None).await?; + + Ok(()) +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..55567c8 --- /dev/null +++ b/justfile @@ -0,0 +1,92 @@ +# justfile for mcpd-plugins-sdk-rust + +# Default recipe to display help information. +default: + @just --list + +# Format code with rustfmt. +fmt: + cargo fmt + +# Check code formatting without making changes. +fmt-check: + cargo fmt --check + +# Run clippy linter. +lint: + cargo clippy --all-targets --all-features -- -D warnings + +# Run clippy with automatic fixes. +lint-fix: + cargo clippy --all-targets --all-features --fix --allow-dirty -- -D warnings + +# Build the library. +build: + cargo build + +# Build in release mode. +release: + cargo build --release + +# Build static binary with musl (Linux only). +static: + cargo build --release --target x86_64-unknown-linux-musl + +# Run tests. +test: + cargo test + +# Run tests with output. +test-verbose: + cargo test -- --nocapture + +# Build all examples. +examples: + cargo build --examples + +# Build examples in release mode. +examples-release: + cargo build --examples --release + +# Run a specific example (usage: just run-example simple_plugin). +run-example name: + cargo run --example {{name}} -- --address /tmp/{{name}}.sock + +# Fast compile check. +check: + cargo check --all-targets --all-features + +# Check for security vulnerabilities. +audit: + cargo audit + +# Check dependencies with cargo-deny. +deny: + cargo deny check + +# Run all checks (format, lint, deny, audit, test). +ci: fmt-check lint deny audit test + +# Clean build artifacts. +clean: + cargo clean + rm -rf proto/ src/generated/ + +# Generate documentation. +doc: + cargo doc --no-deps --open + +# Install required tools. +install-tools: + rustup component add clippy rustfmt + cargo install cargo-audit cargo-deny + +# Install musl target for static builds. +install-musl: + rustup target add x86_64-unknown-linux-musl + +# Full pre-commit check. +pre-commit: fmt lint test + +# Build everything (lib, examples, tests). +all: build examples test diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..6985e1f --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,7 @@ +use crate::proto::Flow; + +/// Request flow constant - process incoming HTTP requests. +pub const FLOW_REQUEST: Flow = Flow::Request; + +/// Response flow constant - process outgoing HTTP responses. +pub const FLOW_RESPONSE: Flow = Flow::Response; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..636b003 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,46 @@ +use thiserror::Error; +use tonic::{Code, Status}; + +/// Error types for plugin operations. +#[derive(Debug, Error)] +pub enum PluginError { + /// Configuration error occurred during plugin initialization. + #[error("Configuration error: {0}")] + Configuration(String), + + /// Server initialization or runtime error. + #[error("Server error: {0}")] + Server(String), + + /// Invalid input provided to the plugin. + #[error("Invalid input: {0}")] + InvalidInput(String), + + /// Internal plugin error. + #[error("Internal error: {0}")] + Internal(String), + + /// I/O error occurred. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// gRPC transport error. + #[error("Transport error: {0}")] + Transport(#[from] tonic::transport::Error), +} + +impl From for Status { + fn from(err: PluginError) -> Self { + match err { + PluginError::Configuration(msg) => Status::new(Code::InvalidArgument, msg), + PluginError::Server(msg) => Status::new(Code::Internal, msg), + PluginError::InvalidInput(msg) => Status::new(Code::InvalidArgument, msg), + PluginError::Internal(msg) => Status::new(Code::Internal, msg), + PluginError::Io(err) => Status::new(Code::Internal, err.to_string()), + PluginError::Transport(err) => Status::new(Code::Unavailable, err.to_string()), + } + } +} + +/// Result type for plugin operations. +pub type Result = std::result::Result; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7f1a840 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,167 @@ +//! # mcpd-plugins-sdk +//! +//! Rust SDK for building [mcpd](https://github.com/mozilla-ai/mcpd) plugins. +//! +//! This SDK provides a simple, trait-based API for creating gRPC plugins that intercept and +//! transform HTTP requests and responses in the mcpd middleware pipeline. +//! +//! ## Features +//! +//! - **Simple trait-based API**: Implement the [`Plugin`] trait with only the methods you need +//! - **Async/await support**: Built on Tokio and Tonic for high-performance async I/O +//! - **Automatic server setup**: [`serve()`] function handles all boilerplate +//! - **Cross-platform**: Unix sockets (Linux/macOS) and TCP support +//! - **Type-safe**: Protocol buffers for serialization +//! - **Graceful shutdown**: SIGINT/SIGTERM handling with cleanup +//! +//! ## Quick Start +//! +//! Add this to your `Cargo.toml`: +//! +//! ```toml +//! [dependencies] +//! mcpd-plugins-sdk = "0.1" +//! tokio = { version = "1", features = ["full"] } +//! tonic = "0.12" +//! ``` +//! +//! Create a simple plugin: +//! +//! ```rust,no_run +//! use mcpd_plugins_sdk::{ +//! Plugin, serve, Metadata, Capabilities, HttpRequest, HttpResponse, +//! FLOW_REQUEST, +//! }; +//! use tonic::{Request, Response, Status}; +//! +//! struct MyPlugin; +//! +//! #[tonic::async_trait] +//! impl Plugin for MyPlugin { +//! async fn get_metadata( +//! &self, +//! _request: Request<()>, +//! ) -> Result, Status> { +//! Ok(Response::new(Metadata { +//! name: "my-plugin".to_string(), +//! version: "1.0.0".to_string(), +//! description: "My custom plugin".to_string(), +//! ..Default::default() +//! })) +//! } +//! +//! async fn get_capabilities( +//! &self, +//! _request: Request<()>, +//! ) -> Result, Status> { +//! Ok(Response::new(Capabilities { +//! flows: vec![FLOW_REQUEST as i32], +//! })) +//! } +//! +//! async fn handle_request( +//! &self, +//! request: Request, +//! ) -> Result, Status> { +//! let mut req = request.into_inner(); +//! +//! // Add custom header. +//! req.headers.insert("X-My-Plugin".to_string(), "processed".to_string()); +//! +//! Ok(Response::new(HttpResponse { +//! continue_: true, +//! modified_request: Some(req), +//! ..Default::default() +//! })) +//! } +//! } +//! +//! #[tokio::main] +//! async fn main() -> Result<(), Box> { +//! serve(MyPlugin, None).await?; +//! Ok(()) +//! } +//! ``` +//! +//! Run the plugin: +//! +//! ```bash +//! cargo run -- --address /tmp/my-plugin.sock --network unix +//! ``` +//! +//! ## Plugin Flows +//! +//! Plugins can participate in two processing flows: +//! +//! - [`FLOW_REQUEST`]: Process incoming HTTP requests before they reach the upstream server +//! - [`FLOW_RESPONSE`]: Process outgoing HTTP responses before they return to the client +//! +//! Declare which flows your plugin supports in the [`Plugin::get_capabilities()`] method. +//! +//! ## Request Handling +//! +//! The [`Plugin::handle_request()`] method receives an [`HttpRequest`] and returns an [`HttpResponse`]. +//! You can: +//! +//! 1. **Pass through unchanged**: Return `continue_: true` with no modifications +//! 2. **Transform the request**: Set `modified_request` field with changes +//! 3. **Short-circuit**: Return `continue_: false` with a custom response +//! +//! ## Example: Authentication Plugin +//! +//! ```rust,no_run +//! use mcpd_plugins_sdk::{Plugin, HttpRequest, HttpResponse}; +//! use tonic::{Request, Response, Status}; +//! +//! struct AuthPlugin; +//! +//! #[tonic::async_trait] +//! impl Plugin for AuthPlugin { +//! async fn handle_request( +//! &self, +//! request: Request, +//! ) -> Result, Status> { +//! let req = request.into_inner(); +//! +//! // Check for Authorization header. +//! if let Some(auth) = req.headers.get("Authorization") { +//! if auth.starts_with("Bearer ") { +//! // Valid token, continue processing. +//! return Ok(Response::new(HttpResponse { +//! continue_: true, +//! ..Default::default() +//! })); +//! } +//! } +//! +//! // No valid token, return 401. +//! Ok(Response::new(HttpResponse { +//! continue_: false, +//! status_code: 401, +//! body: b"Unauthorized".to_vec(), +//! ..Default::default() +//! })) +//! } +//! } +//! ``` + +// Generated protobuf code. +#[allow(clippy::all)] +#[allow(missing_docs)] +pub mod proto { + include!("generated/mozilla.mcpd.plugins.v1.rs"); +} + +mod constants; +mod error; +mod plugin; +mod server; + +// Re-export public API. +pub use constants::{FLOW_REQUEST, FLOW_RESPONSE}; +pub use error::{PluginError, Result}; +pub use plugin::{Plugin, PluginAdapter}; +pub use proto::{ + Capabilities, Flow, HttpRequest, HttpResponse, Metadata, PluginConfig, TelemetryConfig, +}; +pub use server::serve; diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100644 index 0000000..2d2cd97 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,190 @@ +use crate::proto::{ + plugin_server::Plugin as PluginService, Capabilities, HttpRequest, HttpResponse, Metadata, + PluginConfig, +}; +use tonic::{Request, Response, Status}; + +/// Main Plugin trait that all plugins must implement. +/// +/// This trait provides default implementations for all methods, allowing plugins +/// to override only the methods they need. By default: +/// - Health and readiness checks return Ok +/// - Configure and Stop do nothing +/// - GetMetadata returns empty metadata (should be overridden) +/// - GetCapabilities returns no flows (should be overridden) +/// - HandleRequest and HandleResponse pass through requests unchanged +/// +/// # Example +/// +/// ```rust,no_run +/// use mcpd_plugins_sdk::{Plugin, Metadata, Capabilities, FLOW_REQUEST}; +/// use tonic::{Request, Response, Status}; +/// +/// struct MyPlugin; +/// +/// #[tonic::async_trait] +/// impl Plugin for MyPlugin { +/// async fn get_metadata( +/// &self, +/// _request: Request<()>, +/// ) -> Result, Status> { +/// Ok(Response::new(Metadata { +/// name: "my-plugin".to_string(), +/// version: "1.0.0".to_string(), +/// description: "My custom plugin".to_string(), +/// ..Default::default() +/// })) +/// } +/// +/// async fn get_capabilities( +/// &self, +/// _request: Request<()>, +/// ) -> Result, Status> { +/// Ok(Response::new(Capabilities { +/// flows: vec![FLOW_REQUEST as i32], +/// })) +/// } +/// } +/// ``` +#[tonic::async_trait] +pub trait Plugin: Send + Sync + 'static { + /// Returns plugin metadata (name, version, description, etc.). + /// + /// This method should be overridden to provide plugin identification. + async fn get_metadata(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(Metadata::default())) + } + + /// Returns the capabilities of this plugin (which flows it supports). + /// + /// This method should be overridden to declare which processing flows + /// (REQUEST, RESPONSE, or both) the plugin participates in. + async fn get_capabilities( + &self, + _request: Request<()>, + ) -> Result, Status> { + Ok(Response::new(Capabilities { flows: vec![] })) + } + + /// Configures the plugin with host-provided settings. + /// + /// Called once during plugin initialization. Override this to parse + /// custom configuration and initialize resources. + async fn configure(&self, _request: Request) -> Result, Status> { + Ok(Response::new(())) + } + + /// Stops the plugin and cleans up resources. + /// + /// Called during graceful shutdown. Override this to close connections, + /// flush buffers, and release resources. + async fn stop(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(())) + } + + /// Health check endpoint. + /// + /// Returns Ok if the plugin is alive and operational. + async fn check_health(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(())) + } + + /// Readiness check endpoint. + /// + /// Returns Ok if the plugin is ready to handle requests. + async fn check_ready(&self, _request: Request<()>) -> Result, Status> { + Ok(Response::new(())) + } + + /// Handles incoming HTTP requests. + /// + /// Override this method to process, transform, or reject HTTP requests. + /// Return `HttpResponse { continue_: true, .. }` to pass the request to the next handler. + /// Return `HttpResponse { continue_: false, status_code, .. }` to short-circuit and + /// return a response directly to the client. + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + let _req = request.into_inner(); + Ok(Response::new(HttpResponse { + r#continue: true, + ..Default::default() + })) + } + + /// Handles outgoing HTTP responses. + /// + /// Override this method to process, transform, or modify HTTP responses + /// before they are returned to the client. + async fn handle_response( + &self, + response: Request, + ) -> Result, Status> { + let resp = response.into_inner(); + Ok(Response::new(HttpResponse { + r#continue: true, + status_code: resp.status_code, + headers: resp.headers, + body: resp.body, + ..Default::default() + })) + } +} + +/// Adapter that implements the generated gRPC service trait using our Plugin trait. +/// +/// This bridges between the tonic-generated PluginService trait and our custom Plugin trait. +pub struct PluginAdapter { + plugin: P, +} + +impl PluginAdapter

{ + pub fn new(plugin: P) -> Self { + Self { plugin } + } +} + +#[tonic::async_trait] +impl PluginService for PluginAdapter

{ + async fn get_metadata(&self, request: Request<()>) -> Result, Status> { + self.plugin.get_metadata(request).await + } + + async fn get_capabilities( + &self, + request: Request<()>, + ) -> Result, Status> { + self.plugin.get_capabilities(request).await + } + + async fn configure(&self, request: Request) -> Result, Status> { + self.plugin.configure(request).await + } + + async fn stop(&self, request: Request<()>) -> Result, Status> { + self.plugin.stop(request).await + } + + async fn check_health(&self, request: Request<()>) -> Result, Status> { + self.plugin.check_health(request).await + } + + async fn check_ready(&self, request: Request<()>) -> Result, Status> { + self.plugin.check_ready(request).await + } + + async fn handle_request( + &self, + request: Request, + ) -> Result, Status> { + self.plugin.handle_request(request).await + } + + async fn handle_response( + &self, + response: Request, + ) -> Result, Status> { + self.plugin.handle_response(response).await + } +} diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..59b602c --- /dev/null +++ b/src/server.rs @@ -0,0 +1,201 @@ +use crate::plugin::{Plugin, PluginAdapter}; +use crate::proto::plugin_server::PluginServer; +use crate::{PluginError, Result}; +use clap::Parser; +use std::path::PathBuf; +use tokio::signal; +use tonic::transport::Server; +use tracing::{info, warn}; + +#[cfg(unix)] +use tokio::net::UnixListener; + +/// Command-line arguments for the plugin server. +#[derive(Parser, Debug)] +#[command(author, version, about = "mcpd plugin server", long_about = None)] +struct Args { + /// Address to bind to (socket path for unix, host:port for tcp). + #[arg(long)] + address: String, + + /// Network type (unix or tcp). + #[arg(long, default_value = "unix")] + network: String, +} + +/// Serves a plugin on the specified address. +/// +/// This is the main entry point for running a plugin. It handles: +/// - Command-line argument parsing +/// - Server setup (Unix socket or TCP) +/// - Graceful shutdown on SIGINT/SIGTERM +/// - Automatic cleanup of Unix socket files +/// +/// # Arguments +/// +/// * `plugin` - The plugin implementation to serve +/// * `args` - Optional command-line arguments (defaults to std::env::args()) +/// +/// # Example +/// +/// ```rust,no_run +/// use mcpd_plugins_sdk::{Plugin, serve}; +/// +/// struct MyPlugin; +/// +/// #[tonic::async_trait] +/// impl Plugin for MyPlugin { +/// // Implementation... +/// } +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// serve(MyPlugin, None).await?; +/// Ok(()) +/// } +/// ``` +pub async fn serve(plugin: P, args: Option>) -> Result<()> { + // Parse command-line arguments. + let args = if let Some(args) = args { + Args::parse_from(args) + } else { + Args::parse() + }; + + info!( + "Starting plugin server on {} ({})", + args.address, args.network + ); + + // Create the plugin adapter. + let adapter = PluginAdapter::new(plugin); + let service = PluginServer::new(adapter); + + // Serve based on network type. + match args.network.as_str() { + "unix" => serve_unix(service, &args.address).await, + "tcp" => serve_tcp(service, &args.address).await, + network => Err(PluginError::Configuration(format!( + "Unsupported network type: {}", + network + ))), + } +} + +#[cfg(unix)] +async fn serve_unix(service: S, address: &str) -> Result<()> +where + S: tonic::codegen::Service< + http::Request, + Response = http::Response, + Error = std::convert::Infallible, + > + tonic::server::NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, +{ + use tokio_stream::wrappers::UnixListenerStream; + + let path = PathBuf::from(address); + + // Remove existing socket file if it exists. + if path.exists() { + warn!("Removing existing socket file: {}", address); + std::fs::remove_file(&path)?; + } + + // Create Unix listener. + let listener = UnixListener::bind(&path)?; + let stream = UnixListenerStream::new(listener); + + info!("Listening on Unix socket: {}", address); + + // Serve with graceful shutdown. + Server::builder() + .add_service(service) + .serve_with_incoming_shutdown(stream, shutdown_signal()) + .await?; + + // Clean up socket file on shutdown. + if path.exists() { + info!("Cleaning up socket file: {}", address); + let _ = std::fs::remove_file(&path); + } + + Ok(()) +} + +#[cfg(not(unix))] +async fn serve_unix(_service: S, _address: &str) -> Result<()> +where + S: tonic::codegen::Service< + http::Request, + Response = http::Response, + Error = std::convert::Infallible, + > + tonic::server::NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, +{ + Err(PluginError::Configuration( + "Unix sockets not supported on this platform".to_string(), + )) +} + +async fn serve_tcp(service: S, address: &str) -> Result<()> +where + S: tonic::codegen::Service< + http::Request, + Response = http::Response, + Error = std::convert::Infallible, + > + tonic::server::NamedService + + Clone + + Send + + 'static, + S::Future: Send + 'static, +{ + let addr = address + .parse() + .map_err(|e| PluginError::Configuration(format!("Invalid TCP address: {}", e)))?; + + info!("Listening on TCP: {}", address); + + // Serve with graceful shutdown. + Server::builder() + .add_service(service) + .serve_with_shutdown(addr, shutdown_signal()) + .await?; + + Ok(()) +} + +/// Waits for a shutdown signal (SIGINT or SIGTERM). +async fn shutdown_signal() { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => { + info!("Received SIGINT, shutting down gracefully"); + } + _ = terminate => { + info!("Received SIGTERM, shutting down gracefully"); + } + } +} From e652d51adb25ef23a66da3637e401fa614b7f503 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 14:49:32 +0000 Subject: [PATCH 2/6] Fix clippy warnings and align Makefile with CI - Remove redundant single-component path imports of tracing_subscriber from all example files - Update Makefile lint target to use --all-targets --all-features to match CI configuration and justfile This ensures make lint catches the same issues as CI clippy checks. --- Makefile | 2 +- examples/auth_plugin/main.rs | 1 - examples/rate_limit_plugin/main.rs | 1 - examples/simple_plugin/main.rs | 1 - 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Makefile b/Makefile index a15060f..9ca694c 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ examples: .PHONY: lint lint: - cargo clippy -- -D warnings + cargo clippy --all-targets --all-features -- -D warnings .PHONY: fmt fmt: diff --git a/examples/auth_plugin/main.rs b/examples/auth_plugin/main.rs index c57d359..6b74a83 100644 --- a/examples/auth_plugin/main.rs +++ b/examples/auth_plugin/main.rs @@ -10,7 +10,6 @@ use std::collections::HashSet; use std::sync::Arc; use tokio::sync::RwLock; use tonic::{Request, Response, Status}; -use tracing_subscriber; struct AuthPlugin { valid_tokens: Arc>>, diff --git a/examples/rate_limit_plugin/main.rs b/examples/rate_limit_plugin/main.rs index 272d7fa..d97108d 100644 --- a/examples/rate_limit_plugin/main.rs +++ b/examples/rate_limit_plugin/main.rs @@ -11,7 +11,6 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::Mutex; use tonic::{Request, Response, Status}; -use tracing_subscriber; #[derive(Debug, Clone)] struct TokenBucket { diff --git a/examples/simple_plugin/main.rs b/examples/simple_plugin/main.rs index 909bfdc..b1d7a70 100644 --- a/examples/simple_plugin/main.rs +++ b/examples/simple_plugin/main.rs @@ -7,7 +7,6 @@ use mcpd_plugins_sdk::{ serve, Capabilities, HttpRequest, HttpResponse, Metadata, Plugin, FLOW_REQUEST, }; use tonic::{Request, Response, Status}; -use tracing_subscriber; struct SimplePlugin; From 82921aef0e65e2418390d90fed969a8473bbf3fc Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 14:57:46 +0000 Subject: [PATCH 3/6] Bump MSRV to 1.83.0 Update Minimum Supported Rust Version from 1.75.0 to 1.83.0 due to dependency requirements (icu_collections v2.1.1 requires rustc 1.83+). Updated in: - Cargo.toml: rust-version field - README.md: version badge and policy section - CONTRIBUTING.md: prerequisites section - .github/workflows/test.yaml: CI test matrix --- .github/workflows/test.yaml | 2 +- CONTRIBUTING.md | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b407bb5..b219e7e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -17,7 +17,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest] - rust: [stable, 1.75.0] + rust: [stable, 1.83.0] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: dtolnay/rust-toolchain@stable # Action's stable branch. No SHA pinning - repo doesn't publish releases. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 818aaa5..b7e940a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ Thank you for your interest in contributing to the Rust SDK for mcpd plugins! ### Prerequisites -- Rust 1.75 or later +- Rust 1.83 or later - `cargo` and `rustup` ### Install Development Tools diff --git a/Cargo.toml b/Cargo.toml index a321079..11644ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "mcpd-plugins-sdk" version = "0.0.2" edition = "2021" -rust-version = "1.75" +rust-version = "1.83" authors = ["Mozilla AI"] license = "Apache-2.0" description = "Rust SDK for building mcpd plugins" diff --git a/README.md b/README.md index 19192fa..caadfaf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # mcpd-plugins-sdk-rust [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) -[![Rust Version](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org) +[![Rust Version](https://img.shields.io/badge/rust-1.83%2B-orange.svg)](https://www.rust-lang.org) Rust SDK for building [mcpd](https://github.com/mozilla-ai/mcpd) plugins. @@ -387,7 +387,7 @@ PROTO_VERSION=v0.0.3 cargo build ## Rust Version Policy -This crate requires Rust 1.75 or later. We follow a conservative MSRV policy and will clearly communicate any MSRV bumps. +This crate requires Rust 1.83 or later. We follow a conservative MSRV policy and will clearly communicate any MSRV bumps. ## Contributing From 54d46ef7dfe65dae4de85e99793590334af95c13 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 15:06:51 +0000 Subject: [PATCH 4/6] Fix doctests to use r#continue field syntax Updated documentation examples to use the correct r#continue raw identifier syntax instead of continue_ for the HttpResponse field. This matches the protobuf-generated code where continue is a Rust reserved keyword. Fixed in src/lib.rs: - Quick Start example (line 72) - Authentication Plugin example pass-through (line 131) - Authentication Plugin short-circuit (line 139) All doctests now pass. --- src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7f1a840..dbd065e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,7 @@ //! req.headers.insert("X-My-Plugin".to_string(), "processed".to_string()); //! //! Ok(Response::new(HttpResponse { -//! continue_: true, +//! r#continue: true, //! modified_request: Some(req), //! ..Default::default() //! })) @@ -128,7 +128,7 @@ //! if auth.starts_with("Bearer ") { //! // Valid token, continue processing. //! return Ok(Response::new(HttpResponse { -//! continue_: true, +//! r#continue: true, //! ..Default::default() //! })); //! } @@ -136,7 +136,7 @@ //! //! // No valid token, return 401. //! Ok(Response::new(HttpResponse { -//! continue_: false, +//! r#continue: false, //! status_code: 401, //! body: b"Unauthorized".to_vec(), //! ..Default::default() From 7b3d05490e70871e8a06ac7f4828daa8e21ef1b4 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 15:47:59 +0000 Subject: [PATCH 5/6] Fix cargo-deny license configuration Renamed .cargo-deny.toml to deny.toml for proper detection by cargo-deny. Updated license configuration to: - Add missing permissive licenses (0BSD, Unicode-3.0, CDLA-Permissive-2.0) - Remove deprecated configuration options (vulnerability, copyleft, deny) - Add explicit documentation that all allowed licenses are compatible with Apache-2.0 - Simplify configuration to work with latest cargo-deny version All allowed licenses are permissive (non-copyleft) and compatible with the Apache-2.0 license used by this SDK. --- .cargo-deny.toml | 51 ------------------------------------------------ deny.toml | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 51 deletions(-) delete mode 100644 .cargo-deny.toml create mode 100644 deny.toml diff --git a/.cargo-deny.toml b/.cargo-deny.toml deleted file mode 100644 index b2cc0d0..0000000 --- a/.cargo-deny.toml +++ /dev/null @@ -1,51 +0,0 @@ -# cargo-deny configuration -# https://embarkstudios.github.io/cargo-deny/ - -[advisories] -# Deny crates with security vulnerabilities. -vulnerability = "deny" -# Warn about unmaintained crates. -unmaintained = "warn" -# Warn about yanked crates. -yanked = "warn" -# Ignore advisories for crates that are not used in production. -ignore = [] - -[licenses] -# Allow Apache-2.0 and MIT licenses. -allow = [ - "Apache-2.0", - "MIT", - "BSD-3-Clause", - "ISC", - "Unicode-DFS-2016", -] -# Deny AGPLv3 and GPLv3 (copyleft licenses). -deny = [ - "AGPL-3.0", - "GPL-3.0", -] -# Confidence threshold for license detection. -confidence-threshold = 0.8 -# Use exceptions for specific crates if needed. -exceptions = [] - -[bans] -# Warn about multiple versions of the same crate. -multiple-versions = "warn" -# Deny specific crates. -deny = [] -# Skip certain crates from multiple version detection. -skip = [] -# Skip tree for certain crates. -skip-tree = [] - -[sources] -# Deny crates from unknown registries. -unknown-registry = "deny" -# Deny crates from git repositories (prefer crates.io). -unknown-git = "deny" -# Allow crates from crates.io. -allow-registry = ["https://github.com/rust-lang/crates.io-index"] -# Allow specific git sources if needed. -allow-git = [] diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..80496d9 --- /dev/null +++ b/deny.toml @@ -0,0 +1,35 @@ +# cargo-deny configuration +# https://embarkstudios.github.io/cargo-deny/ + +[advisories] +# Check all dependencies for security vulnerabilities. +ignore = [] + +[licenses] +# This SDK is licensed under Apache-2.0. +# Only allow permissive licenses that are compatible with Apache-2.0. +# All licenses below are permissive (non-copyleft) and can be combined with Apache-2.0. +allow = [ + "Apache-2.0", # Same license as this SDK. + "MIT", # Compatible: Permissive, allows relicensing. + "BSD-2-Clause", # Compatible: Permissive BSD variant. + "BSD-3-Clause", # Compatible: Permissive BSD variant. + "ISC", # Compatible: Functionally equivalent to MIT. + "0BSD", # Compatible: Public domain equivalent, most permissive BSD. + "Unicode-3.0", # Compatible: Permissive, used by ICU internationalization crates. + "CDLA-Permissive-2.0", # Compatible: Community Data License, permissive for data/root certificates. +] +# Confidence threshold for license detection. +confidence-threshold = 0.8 + +[bans] +# Warn about multiple versions of the same crate. +multiple-versions = "warn" + +[sources] +# Deny crates from unknown registries. +unknown-registry = "deny" +# Deny crates from git repositories (prefer crates.io). +unknown-git = "deny" +# Allow crates from crates.io. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] From d13e5b0e7cfd265f35df89ad345121e9ff333bcf Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Tue, 11 Nov 2025 16:29:59 +0000 Subject: [PATCH 6/6] Pin GHA: EmbarkStudios/cargo-deny-action --- .github/workflows/security.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index 1b026e7..84b1758 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install protoc run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - uses: EmbarkStudios/cargo-deny-action@v2 + - uses: EmbarkStudios/cargo-deny-action@f2ba7abc2abebaf185c833c3961145a3c275caad # v2.0.13 with: arguments: --all-features @@ -28,4 +28,4 @@ jobs: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install protoc run: sudo apt-get update && sudo apt-get install -y protobuf-compiler - - uses: actions-rust-lang/audit@v1 + - uses: actions-rust-lang/audit@66172f762800e4befdf5c0765d24bd4c79891f47 # v1.2.5