diff --git a/MANIFEST.in b/MANIFEST.in
index 6090fb781..cb5cb9a5c 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -200,3 +200,12 @@ exclude plugins/external/llmguard/Containerfile
exclude plugins/external/llmguard/MANIFEST.in
exclude plugins/external/llmguard/pyproject.toml
exclude plugins/external/llmguard/run-server.sh
+
+# Exclude cedar
+
+exclude plugins/external/cedar/.dockerignore
+exclude plugins/external/cedar/.env.template
+exclude plugins/external/cedar/.ruff.toml
+exclude plugins/external/cedar/Containerfile
+exclude plugins/external/cedar/MANIFEST.in
+exclude plugins/external/cedar/pyproject.toml
\ No newline at end of file
diff --git a/docs/docs/architecture/plugins.md b/docs/docs/architecture/plugins.md
index b34b8ffab..d2ba0da3d 100644
--- a/docs/docs/architecture/plugins.md
+++ b/docs/docs/architecture/plugins.md
@@ -103,7 +103,7 @@ flowchart TB
subgraph "External Services"
AI["AI Safety Services
(LlamaGuard, OpenAI)"]
- Security["Security Services
(Vault, OPA)"]
+ Security["Security Services
(Vault, OPA, Cedar)"]
end
Client --request--> GW
@@ -141,7 +141,7 @@ The framework supports two distinct plugin deployment patterns:
- Standalone MCP servers implementing plugin logic
- Can be written in any language (Python, TypeScript, Go, Rust, etc.)
- Communicate via MCP protocol (Streamable HTTP, STDIO, SSE)
-- Examples: OPA filter, LlamaGuard, OpenAI Moderation, custom AI services
+- Examples: OPA filter, Cedar Policy Plugin (RBAC), LlamaGuard, OpenAI Moderation, custom AI services
### Plugin Configuration Schema
@@ -1721,6 +1721,7 @@ FEDERATION_POST_SYNC = "federation_post_sync" # Post-federation processing
#### Current Integrations
- ✅ **Open Policy Agent (OPA):** Policy-as-code enforcement engine
+- ✅ **Cedar Policy Plugin:** Policy-as-code enforcement engine, RBAC
- ✅ **LlamaGuard:** Content safety classification and filtering
- ✅ **OpenAI Moderation API:** Commercial content moderation
- ✅ **Custom MCP Servers:** Any language, any protocol
diff --git a/docs/docs/using/plugins/plugins.md b/docs/docs/using/plugins/plugins.md
index 660afdce3..36e57c2ce 100644
--- a/docs/docs/using/plugins/plugins.md
+++ b/docs/docs/using/plugins/plugins.md
@@ -104,6 +104,7 @@ Plugins for enforcing custom policies and business rules.
| Plugin | Type | Description |
|--------|------|-------------|
| [OPA Plugin](https://github.com/IBM/mcp-context-forge/tree/main/plugins/external/opa) | External | Enforces Rego policies on tool invocations via an OPA server. Allows selective policy application per tool with context injection and customizable policy endpoints |
+| [Cedar (RBAC) Plugin](https://github.com/IBM/mcp-context-forge/tree/main/plugins/external/cedar) | External | Enforces RBAC-based policies on MCP servers using Cedar (leveraging the cedarpy library) or a custom DSL, for local evaluation with flexible configuration and output redaction. |
## Plugin Types
diff --git a/plugins/external/cedar/.dockerignore b/plugins/external/cedar/.dockerignore
new file mode 100644
index 000000000..e9a71f900
--- /dev/null
+++ b/plugins/external/cedar/.dockerignore
@@ -0,0 +1,363 @@
+# syntax=docker/dockerfile:1
+#----------------------------------------------------------------------
+# Docker Build Context Optimization
+#
+# This .dockerignore file excludes unnecessary files from the Docker
+# build context to improve build performance and security.
+#----------------------------------------------------------------------
+
+#----------------------------------------------------------------------
+# 1. Development and source directories (not needed in production)
+#----------------------------------------------------------------------
+agent_runtimes/
+charts/
+deployment/
+docs/
+deployment/k8s/
+mcp-servers/
+tests/
+test/
+attic/
+*.md
+.benchmarks/
+
+# Development environment directories
+.devcontainer/
+.github/
+.vscode/
+.idea/
+
+#----------------------------------------------------------------------
+# 2. Version control
+#----------------------------------------------------------------------
+.git/
+.gitignore
+.gitattributes
+.gitmodules
+
+#----------------------------------------------------------------------
+# 3. Python build artifacts and caches
+#----------------------------------------------------------------------
+# Byte-compiled files
+__pycache__/
+*.py[cod]
+*.pyc
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+.wily/
+
+# PyInstaller
+*.manifest
+*.spec
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+#----------------------------------------------------------------------
+# 4. Virtual environments
+#----------------------------------------------------------------------
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+.python37/
+.python39/
+.python-version
+
+# PDM
+pdm.lock
+.pdm.toml
+.pdm-python
+
+#----------------------------------------------------------------------
+# 5. Package managers and dependencies
+#----------------------------------------------------------------------
+# Node.js
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.npm
+.yarn
+
+# pip
+pip-log.txt
+pip-delete-this-directory.txt
+
+#----------------------------------------------------------------------
+# 6. Docker and container files (avoid recursive copies)
+#----------------------------------------------------------------------
+Dockerfile
+Dockerfile.*
+Containerfile
+Containerfile.*
+docker-compose.yml
+docker-compose.*.yml
+podman-compose*.yaml
+.dockerignore
+
+#----------------------------------------------------------------------
+# 7. IDE and editor files
+#----------------------------------------------------------------------
+# JetBrains
+.idea/
+*.iml
+*.iws
+*.ipr
+
+# VSCode
+.vscode/
+*.code-workspace
+
+# Vim
+*.swp
+*.swo
+*~
+
+# Emacs
+*~
+\#*\#
+.\#*
+
+# macOS
+.DS_Store
+.AppleDouble
+.LSOverride
+
+#----------------------------------------------------------------------
+# 8. Build tools and CI/CD configurations
+#----------------------------------------------------------------------
+# Testing configurations
+.coveragerc
+.pylintrc
+.flake8
+pytest.ini
+tox.ini
+.pytest.ini
+
+# Linting and formatting
+.hadolint.yaml
+.pre-commit-config.yaml
+.pycodestyle
+.pyre_configuration
+.pyspelling.yaml
+.ruff.toml
+.shellcheckrc
+
+# Build configurations
+Makefile
+setup.cfg
+pyproject.toml.bak
+MANIFEST.in
+
+# CI/CD
+.travis.*
+.gitlab-ci.yml
+.circleci/
+.github/
+azure-pipelines.yml
+Jenkinsfile
+
+# Code quality
+sonar-code.properties
+sonar-project.properties
+.scannerwork/
+whitesource.config
+.whitesource
+
+# Other tools
+.bumpversion.cfg
+.editorconfig
+mypy.ini
+
+#----------------------------------------------------------------------
+# 9. Application runtime files (should not be in image)
+#----------------------------------------------------------------------
+# Databases
+*.db
+*.sqlite
+*.sqlite3
+mcp.db
+db.sqlite3
+
+# Logs
+*.log
+logs/
+log/
+
+# Certificates and secrets
+certs/
+*.pem
+*.key
+*.crt
+*.csr
+.env
+.env.*
+
+# Generated files
+public/
+static/
+media/
+
+# Application instances
+instance/
+local_settings.py
+
+#----------------------------------------------------------------------
+# 10. Framework-specific files
+#----------------------------------------------------------------------
+# Django
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+media/
+
+# Flask
+instance/
+.webassets-cache
+
+# Scrapy
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+docs/build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+*.ipynb
+
+# IPython
+profile_default/
+ipython_config.py
+
+# celery
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+#----------------------------------------------------------------------
+# 11. Backup and temporary files
+#----------------------------------------------------------------------
+*.bak
+*.backup
+*.tmp
+*.temp
+*.orig
+*.rej
+.backup/
+backup/
+tmp/
+temp/
+
+#----------------------------------------------------------------------
+# 12. Documentation and miscellaneous
+#----------------------------------------------------------------------
+*.md
+!README.md
+LICENSE
+CHANGELOG
+AUTHORS
+CONTRIBUTORS
+TODO
+TODO.md
+DEVELOPING.md
+CONTRIBUTING.md
+
+# Spelling
+.spellcheck-en.txt
+*.dic
+
+# Shell scripts (if not needed in container)
+test.sh
+scripts/test/
+scripts/dev/
+
+#----------------------------------------------------------------------
+# 13. OS-specific files
+#----------------------------------------------------------------------
+# Windows
+Thumbs.db
+ehthumbs.db
+Desktop.ini
+$RECYCLE.BIN/
+
+# Linux
+*~
+.fuse_hidden*
+.directory
+.Trash-*
+.nfs*
+
+#----------------------------------------------------------------------
+# End of .dockerignore
+#----------------------------------------------------------------------
diff --git a/plugins/external/cedar/.env.template b/plugins/external/cedar/.env.template
new file mode 100644
index 000000000..5dbc57403
--- /dev/null
+++ b/plugins/external/cedar/.env.template
@@ -0,0 +1,63 @@
+#####################################
+# Plugins Settings
+#####################################
+
+# Enable the plugin framework
+PLUGINS_ENABLED=false
+
+# Enable auto-completion for plugins CLI
+PLUGINS_CLI_COMPLETION=false
+
+# default host port to listen on
+PLUGINS_SERVER_HOST=0.0.0.0
+
+# Set markup mode for plugins CLI
+# Valid options:
+# rich: use rich markup
+# markdown: allow markdown in help strings
+# disabled: disable markup
+# If unset (commented out), uses "rich" if rich is detected, otherwise disables it.
+PLUGINS_CLI_MARKUP_MODE=rich
+
+# Configuration path for plugin loader
+PLUGINS_CONFIG=./resources/plugins/config.yaml
+
+# Configuration path for chuck mcp runtime
+CHUK_MCP_CONFIG_PATH=./resources/runtime/config.yaml
+
+# Configuration for plugins transport
+PLUGINS_TRANSPORT=streamablehttp
+
+#####################################
+# MCP External Plugin Server - mTLS Configuration
+#####################################
+
+# Enable SSL/TLS for external plugin MCP server
+# Options: true, false (default)
+# When true: Enables HTTPS and optionally mTLS for the plugin MCP server
+MCP_SSL_ENABLED=false
+
+# SSL/TLS Certificate Files
+# Path to server private key (required when MCP_SSL_ENABLED=true)
+# Generate with: openssl genrsa -out certs/mcp/server.key 2048
+# MCP_SSL_KEYFILE=certs/mcp/server.key
+
+# Path to server certificate (required when MCP_SSL_ENABLED=true)
+# Generate with: openssl req -new -x509 -key certs/mcp/server.key -out certs/mcp/server.crt -days 365
+# MCP_SSL_CERTFILE=certs/mcp/server.crt
+
+# Optional password for encrypted private key
+# MCP_SSL_KEYFILE_PASSWORD=
+
+# mTLS (Mutual TLS) Configuration
+# Client certificate verification mode:
+# 0 (CERT_NONE): No client certificate required - standard TLS (default)
+# 1 (CERT_OPTIONAL): Client certificate optional - validate if provided
+# 2 (CERT_REQUIRED): Client certificate required - full mTLS
+# Default: 0 (standard TLS without client verification)
+MCP_SSL_CERT_REQS=0
+
+# CA certificate bundle for verifying client certificates
+# Required when MCP_SSL_CERT_REQS=1 or MCP_SSL_CERT_REQS=2
+# Can be a single CA file or a bundle containing multiple CAs
+# MCP_SSL_CA_CERTS=certs/mcp/ca.crt
diff --git a/plugins/external/cedar/.ruff.toml b/plugins/external/cedar/.ruff.toml
new file mode 100644
index 000000000..443a275df
--- /dev/null
+++ b/plugins/external/cedar/.ruff.toml
@@ -0,0 +1,63 @@
+# Exclude a variety of commonly ignored directories.
+exclude = [
+ ".bzr",
+ ".direnv",
+ ".eggs",
+ ".git",
+ ".git-rewrite",
+ ".hg",
+ ".ipynb_checkpoints",
+ ".mypy_cache",
+ ".nox",
+ ".pants.d",
+ ".pyenv",
+ ".pytest_cache",
+ ".pytype",
+ ".ruff_cache",
+ ".svn",
+ ".tox",
+ ".venv",
+ ".vscode",
+ "__pypackages__",
+ "_build",
+ "buck-out",
+ "build",
+ "dist",
+ "node_modules",
+ "site-packages",
+ "venv",
+ "docs",
+ "test"
+]
+
+# 200 line length
+line-length = 200
+indent-width = 4
+
+# Assume Python 3.11
+target-version = "py311"
+
+[lint]
+# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
+select = ["E4", "E7", "E9", "F"]
+ignore = []
+
+# Allow fix for all enabled rules (when `--fix`) is provided.
+fixable = ["ALL"]
+unfixable = []
+
+# Allow unused variables when underscore-prefixed.
+dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
+
+[format]
+# Like Black, use double quotes for strings.
+quote-style = "double"
+
+# Like Black, indent with spaces, rather than tabs.
+indent-style = "space"
+
+# Like Black, respect magic trailing commas.
+skip-magic-trailing-comma = false
+
+# Like Black, automatically detect the appropriate line ending.
+line-ending = "auto"
diff --git a/plugins/external/cedar/Containerfile b/plugins/external/cedar/Containerfile
new file mode 100644
index 000000000..d2d5f6748
--- /dev/null
+++ b/plugins/external/cedar/Containerfile
@@ -0,0 +1,47 @@
+# syntax=docker/dockerfile:1.7
+ARG UBI=python-312-minimal
+
+FROM registry.access.redhat.com/ubi9/${UBI} AS builder
+
+ARG PYTHON_VERSION=3.12
+
+ARG VERSION
+ARG COMMIT_ID
+ARG SKILLS_SDK_COMMIT_ID
+ARG SKILLS_SDK_VERSION
+ARG BUILD_TIME_SKILLS_INSTALL
+
+ENV APP_HOME=/app
+
+USER 0
+
+# Image pre-requisites
+RUN INSTALL_PKGS="git make gcc gcc-c++ python${PYTHON_VERSION}-devel" && \
+ microdnf -y --setopt=tsflags=nodocs --setopt=install_weak_deps=0 install $INSTALL_PKGS && \
+ microdnf -y clean all --enablerepo='*'
+
+# Setup alias from HOME to APP_HOME
+RUN mkdir -p ${APP_HOME} && \
+ chown -R 1001:0 ${APP_HOME} && \
+ ln -s ${HOME} ${APP_HOME} && \
+ mkdir -p ${HOME}/resources/config && \
+ chown -R 1001:0 ${HOME}/resources/config
+
+USER 1001
+
+# Install plugin package
+COPY . .
+RUN pip install --no-cache-dir uv && python -m uv pip install .
+
+# Make default cache directory writable
+RUN mkdir -p -m 0776 ${HOME}/.cache
+
+# Update labels
+LABEL maintainer="Context Forge MCP Gateway Team" \
+ name="mcp/mcppluginserver" \
+ version="${VERSION}" \
+ url="https://github.com/IBM/mcp-context-forge" \
+ description="MCP Plugin Server for the Context Forge MCP Gateway"
+
+# App entrypoint
+ENTRYPOINT ["sh", "-c", "${HOME}/run-server.sh"]
diff --git a/plugins/external/cedar/MANIFEST.in b/plugins/external/cedar/MANIFEST.in
new file mode 100644
index 000000000..1fb92c60a
--- /dev/null
+++ b/plugins/external/cedar/MANIFEST.in
@@ -0,0 +1,67 @@
+# ──────────────────────────────────────────────────────────────
+# MANIFEST.in - source-distribution contents for cedarpolicyplugin
+# ──────────────────────────────────────────────────────────────
+
+# 1️⃣ Core project files that SDists/Wheels should always carry
+include LICENSE
+include README.md
+include pyproject.toml
+include Containerfile
+
+# 2️⃣ Top-level config, examples and helper scripts
+include *.py
+include *.md
+include *.example
+include *.lock
+include *.properties
+include *.toml
+include *.yaml
+include *.yml
+include *.json
+include *.sh
+include *.txt
+recursive-include tests/async *.py
+recursive-include tests/async *.yaml
+
+# 3️⃣ Tooling/lint configuration dot-files (explicit so they're not lost)
+include .env.make
+include .interrogaterc
+include .jshintrc
+include whitesource.config
+include .darglint
+include .dockerignore
+include .flake8
+include .htmlhintrc
+include .pycodestyle
+include .pylintrc
+include .whitesource
+include .coveragerc
+# include .gitignore # purely optional but many projects ship it
+include .bumpversion.cfg
+include .yamllint
+include .editorconfig
+include .snyk
+
+# 4️⃣ Runtime data that lives *inside* the package at import time
+recursive-include resources/plugins *.yaml
+recursive-include cedarpolicyplugin *.yaml
+
+# 5️⃣ (Optional) include MKDocs-based docs in the sdist
+# graft docs
+
+# 6️⃣ Never publish caches, compiled or build outputs, deployment, agent_runtimes, etc.
+global-exclude __pycache__ *.py[cod] *.so *.dylib
+prune build
+prune dist
+prune .eggs
+prune *.egg-info
+prune charts
+prune k8s
+prune .devcontainer
+exclude CLAUDE.*
+exclude llms-full.txt
+
+# Exclude deployment, mcp-servers and agent_runtimes
+prune deployment
+prune mcp-servers
+prune agent_runtimes
diff --git a/plugins/external/cedar/Makefile b/plugins/external/cedar/Makefile
new file mode 100644
index 000000000..a6855e6e3
--- /dev/null
+++ b/plugins/external/cedar/Makefile
@@ -0,0 +1,449 @@
+
+REQUIRED_BUILD_BINS := uv
+
+SHELL := /bin/bash
+.SHELLFLAGS := -eu -o pipefail -c
+
+# Project variables
+PACKAGE_NAME = cedarpolicyplugin
+PROJECT_NAME = cedarpolicyplugin
+TARGET ?= cedarpolicyplugin
+
+# Virtual-environment variables
+VENVS_DIR ?= $(HOME)/.venv
+VENV_DIR ?= $(VENVS_DIR)/$(PROJECT_NAME)
+
+# =============================================================================
+# Linters
+# =============================================================================
+
+black:
+ @echo "🎨 black $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 $(TARGET)
+
+black-check:
+ @echo "🎨 black --check $(TARGET)..." && $(VENV_DIR)/bin/black -l 200 --check --diff $(TARGET)
+
+ruff:
+ @echo "⚡ ruff $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET) && $(VENV_DIR)/bin/ruff format $(TARGET)
+
+ruff-check:
+ @echo "⚡ ruff check $(TARGET)..." && $(VENV_DIR)/bin/ruff check $(TARGET)
+
+ruff-fix:
+ @echo "⚡ ruff check --fix $(TARGET)..." && $(VENV_DIR)/bin/ruff check --fix $(TARGET)
+
+ruff-format:
+ @echo "⚡ ruff format $(TARGET)..." && $(VENV_DIR)/bin/ruff format $(TARGET)
+
+# =============================================================================
+# Container runtime configuration and operations
+# =============================================================================
+
+# Container resource limits
+CONTAINER_MEMORY = 2048m
+CONTAINER_CPUS = 2
+
+# Auto-detect container runtime if not specified - DEFAULT TO DOCKER
+CONTAINER_RUNTIME ?= $(shell command -v docker >/dev/null 2>&1 && echo docker || echo podman)
+
+# Alternative: Always default to docker unless explicitly overridden
+# CONTAINER_RUNTIME ?= docker
+
+# Container port
+CONTAINER_PORT ?= 8000
+CONTAINER_INTERNAL_PORT ?= 8000
+
+print-runtime:
+ @echo Using container runtime: $(CONTAINER_RUNTIME)
+
+# Base image name (without any prefix)
+IMAGE_BASE ?= mcpgateway/$(PROJECT_NAME)
+IMAGE_TAG ?= latest
+
+# Handle runtime-specific image naming
+ifeq ($(CONTAINER_RUNTIME),podman)
+ # Podman adds localhost/ prefix for local builds
+ IMAGE_LOCAL := localhost/$(IMAGE_BASE):$(IMAGE_TAG)
+ IMAGE_LOCAL_DEV := localhost/$(IMAGE_BASE)-dev:$(IMAGE_TAG)
+ IMAGE_PUSH := $(IMAGE_BASE):$(IMAGE_TAG)
+else
+ # Docker doesn't add prefix
+ IMAGE_LOCAL := $(IMAGE_BASE):$(IMAGE_TAG)
+ IMAGE_LOCAL_DEV := $(IMAGE_BASE)-dev:$(IMAGE_TAG)
+ IMAGE_PUSH := $(IMAGE_BASE):$(IMAGE_TAG)
+endif
+
+print-image:
+ @echo "🐳 Container Runtime: $(CONTAINER_RUNTIME)"
+ @echo "Using image: $(IMAGE_LOCAL)"
+ @echo "Development image: $(IMAGE_LOCAL_DEV)"
+ @echo "Push image: $(IMAGE_PUSH)"
+
+
+
+# Function to get the actual image name as it appears in image list
+define get_image_name
+$(shell $(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "(localhost/)?$(IMAGE_BASE):$(IMAGE_TAG)" | head -1)
+endef
+
+# Function to normalize image name for operations
+define normalize_image
+$(if $(findstring localhost/,$(1)),$(1),$(if $(filter podman,$(CONTAINER_RUNTIME)),localhost/$(1),$(1)))
+endef
+
+# Containerfile to use (can be overridden)
+#CONTAINER_FILE ?= Containerfile
+CONTAINER_FILE ?= $(shell [ -f "Containerfile" ] && echo "Containerfile" || echo "Dockerfile")
+
+# Define COMMA for the conditional Z flag
+COMMA := ,
+
+container-info:
+ @echo "🐳 Container Runtime Configuration"
+ @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ @echo "Runtime: $(CONTAINER_RUNTIME)"
+ @echo "Base Image: $(IMAGE_BASE)"
+ @echo "Tag: $(IMAGE_TAG)"
+ @echo "Local Image: $(IMAGE_LOCAL)"
+ @echo "Push Image: $(IMAGE_PUSH)"
+ @echo "Actual Image: $(call get_image_name)"
+ @echo "Container File: $(CONTAINER_FILE)"
+ @echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+
+# Auto-detect platform based on uname
+PLATFORM ?= linux/$(shell uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
+
+container-build:
+ @echo "🔨 Building with $(CONTAINER_RUNTIME) for platform $(PLATFORM)..."
+ $(CONTAINER_RUNTIME) build \
+ --platform=$(PLATFORM) \
+ -f $(CONTAINER_FILE) \
+ --tag $(IMAGE_BASE):$(IMAGE_TAG) \
+ .
+ @echo "✅ Built image: $(call get_image_name)"
+ $(CONTAINER_RUNTIME) images $(IMAGE_BASE):$(IMAGE_TAG)
+
+container-run: container-check-image
+ @echo "🚀 Running with $(CONTAINER_RUNTIME)..."
+ -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true
+ -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true
+ $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \
+ --env-file=.env \
+ -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \
+ --restart=always \
+ --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \
+ --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \
+ --health-interval=1m --health-retries=3 \
+ --health-start-period=30s --health-timeout=10s \
+ -d $(call get_image_name)
+ @sleep 2
+ @echo "✅ Container started"
+ @echo "🔍 Health check status:"
+ @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured"
+
+container-run-host: container-check-image
+ @echo "🚀 Running with $(CONTAINER_RUNTIME)..."
+ -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true
+ -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true
+ $(CONTAINER_RUNTIME) run --name $(PROJECT_NAME) \
+ --env-file=.env \
+ --network=host \
+ -p $(CONTAINER_PORT):$(CONTAINER_INTERNAL_PORT) \
+ --restart=always \
+ --memory=$(CONTAINER_MEMORY) --cpus=$(CONTAINER_CPUS) \
+ --health-cmd="curl --fail http://localhost:$(CONTAINER_INTERNAL_PORT)/health || exit 1" \
+ --health-interval=1m --health-retries=3 \
+ --health-start-period=30s --health-timeout=10s \
+ -d $(call get_image_name)
+ @sleep 2
+ @echo "✅ Container started"
+ @echo "🔍 Health check status:"
+ @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo "No health check configured"
+
+container-push: container-check-image
+ @echo "📤 Preparing to push image..."
+ @# For Podman, we need to remove localhost/ prefix for push
+ @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \
+ actual_image=$$($(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | grep -E "$(IMAGE_BASE):$(IMAGE_TAG)" | head -1); \
+ if echo "$$actual_image" | grep -q "^localhost/"; then \
+ echo "🏷️ Tagging for push (removing localhost/ prefix)..."; \
+ $(CONTAINER_RUNTIME) tag "$$actual_image" $(IMAGE_PUSH); \
+ fi; \
+ fi
+ $(CONTAINER_RUNTIME) push $(IMAGE_PUSH)
+ @echo "✅ Pushed: $(IMAGE_PUSH)"
+
+container-check-image:
+ @echo "🔍 Checking for image..."
+ @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \
+ if ! $(CONTAINER_RUNTIME) image exists $(IMAGE_LOCAL) 2>/dev/null && \
+ ! $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \
+ echo "❌ Image not found: $(IMAGE_LOCAL)"; \
+ echo "💡 Run 'make container-build' first"; \
+ exit 1; \
+ fi; \
+ else \
+ if ! $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q . && \
+ ! $(CONTAINER_RUNTIME) images -q $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null | grep -q .; then \
+ echo "❌ Image not found: $(IMAGE_LOCAL)"; \
+ echo "💡 Run 'make container-build' first"; \
+ exit 1; \
+ fi; \
+ fi
+ @echo "✅ Image found"
+
+container-stop:
+ @echo "🛑 Stopping container..."
+ -$(CONTAINER_RUNTIME) stop $(PROJECT_NAME) 2>/dev/null || true
+ -$(CONTAINER_RUNTIME) rm $(PROJECT_NAME) 2>/dev/null || true
+ @echo "✅ Container stopped and removed"
+
+container-logs:
+ @echo "📜 Streaming logs (Ctrl+C to exit)..."
+ $(CONTAINER_RUNTIME) logs -f $(PROJECT_NAME)
+
+container-shell:
+ @echo "🔧 Opening shell in container..."
+ @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \
+ echo "❌ Container $(PROJECT_NAME) is not running"; \
+ echo "💡 Run 'make container-run' first"; \
+ exit 1; \
+ fi
+ @$(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/bash 2>/dev/null || \
+ $(CONTAINER_RUNTIME) exec -it $(PROJECT_NAME) /bin/sh
+
+container-health:
+ @echo "🏥 Checking container health..."
+ @if ! $(CONTAINER_RUNTIME) ps -q -f name=$(PROJECT_NAME) | grep -q .; then \
+ echo "❌ Container $(PROJECT_NAME) is not running"; \
+ exit 1; \
+ fi
+ @echo "Status: $$($(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{.State.Health.Status}}' 2>/dev/null || echo 'No health check')"
+ @echo "Logs:"
+ @$(CONTAINER_RUNTIME) inspect $(PROJECT_NAME) --format='{{range .State.Health.Log}}{{.Output}}{{end}}' 2>/dev/null || true
+
+container-build-multi:
+ @echo "🔨 Building multi-architecture image..."
+ @if [ "$(CONTAINER_RUNTIME)" = "docker" ]; then \
+ if ! docker buildx inspect $(PROJECT_NAME)-builder >/dev/null 2>&1; then \
+ echo "📦 Creating buildx builder..."; \
+ docker buildx create --name $(PROJECT_NAME)-builder; \
+ fi; \
+ docker buildx use $(PROJECT_NAME)-builder; \
+ docker buildx build \
+ --platform=linux/amd64,linux/arm64 \
+ -f $(CONTAINER_FILE) \
+ --tag $(IMAGE_BASE):$(IMAGE_TAG) \
+ --push \
+ .; \
+ elif [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \
+ echo "📦 Building manifest with Podman..."; \
+ $(CONTAINER_RUNTIME) build --platform=linux/amd64,linux/arm64 \
+ -f $(CONTAINER_FILE) \
+ --manifest $(IMAGE_BASE):$(IMAGE_TAG) \
+ .; \
+ echo "💡 To push: podman manifest push $(IMAGE_BASE):$(IMAGE_TAG)"; \
+ else \
+ echo "❌ Multi-arch builds require Docker buildx or Podman"; \
+ exit 1; \
+ fi
+
+# Helper targets for debugging image issues
+image-list:
+ @echo "📋 Images matching $(IMAGE_BASE):"
+ @$(CONTAINER_RUNTIME) images --format "table {{.Repository}}:{{.Tag}}\t{{.ID}}\t{{.Created}}\t{{.Size}}" | \
+ grep -E "(IMAGE|$(IMAGE_BASE))" || echo "No matching images found"
+
+image-clean:
+ @echo "🧹 Removing all $(IMAGE_BASE) images..."
+ @$(CONTAINER_RUNTIME) images --format "{{.Repository}}:{{.Tag}}" | \
+ grep -E "(localhost/)?$(IMAGE_BASE)" | \
+ xargs $(XARGS_FLAGS) $(CONTAINER_RUNTIME) rmi -f 2>/dev/null
+ @echo "✅ Images cleaned"
+
+# Fix image naming issues
+image-retag:
+ @echo "🏷️ Retagging images for consistency..."
+ @if [ "$(CONTAINER_RUNTIME)" = "podman" ]; then \
+ if $(CONTAINER_RUNTIME) image exists $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null; then \
+ $(CONTAINER_RUNTIME) tag $(IMAGE_BASE):$(IMAGE_TAG) $(IMAGE_LOCAL) 2>/dev/null || true; \
+ fi; \
+ else \
+ if $(CONTAINER_RUNTIME) images -q $(IMAGE_LOCAL) 2>/dev/null | grep -q .; then \
+ $(CONTAINER_RUNTIME) tag $(IMAGE_LOCAL) $(IMAGE_BASE):$(IMAGE_TAG) 2>/dev/null || true; \
+ fi; \
+ fi
+ @echo "✅ Images retagged" # This always shows success
+
+# Runtime switching helpers
+use-docker:
+ @echo "export CONTAINER_RUNTIME=docker"
+ @echo "💡 Run: export CONTAINER_RUNTIME=docker"
+
+use-podman:
+ @echo "export CONTAINER_RUNTIME=podman"
+ @echo "💡 Run: export CONTAINER_RUNTIME=podman"
+
+show-runtime:
+ @echo "Current runtime: $(CONTAINER_RUNTIME)"
+ @echo "Detected from: $$(command -v $(CONTAINER_RUNTIME) || echo 'not found')" # Added
+ @echo "To switch: make use-docker or make use-podman"
+
+
+
+# =============================================================================
+# Targets
+# =============================================================================
+
+.PHONY: venv
+venv:
+ @rm -Rf "$(VENV_DIR)"
+ @test -d "$(VENVS_DIR)" || mkdir -p "$(VENVS_DIR)"
+ @python3 -m venv "$(VENV_DIR)"
+ @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m pip install --upgrade pip setuptools pdm uv"
+ @echo -e "✅ Virtual env created.\n💡 Enter it with:\n . $(VENV_DIR)/bin/activate\n"
+
+.PHONY: install
+install: venv
+ $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`)))
+ @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install ."
+
+.PHONY: install-dev
+install-dev: venv
+ $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`)))
+ @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install -e .[dev]"
+
+.PHONY: install-editable
+install-editable: venv
+ $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`)))
+ @/bin/bash -c "source $(VENV_DIR)/bin/activate && python3 -m uv pip install -e .[dev]"
+
+.PHONY: uninstall
+uninstall:
+ pip uninstall $(PACKAGE_NAME)
+
+.PHONY: dist
+dist: clean ## Build wheel + sdist into ./dist
+ @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv
+ @/bin/bash -eu -c "\
+ source $(VENV_DIR)/bin/activate && \
+ python3 -m pip install --quiet --upgrade pip build && \
+ python3 -m build"
+ @echo '🛠 Wheel & sdist written to ./dist'
+
+.PHONY: wheel
+wheel: ## Build wheel only
+ @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv
+ @/bin/bash -eu -c "\
+ source $(VENV_DIR)/bin/activate && \
+ python3 -m pip install --quiet --upgrade pip build && \
+ python3 -m build -w"
+ @echo '🛠 Wheel written to ./dist'
+
+.PHONY: sdist
+sdist: ## Build source distribution only
+ @test -d "$(VENV_DIR)" || $(MAKE) --no-print-directory venv
+ @/bin/bash -eu -c "\
+ source $(VENV_DIR)/bin/activate && \
+ python3 -m pip install --quiet --upgrade pip build && \
+ python3 -m build -s"
+ @echo '🛠 Source distribution written to ./dist'
+
+.PHONY: verify
+verify: dist ## Build, run metadata & manifest checks
+ @/bin/bash -c "source $(VENV_DIR)/bin/activate && \
+ twine check dist/* && \
+ check-manifest && \
+ pyroma -d ."
+ @echo "✅ Package verified - ready to publish."
+
+.PHONY: lint-fix
+lint-fix:
+ @# Handle file arguments
+ @target_file="$(word 2,$(MAKECMDGOALS))"; \
+ if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \
+ actual_target="$$target_file"; \
+ else \
+ actual_target="$(TARGET)"; \
+ fi; \
+ for target in $$(echo $$actual_target); do \
+ if [ ! -e "$$target" ]; then \
+ echo "❌ File/directory not found: $$target"; \
+ exit 1; \
+ fi; \
+ done; \
+ echo "🔧 Fixing lint issues in $$actual_target..."; \
+ $(MAKE) --no-print-directory black TARGET="$$actual_target"; \
+ $(MAKE) --no-print-directory ruff-fix TARGET="$$actual_target"
+
+.PHONY: lint-check
+lint-check:
+ @# Handle file arguments
+ @target_file="$(word 2,$(MAKECMDGOALS))"; \
+ if [ -n "$$target_file" ] && [ "$$target_file" != "" ]; then \
+ actual_target="$$target_file"; \
+ else \
+ actual_target="$(TARGET)"; \
+ fi; \
+ for target in $$(echo $$actual_target); do \
+ if [ ! -e "$$target" ]; then \
+ echo "❌ File/directory not found: $$target"; \
+ exit 1; \
+ fi; \
+ done; \
+ echo "🔧 Fixing lint issues in $$actual_target..."; \
+ $(MAKE) --no-print-directory black-check TARGET="$$actual_target"; \
+ $(MAKE) --no-print-directory ruff-check TARGET="$$actual_target"
+
+.PHONY: lock
+lock:
+ $(foreach bin,$(REQUIRED_BUILD_BINS), $(if $(shell command -v $(bin) 2> /dev/null),,$(error Couldn't find `$(bin)`. Please run `make init`)))
+ uv lock
+
+.PHONY: test
+test:
+ pytest tests
+
+.PHONY: serve
+serve:
+ @echo "Implement me."
+
+.PHONY: build
+build:
+ @$(MAKE) container-build
+
+.PHONY: start
+start:
+ @$(MAKE) container-run
+
+.PHONY: stop
+stop:
+ @$(MAKE) container-stop
+
+.PHONY: clean
+clean:
+ find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete
+ rm -rf *.egg-info .pytest_cache tests/.pytest_cache build dist .ruff_cache .coverage
+
+.PHONY: help
+help:
+ @echo "This Makefile is offered for convenience."
+ @echo ""
+ @echo "The following are the valid targets for this Makefile:"
+ @echo "...install Install package from sources"
+ @echo "...install-dev Install package from sources with dev packages"
+ @echo "...install-editable Install package from sources in editabled mode"
+ @echo "...uninstall Uninstall package"
+ @echo "...dist Clean-build wheel *and* sdist into ./dist"
+ @echo "...wheel Build wheel only"
+ @echo "...sdist Build source distribution only"
+ @echo "...verify Build + twine + check-manifest + pyroma (no upload)"
+ @echo "...serve Start API server locally"
+ @echo "...build Build API server container image"
+ @echo "...start Start the API server container"
+ @echo "...start Stop the API server container"
+ @echo "...lock Lock dependencies"
+ @echo "...lint-fix Check and fix lint errors"
+ @echo "...lint-check Check for lint errors"
+ @echo "...test Run all tests"
+ @echo "...clean Remove all artifacts and builds"
diff --git a/plugins/external/cedar/README.md b/plugins/external/cedar/README.md
new file mode 100644
index 000000000..4d99b6e09
--- /dev/null
+++ b/plugins/external/cedar/README.md
@@ -0,0 +1,332 @@
+# Cedar RBAC Plugin for MCP Gateway
+
+> Author: Shriti Priya
+> Version: 0.1.0
+
+A plugin that evaluates Cedar policies and user‑friendly custom-DSL policies on incoming requests, and then allows or denies those requests using RBAC-based decisions which are enforced in cedar language and using library `cedarpy`.
+
+## Cedar Language
+
+Cedar is an open-source language and specification for defining and evaluating permission policies. It allows you to specify who is authorized to perform which actions within your application.
+For more details: https://www.cedarpolicy.com/en
+
+## RBAC
+
+Role-based access control (RBAC) is an authorization model where permissions are attached to roles (like admin, manager, viewer), and users are assigned to those roles instead of getting permissions directly. This makes access control easier to manage and reason about in larger systems.
+
+## CedarPolicyPlugin
+
+This plugin supports two ways of defining policies in the configuration file, controlled by the `policy_lang` parameter.
+
+### Cedar Mode
+
+`plugins/external/cedar/resources/config.yaml`
+
+When `policy_lang` is set to cedar, policies are written in the Cedar language under the policy key, using the following structure:
+
+```yaml
+ - id: allow-employee-basic-access
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"get_leave_balance" #tool name
+ - Action::"request_certificate"
+ resource:
+ - Server::"askHR" # mcp-server name
+ - Agent::"employee_agent" # agent name
+```
+1. **id** is a unique string identifier for the policy.
+2. **effect** can be either Permit or Forbid and determines whether matching requests are allowed or denied.
+3. **principal** specifies who the policy applies to; here it targets the employee role.
+4. **action** lists one or more tools that the principal is attempting to invoke. It could also be actions controlling the visibility of output, either to see full output or redacted output based on user role.
+5. **resource** lists the servers, agents, prompts and resources that the actions can target.
+
+### Custom DSL mode
+
+`plugins/external/cedar/examples/config-dsl.yaml`
+
+When `policy_lang` is set to `custom_dsl`, policies are written in a compact, human-readable mini-language as a YAML multiline string. This allows non-experts to define role, resource, and action in a single, easy-to-scan block.
+following syntax:
+
+
+## Syntax
+
+Policies use the following basic pattern:
+
+```
+[role::/]
+
+
+```
+
+For example:
+
+```yaml
+ [role:hr:server/hr_tool]
+ update_payroll
+```
+
+In this example, role is hr, resource is server, and action is hr_tool. The line update_payroll represents the specific operation being authorized for that role–resource–action tuple.
+
+
+## Configuration
+
+1. **policy_lang**: Specifies the policy language used, `cedar` or `custom_dsl`.
+2. **policy_output_keywords**: Defines keywords for output views such as `view_full_output` and `view_redacted_output` which can be used in policies or applications to control the output visibility.
+3. **policy_redaction_spec**: Contains a regex pattern for redaction; in this case, the pattern matches currency-like strings (e.g., "$123,456") for potential redaction in the policy output, protecting sensitive information.
+4. **policy**: Defines the RBAC policy
+
+## Installation
+
+1. In the folder `plugins/external/cedar`, copy `.env.example` to `.env` file.
+2. If you are using `policy_lang` to be `cedar`, add the plugin configuration to `plugins/external/cedar/resources/plugins/config.yaml`:
+
+```yaml
+plugins:
+ - name: "CedarPolicyPlugin"
+ kind: "cedarpolicyplugin.plugin.CedarPolicyPlugin"
+ description: "A plugin that does policy decision and enforcement using cedar"
+ version: "0.1.0"
+ author: "Shriti Priya"
+ hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke", "resource_pre_fetch", "resource_post_fetch"]
+ tags: ["plugin"]
+ mode: "enforce" # enforce | permissive | disabled
+ priority: 150
+ conditions:
+ # Apply to specific tools/servers
+ - server_ids: [] # Apply to all servers
+ tenant_ids: [] # Apply to all tenants
+ config:
+ policy_lang: cedar
+ policy_output_keywords:
+ view_full: "view_full_output"
+ view_redacted: "view_redacted_output"
+ policy_redaction_spec:
+ pattern: '"\$\d{1,}(,\d{1,})*"' # provide regex, if none, then replace all
+ policy:
+ ### Tool invocation policies ###
+ - id: allow-employee-basic-access
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"get_leave_balance" #tool name
+ - Action::"request_certificate"
+ resource:
+ - Server::"askHR" # mcp-server name
+ - Agent::"employee_agent" # agent name
+
+ - id: allow-manager-full-access
+ effect: Permit
+ principal: Role::"manager"
+ action:
+ - Action::"get_leave_balance"
+ - Action::"approve_leave"
+ - Action::"promote_employee"
+ - Action::"view_performance"
+ - Action::"view_full_output"
+ resource:
+ - Agent::"manager_agent"
+ - Server::"payroll_tool"
+
+ - id: allow-hr-hr_tool
+ effect: Permit
+ principal: Role::"hr"
+ action:
+ - Action::"update_payroll"
+ - Action::"view_performance"
+ - Action::"view_full_output"
+ resource: Server::"hr_tool"
+
+ - id: redact-non-manager-views
+ effect: Permit
+ principal: Role::"employee"
+ action: Action::"view_redacted_output"
+ resource:
+ - Server::"payroll_tool"
+ - Agent::"manager_agent"
+ - Server::"askHR"
+
+ ### Resource invocation policies ###
+ - id: allow-admin-resources # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Resource::""https://example.com/data"" #Resource::
+
+ - id: allow-employee-redacted-resources # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Resource::""https://example.com/data"" #Resource::
+
+ ### Prompt invocation policies ###
+ - id: allow-admin-prompts # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Prompt::"judge_prompts" #Prompt::
+
+
+ - id: allow-employee-redacted-prompts # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Prompt::"judge_prompts" #Prompt::
+
+```
+
+#### Tool Invocation Policies
+
+For the RBAC policy related to `tool_pre_invoke` and `tool_post_invoke`
+Example:
+```yaml
+ - id: allow-employee-basic-access
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"get_leave_balance" #tool name
+ - Action::"request_certificate"
+ resource:
+ - Server::"askHR" # mcp-server name
+ - Agent::"employee_agent" # agent name
+```
+
+Here, user with role `employee` (**Role**) is only allowed to invoke tool `get_leave_balance` (**Action**) belonging to the MCP server or (**Server**).
+
+In another policy defined for tools
+
+```yaml
+
+ - id: allow-hr-hr_tool
+ effect: Permit
+ principal: Role::"hr"
+ action:
+ - Action::"update_payroll"
+ - Action::"view_performance"
+ - Action::"view_full_output"
+ resource: Server::"hr_tool"
+
+ - id: redact-non-manager-views
+ effect: Permit
+ principal: Role::"employee"
+ action: Action::"view_redacted_output"
+ resource:
+ - Server::"payroll_tool"
+ - Agent::"manager_agent"
+ - Server::"askHR"
+```
+
+
+The actions like `view_full_output` and `view_redacted_output` has been used. This basically controls the
+level of output visibile to the user. In the above policy, user with role `hr` is only allowed to view the output of `update_payroll`. Similary for the second policy, user with role `employee` is only allowed to view redacted output of the tool.
+
+
+#### Prompt Invocation Policies
+
+
+```yaml
+
+ ### Prompt invocation policies ###
+ - id: allow-admin-prompts # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Prompt::"judge_prompts" #Prompt::
+
+
+ - id: allow-employee-redacted-prompts # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Prompt::"judge_prompts" #Prompt::
+```
+
+Here, in the above polcicy, given a prompt template `judge_prompts`, user of role `admin` is only allowed to view full prompt. However, if a user is of role `employee`, then it could only see redacted version of the prompt.
+
+
+#### Resource Invocation Policies
+
+**NOTE:** Please don't be confused with the word resource in cedar to the word resource in MCP ContextForge.
+
+```yaml
+
+ - id: allow-admin-resources # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Resource::"https://example.com/data" #Resource::
+
+ - id: allow-employee-redacted-resources # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Resource::"https://example.com/data" #Resource::
+```
+
+Here, `Resource` word used in policy, is if resource hooks are invoked. So, in the above policy,
+user with role `admin` is only allowed to view full output of uri `https://example.com/data`. Where, the user is of `employee` role, it can only see the redacted versionaaaaa of the resource output.
+
+
+#### policy_output_keywords
+
+```
+ view_full: "view_full_output"
+ view_redacted: "view_redacted_output"
+```
+
+has been provided, so everytime a user defines a policy, if it wants to control the output visibility of
+any of the tool, prompt, resource or agent in MCP gateway, it can provide the keyword, it's supposed to use in the policy in `policy_output_keywords`. CedarPolicyPlugin will internally use this mapping to redact or fully display the tool, prompt or resource response in post hooks.
+
+
+
+
+3. Now, the policy and plugin configurations are defined in `resources/config.yaml` file, next step is build this as an external MCP server.
+
+* `make venv`: This will create a virtual environment to develop or build your plugin.
+* `make install && make install-dev`: To install all the required libraries in the environment.
+* `make build`: This will build a docker image named `mcpgateway/cedarpolicyplugin`
+* `make start`: This will start the cedarpolicyplugin container.
+
+This confirms that your container is running fine:
+```
+WARNING:mcpgateway.observability:OpenTelemetry not installed. Proceeding with graceful fallbacks.
+INFO: Started server process [9]
+INFO: Waiting for application startup.
+INFO: Application startup complete.
+INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
+INFO: 127.0.0.1:55196 - "GET /health HTTP/1.1" 200 OK
+
+```
+
+4. Now, you can add this external plugin configuration, in `plugins/config.yaml`:
+3. The next step is to enable the opa plugin which you can do by adding `PLUGINS_ENABLED=true` and the following blob in `plugins/config.yaml` file. This will indicate that OPA Plugin is running as an external MCP server.
+
+ ```yaml
+ - name: "CedarPolicyPlugin"
+ kind: "external"
+ priority: 10 # adjust the priority
+ mcp:
+ proto: STREAMABLEHTTP
+ url: http://127.0.0.1:8000/mcp
+ ```
+
+## Testing
+
+There are set of test cases in the `cedar/tests` folder. The file named `test_cedarpolicyplugin.py` file which contains detailed test cases for RBAC policies enforced on tools, prompts and resources.
+run `make test` to run all the test cases.
+
+
+
+## Difference from OPAPlugin
+
+The OPA plugin runs an OPA server to enforce policies, whereas the Cedar plugin uses the `cedarpy` library and performs policy enforcement locally without requiring an external service.
+OPA plugin requires to know `rego` to define policies by user while the `Cedar` plugin can be defined either in `cedar` or user friendly `custom_dsl` language.
+Right now, the cedar plugin enforces RBAC policies and it could be extended to enforce ABAC policies using the same plugin.
diff --git a/plugins/external/cedar/cedarpolicyplugin/__init__.py b/plugins/external/cedar/cedarpolicyplugin/__init__.py
new file mode 100644
index 000000000..52cdda086
--- /dev/null
+++ b/plugins/external/cedar/cedarpolicyplugin/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+"""MCP Gateway CedarPolicyPlugin Plugin - A plugin that does policy decision and enforcement using cedar.
+
+Copyright 2025
+SPDX-License-Identifier: Apache-2.0
+Authors: Shriti Priya
+
+"""
+
+import importlib.metadata
+
+# Package version
+try:
+ __version__ = importlib.metadata.version("cedarpolicyplugin")
+except Exception:
+ __version__ = "0.1.0"
+
+__author__ = "Shriti Priya"
+__copyright__ = "Copyright 2025"
+__license__ = "Apache 2.0"
+__description__ = "A plugin that does policy decision and enforcement using cedar"
+__url__ = "https://ibm.github.io/mcp-context-forge/"
+__download_url__ = "https://github.com/IBM/mcp-context-forge"
+__packages__ = ["cedarpolicyplugin"]
diff --git a/plugins/external/cedar/cedarpolicyplugin/plugin-manifest.yaml b/plugins/external/cedar/cedarpolicyplugin/plugin-manifest.yaml
new file mode 100644
index 000000000..38ec3ceea
--- /dev/null
+++ b/plugins/external/cedar/cedarpolicyplugin/plugin-manifest.yaml
@@ -0,0 +1,9 @@
+description: "A plugin that does policy decision and enforcement using cedar"
+author: "Shriti Priya"
+version: "0.1.0"
+available_hooks:
+ - "prompt_pre_hook"
+ - "prompt_post_hook"
+ - "tool_pre_hook"
+ - "tool_post_hook"
+default_configs:
diff --git a/plugins/external/cedar/cedarpolicyplugin/plugin.py b/plugins/external/cedar/cedarpolicyplugin/plugin.py
new file mode 100644
index 000000000..852e745d8
--- /dev/null
+++ b/plugins/external/cedar/cedarpolicyplugin/plugin.py
@@ -0,0 +1,678 @@
+# -*- coding: utf-8 -*-
+"""A plugin that does policy decision and enforcement using cedar.
+
+Copyright 2025
+SPDX-License-Identifier: Apache-2.0
+Authors: Shriti Priya
+
+This module loads configurations for plugins.
+"""
+
+# Standard
+from enum import Enum
+from typing import Any
+import re
+
+# Third-Party
+from cedarpolicyplugin.schema import CedarConfig, CedarInput
+from cedarpy import is_authorized, AuthzResult, Decision
+from urllib.parse import urlparse
+
+# First-Party
+from mcpgateway.plugins.framework import (
+ Plugin,
+ PluginError,
+ PluginErrorModel,
+ ToolPreInvokePayload,
+ ToolPreInvokeResult,
+ ToolPostInvokePayload,
+ ToolPostInvokeResult,
+ PromptPosthookPayload,
+ PromptPosthookResult,
+ PromptPrehookResult,
+ PromptPrehookPayload,
+)
+from mcpgateway.plugins.framework.hooks.resources import ResourcePreFetchPayload, ResourcePostFetchPayload, ResourcePreFetchResult, ResourcePostFetchResult
+from mcpgateway.plugins.framework import PluginConfig, PluginContext, PluginViolation
+from mcpgateway.services.logging_service import LoggingService
+
+
+# Initialize logging service first
+logging_service = LoggingService()
+logger = logging_service.get_logger(__name__)
+
+
+class CedarCodes(str, Enum):
+ """CedarCodes implementation."""
+
+ ALLOW_CODE = "ALLOW"
+ DENIAL_CODE = "DENY"
+ AUDIT_CODE = "AUDIT"
+ REQUIRES_HUMAN_APPROVAL_CODE = "REQUIRES_APPROVAL"
+
+
+class CedarResponseTemplates(str, Enum):
+ """CedarResponseTemplates implementation."""
+
+ CEDAR_REASON = "Cedar policy denied for {hook_type}"
+ CEDAR_DESC = "{hook_type} not allowed"
+
+
+class CedarResourceTemplates(str, Enum):
+ """CedarResourceTemplates implementation."""
+
+ SERVER = 'Server::"{resource_type}"'
+ AGENT = 'Agent::"{resource_type}"'
+ PROMPT = 'Prompt::"{resource_type}"'
+ RESOURCE = 'Resource::"{resource_type}"'
+
+
+class CedarErrorCodes(str, Enum):
+ """CedarPolicyPlugin errors"""
+
+ UNSUPPORTED_RESOURCE_TYPE = "Unspecified resource types, accepted resources server, prompt, agent and resource"
+ UNSPECIFIED_USER_ROLE = "User role is not defined"
+ UNSPECIFIED_POLICY = "No policy has been provided"
+ UNSPECIFIED_OUTPUT_ACTION = "Unspecified output action in policy configuration"
+ UNSPECIFIED_SERVER = "Unspecified server for tool request"
+ UNSUPPORTED_CONTENT_TYPE = "Unsupported content type"
+
+
+class CedarPolicyPlugin(Plugin):
+ """A plugin that does policy decision and enforcement using cedar."""
+
+ def __init__(self, config: PluginConfig):
+ """Entry init block for plugin.
+
+ Args:
+ logger: logger that the skill can make use of
+ config: the skill configuration
+ """
+ super().__init__(config)
+ self.cedar_config = CedarConfig.model_validate(self._config.config)
+ self.cedar_context_key = "cedar_policy_context"
+ self.jwt_info = {}
+ logger.info(f"CedarPolicyPlugin initialised with configuration {self.cedar_config}")
+
+ def _set_jwt_info(self, user_role_mapping: dict) -> None:
+ """Sets user role mapping information from jwt tokens
+
+ Args:
+ info(dict): with user mappings
+ """
+ self.jwt_info["users"] = user_role_mapping
+
+ def _extract_payload_key(self, content: Any = None, key: str = None, result: dict[str, list] = None) -> None:
+ """Function to extract values of passed in key in the payload recursively based on if the content is of type list, dict
+ str or pydantic structure. The value is inplace updated in result.
+
+ Args:
+ content: The content of post hook results.
+ key: The key for which value needs to be extracted for.
+ result: A list of all the values for a key.
+ """
+ if isinstance(content, list):
+ for element in content:
+ if isinstance(element, dict) and key in element:
+ self._extract_payload_key(element, key, result)
+ elif isinstance(content, dict):
+ if key in content or hasattr(content, key):
+ result[key].append(content[key])
+ elif isinstance(content, str):
+ result[key].append(content)
+ elif hasattr(content, key):
+ result[key].append(getattr(content, key))
+ else:
+ logger.error(f"{CedarErrorCodes.UNSUPPORTED_CONTENT_TYPE.value}: {type(content)}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSUPPORTED_CONTENT_TYPE.value, plugin_name="CedarPolicyPlugin"))
+
+ def _evaluate_policy(self, request: dict, policy_expr: str) -> str:
+ """Function that evaluates and enforce cedar policy using is_authorized function in cedarpy library
+ Args:
+ request(dict): The request dict consisting of principal, action, resource or context keys.
+ policy_exp(str): The policy expression to evaluate the request on
+
+ Returns:
+ decision(str): "Allow" or "Deny"
+ """
+ result: AuthzResult = is_authorized(request, policy_expr, [])
+ decision = "Allow" if result.decision == Decision.Allow else "Deny"
+ return decision
+
+ def _yamlpolicy2text(self, policies: list) -> str:
+ """Function to convert yaml representation of policies to text
+ Args:
+ policies(list): A list of cedar policies with dict values consisting of individual policies
+
+ Returns:
+ cedar_policy_text(str): string representation of policy
+ """
+ cedar_policy_text = ""
+ for policy in policies:
+ actions = policy["action"] if isinstance(policy["action"], list) else [policy["action"]]
+ resources = policy["resource"] if isinstance(policy["resource"], list) else [policy["resource"]]
+
+ for res in resources:
+ actions_str = ", ".join(actions)
+ cedar_policy_text += "permit(\n"
+ cedar_policy_text += f' principal == {policy["principal"]},\n'
+ cedar_policy_text += f" action in [{actions_str}],\n"
+ cedar_policy_text += f" resource == {res}\n"
+ cedar_policy_text += ");\n\n"
+
+ return cedar_policy_text
+
+ def _dsl2cedar(self, policy_string: str) -> str:
+ """Function to convert custom dsl representation of policies to cedar
+ Args:
+ policy_string: string representation of policies
+
+ Returns:
+ cedar_policy_text(str): string representation of policy
+ """
+ lines = [line.strip() for line in policy_string.splitlines() if line.strip()]
+ policies = []
+ current_role = None
+ current_actions = []
+ resource_category = None
+ resource_name = None
+
+ pattern = r"\[role:([A-Za-z0-9_]+):(resource|prompt|server|agent)/([^\]]+)\]"
+ for line in lines:
+ match = re.match(pattern, line)
+ if match:
+ if current_role and resource_category and resource_name and current_actions:
+ resource_category = resource_category.capitalize()
+ policies.append(
+ {
+ "id": f"allow-{current_role}-{resource_category}",
+ "effect": "Permit",
+ "principal": f'Role::"{current_role}"',
+ "action": [f'Action::"{a}"' for a in current_actions],
+ "resource": f'{resource_category}::"{resource_name}"',
+ }
+ )
+ current_role, resource_category, resource_name = match.groups()
+ current_actions = []
+ else:
+ current_actions.append(line)
+ if current_role and resource_category and resource_name and current_actions:
+ resource_category = resource_category.capitalize()
+ policies.append(
+ {
+ "id": f"allow-{current_role}-{resource_category}",
+ "effect": "Permit",
+ "principal": f'Role::"{current_role}"',
+ "action": [f'Action::"{a}"' for a in current_actions],
+ "resource": f'{resource_category}::"{resource_name}"',
+ }
+ )
+
+ cedar_policy_text = self._yamlpolicy2text(policies)
+ return cedar_policy_text
+
+ def _preprocess_request(self, user: str, action: str, resource: str, hook_type: str) -> CedarInput:
+ """Function to pre process request into a format that cedar accepts
+ Args:
+ user(str): name of the user
+ action(str): action requested by the user
+ resource(str): resource requested by the user
+ hook_type(str): the hook type on which invocation is made
+
+ Returns:
+ request(CedarInput): pydantic representation of request as excpected by cedar policy
+ """
+ user_role = ""
+ if hook_type in ["tool_post_invoke", "tool_pre_invoke"]:
+ resource_expr = CedarResourceTemplates.SERVER.format(resource_type=resource)
+ elif hook_type in ["agent_post_invoke", "agent_pre_invoke"]:
+ resource_expr = CedarResourceTemplates.AGENT.format(resource_type=resource)
+ elif hook_type in ["resource_post_fetch", "resource_pre_fetch"]:
+ resource_expr = CedarResourceTemplates.RESOURCE.format(resource_type=resource)
+ elif hook_type in ["prompt_post_fetch", "prompt_pre_fetch"]:
+ resource_expr = CedarResourceTemplates.PROMPT.format(resource_type=resource)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSUPPORTED_RESOURCE_TYPE.value}: {hook_type}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSUPPORTED_RESOURCE_TYPE.value, plugin_name="CedarPolicyPlugin"))
+
+ if len(self.jwt_info) > 0 and "users" in self.jwt_info:
+ user_role = self.jwt_info["users"].get(user)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_USER_ROLE.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_USER_ROLE.value, plugin_name="CedarPolicyPlugin"))
+
+ principal_expr = f'Role::"{user_role}"'
+ action_expr = f'Action::"{action}"'
+ request = CedarInput(principal=principal_expr, action=action_expr, resource=resource_expr, context={}).model_dump()
+ return request
+
+ def _redact_output(self, payload: str, pattern: str) -> str:
+ """Function that redacts the output of prompt, tool or resource
+ NOTE: It's an extremely simple logic for redaction, could be replaced with more advanced
+ as per need.
+ Args:
+ payload(str): payload or output
+ pattern(str): regex expression to replace
+ Returns:
+ redacted_text(str): redacted representation of payload string
+ """
+ redacted_text = ""
+ if not pattern:
+ redacted_text = payload
+ elif pattern == "all":
+ redacted_text = "[REDACTED]"
+ else:
+ redacted_text = re.sub(pattern, "[REDACTED]", payload)
+ return redacted_text
+
+ async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginContext) -> PromptPrehookResult:
+ """The plugin hook run before a prompt is retrieved and rendered.
+
+ Args:
+ payload: The prompt payload to be analyzed.
+ context: contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the prompt can proceed.
+ """
+ hook_type = "prompt_pre_fetch"
+ logger.info(f"Processing {hook_type} for '{payload.args}' with {len(payload.args) if payload.args else 0}")
+ logger.info(f"Processing context {context}")
+
+ if not payload.args:
+ return PromptPrehookResult()
+
+ policy = None
+ user = ""
+ result_full = None
+ result_redacted = None
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+
+ if self.cedar_config.policy_output_keywords:
+ view_full = self.cedar_config.policy_output_keywords.get("view_full", None)
+ view_redacted = self.cedar_config.policy_output_keywords.get("view_redacted", None)
+ if not view_full and not view_redacted:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value, plugin_name="CedarPolicyPlugin"))
+ if view_full and policy:
+ request = self._preprocess_request(user, view_full, payload.prompt_id, hook_type)
+ result_full = self._evaluate_policy(request, policy)
+ if view_redacted and policy:
+ request = self._preprocess_request(user, view_redacted, payload.prompt_id, hook_type)
+ result_redacted = self._evaluate_policy(request, policy)
+
+ if result_full == Decision.Deny.value and result_redacted == Decision.Deny.value:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return PromptPrehookResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return PromptPrehookResult(continue_processing=True)
+
+ async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult:
+ """Plugin hook run after a prompt is rendered.
+
+ Args:
+ payload: The prompt payload to be analyzed.
+ context: Contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the prompt can proceed.
+ """
+ hook_type = "prompt_post_fetch"
+ logger.info(f"Processing {hook_type} for '{payload.result}'")
+ logger.info(f"Processing context {context}")
+
+ if not payload.result:
+ return PromptPosthookResult()
+
+ policy = None
+ user = ""
+ result_full = None
+ result_redacted = None
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+
+ if self.cedar_config.policy_output_keywords:
+ view_full = self.cedar_config.policy_output_keywords.get("view_full", None)
+ view_redacted = self.cedar_config.policy_output_keywords.get("view_redacted", None)
+ if not view_full and not view_redacted:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value, plugin_name="CedarPolicyPlugin"))
+ if view_full and policy:
+ request = self._preprocess_request(user, view_full, payload.prompt_id, hook_type)
+ result_full = self._evaluate_policy(request, policy)
+ if view_redacted and policy:
+ request = self._preprocess_request(user, view_redacted, payload.prompt_id, hook_type)
+ result_redacted = self._evaluate_policy(request, policy)
+
+ if result_full == Decision.Allow.value:
+ return PromptPosthookResult(continue_processing=True)
+
+ elif result_redacted == Decision.Allow.value:
+ if payload.result.messages:
+ for index, message in enumerate(payload.result.messages):
+ value = self._redact_output(message.content.text, self.cedar_config.policy_redaction_spec.pattern)
+ payload.result.messages[index].content.text = value
+ return PromptPosthookResult(modified_payload=payload, continue_processing=True)
+ else:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return PromptPosthookResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return PromptPosthookResult(continue_processing=True)
+
+ async def tool_pre_invoke(self, payload: ToolPreInvokePayload, context: PluginContext) -> ToolPreInvokeResult:
+ """Plugin hook run before a tool is invoked.
+
+ Args:
+ payload: The tool payload to be analyzed.
+ context: Contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the tool can proceed.
+ """
+ hook_type = "tool_pre_invoke"
+ logger.info(f"Processing {hook_type} for '{payload.args}' with {len(payload.args) if payload.args else 0}")
+ logger.info(f"Processing context {context}")
+
+ if not payload.args:
+ return ToolPreInvokeResult()
+
+ policy = None
+ user = ""
+ server_id = ""
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+ server_id = context.global_context.server_id
+
+ if server_id:
+ request = self._preprocess_request(user, payload.name, server_id, hook_type)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_SERVER.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_SERVER.value, plugin_name="CedarPolicyPlugin"))
+
+ if policy:
+ decision = self._evaluate_policy(request, policy)
+ if decision == Decision.Deny.value:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return ToolPreInvokeResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return ToolPreInvokeResult(continue_processing=True)
+
+ async def tool_post_invoke(self, payload: ToolPostInvokePayload, context: PluginContext) -> ToolPostInvokeResult:
+ """Plugin hook run after a tool is invoked.
+
+ Args:
+ payload: The tool result payload to be analyzed.
+ context: Contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the tool result should proceed.
+ """
+
+ hook_type = "tool_post_invoke"
+ logger.info(f"Processing {hook_type} for '{payload.result}' with {len(payload.result) if payload.result else 0}")
+ logger.info(f"Processing context {context}")
+
+ if not payload.result:
+ return ToolPostInvokeResult()
+
+ policy = None
+ user = ""
+ server_id = ""
+ result_full = None
+ result_redacted = None
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+ server_id = context.global_context.server_id
+
+ if self.cedar_config.policy_output_keywords:
+ view_full = self.cedar_config.policy_output_keywords.get("view_full", None)
+ view_redacted = self.cedar_config.policy_output_keywords.get("view_redacted", None)
+ if not view_full and not view_redacted:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value, plugin_name="CedarPolicyPlugin"))
+ if view_full and policy:
+ request = self._preprocess_request(user, view_full, server_id, hook_type)
+ result_full = self._evaluate_policy(request, policy)
+ if view_redacted and policy:
+ request = self._preprocess_request(user, view_redacted, server_id, hook_type)
+ result_redacted = self._evaluate_policy(request, policy)
+
+ # Evaluate Policy and based on that redact output
+ if policy:
+ request = self._preprocess_request(user, payload.name, server_id, hook_type)
+ result_action = self._evaluate_policy(request, policy)
+ # Check if full output view is allowed by policy
+ if result_action == Decision.Allow.value:
+ if result_full == Decision.Allow.value:
+ return ToolPostInvokeResult(continue_processing=True)
+ if result_redacted == Decision.Allow.value:
+ if payload.result and isinstance(payload.result, dict):
+ for key in payload.result:
+ if isinstance(payload.result[key], str):
+ value = self._redact_output(payload.result[key], self.cedar_config.policy_redaction_spec.pattern)
+ payload.result[key] = value
+ elif payload.result and isinstance(payload.result, str):
+ payload.result = self._redact_output(payload.result, self.cedar_config.policy_redaction_spec.pattern)
+ return ToolPostInvokeResult(continue_processing=True, modified_payload=payload)
+ # If none of the redacted or full output views are allowed by policy then deny
+ else:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return ToolPostInvokeResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return ToolPostInvokeResult(continue_processing=True)
+
+ async def resource_pre_fetch(self, payload: ResourcePreFetchPayload, context: PluginContext) -> ResourcePreFetchResult:
+ """OPA Plugin hook that runs after resource pre fetch. This hook takes in payload and context and further evaluates rego
+ policies on the input by sending the request to opa server.
+
+ Args:
+ payload: The resource pre fetch input or payload to be analyzed.
+ context: Contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the resource input can be passed further.
+ """
+
+ hook_type = "resource_pre_fetch"
+ logger.info(f"Processing {hook_type} for '{payload.uri}'")
+ logger.info(f"Processing context {context}")
+
+ if not payload.uri:
+ return ResourcePreFetchResult()
+
+ try:
+ parsed = urlparse(payload.uri)
+ except Exception as e:
+ violation = PluginViolation(reason="Invalid URI", description=f"Could not parse resource URI: {e}", code="INVALID_URI", details={"uri": payload.uri, "error": str(e)})
+ return ResourcePreFetchResult(continue_processing=False, violation=violation)
+
+ # Check if URI has a scheme
+ if not parsed.scheme:
+ violation = PluginViolation(reason="Invalid URI format", description="URI must have a valid scheme (protocol)", code="INVALID_URI", details={"uri": payload.uri})
+ return ResourcePreFetchResult(continue_processing=False, violation=violation)
+
+ policy = None
+ user = ""
+ result_full = None
+ result_redacted = None
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+
+ if self.cedar_config.policy_output_keywords:
+ view_full = self.cedar_config.policy_output_keywords.get("view_full", None)
+ view_redacted = self.cedar_config.policy_output_keywords.get("view_redacted", None)
+ if not view_full and not view_redacted:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value, plugin_name="CedarPolicyPlugin"))
+ if view_full and policy:
+ request = self._preprocess_request(user, view_full, payload.uri, hook_type)
+ result_full = self._evaluate_policy(request, policy)
+ if view_redacted and policy:
+ request = self._preprocess_request(user, view_redacted, payload.uri, hook_type)
+ result_redacted = self._evaluate_policy(request, policy)
+
+ if result_full == Decision.Deny.value and result_redacted == Decision.Deny.value:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return ResourcePreFetchResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return ResourcePreFetchResult(continue_processing=True)
+
+ async def resource_post_fetch(self, payload: ResourcePostFetchPayload, context: PluginContext) -> ResourcePostFetchResult:
+ """OPA Plugin hook that runs after resource post fetch. This hook takes in payload and context and further evaluates rego
+ policies on the output by sending the request to opa server.
+
+ Args:
+ payload: The resource post fetch output or payload to be analyzed.
+ context: Contextual information about the hook call.
+
+ Returns:
+ The result of the plugin's analysis, including whether the resource output can be passed further.
+ """
+ hook_type = "resource_post_fetch"
+ logger.info(f"Processing {hook_type} for '{payload.uri}'")
+ logger.info(f"Processing context {context}")
+
+ policy = None
+ user = ""
+ result_full = None
+ result_redacted = None
+
+ if self.cedar_config.policy_lang == "cedar":
+ if self.cedar_config.policy:
+ policy = self._yamlpolicy2text(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+ if self.cedar_config.policy_lang == "custom_dsl":
+ if self.cedar_config.policy:
+ policy = self._dsl2cedar(self.cedar_config.policy)
+ else:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_POLICY.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_POLICY.value, plugin_name="CedarPolicyPlugin"))
+
+ if context.global_context.user:
+ user = context.global_context.user
+
+ if self.cedar_config.policy_output_keywords:
+ view_full = self.cedar_config.policy_output_keywords.get("view_full", None)
+ view_redacted = self.cedar_config.policy_output_keywords.get("view_redacted", None)
+ if not view_full and not view_redacted:
+ logger.error(f"{CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value}")
+ raise PluginError(PluginErrorModel(message=CedarErrorCodes.UNSPECIFIED_OUTPUT_ACTION.value, plugin_name="CedarPolicyPlugin"))
+ if view_full and policy:
+ request = self._preprocess_request(user, view_full, payload.uri, hook_type)
+ result_full = self._evaluate_policy(request, policy)
+ if view_redacted and policy:
+ request = self._preprocess_request(user, view_redacted, payload.uri, hook_type)
+ result_redacted = self._evaluate_policy(request, policy)
+
+ if result_full == Decision.Allow.value:
+ return ResourcePostFetchResult(continue_processing=True)
+
+ elif result_redacted == Decision.Allow.value:
+ if payload.content:
+ if hasattr(payload.content, "text"):
+ value = self._redact_output(payload.content.text, self.cedar_config.policy_redaction_spec.pattern)
+ payload.content.text = value
+ return ResourcePostFetchResult(modified_payload=payload, continue_processing=True)
+
+ else:
+ violation = PluginViolation(
+ reason=CedarResponseTemplates.CEDAR_REASON.format(hook_type=hook_type),
+ description=CedarResponseTemplates.CEDAR_DESC.format(hook_type=hook_type),
+ code=CedarCodes.DENIAL_CODE,
+ details={},
+ )
+ return ResourcePostFetchResult(modified_payload=payload, violation=violation, continue_processing=False)
+ return ResourcePostFetchResult(continue_processing=True)
diff --git a/plugins/external/cedar/cedarpolicyplugin/schema.py b/plugins/external/cedar/cedarpolicyplugin/schema.py
new file mode 100644
index 000000000..9274e7674
--- /dev/null
+++ b/plugins/external/cedar/cedarpolicyplugin/schema.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+"""A schema file for OPA plugin.
+
+Copyright 2025
+SPDX-License-Identifier: Apache-2.0
+Authors: Shriti Priya
+
+This module defines schema for Cedar plugin.
+"""
+
+# Standard
+from typing import Any, Optional, Union
+
+# Third-Party
+from pydantic import BaseModel
+
+
+class CedarInput(BaseModel):
+ """BaseOPAInputKeys
+
+ Attributes:
+ user (str) : specifying the user
+ action (str): specifies the action
+ resource (str): specifies the resource
+ context (Optional[dict[str, Any]]) : context provided for policy evaluation.
+ """
+
+ principal: str = ""
+ action: str = ""
+ resource: str = ""
+ context: Optional[dict[Any, Any]] = None
+
+
+class Redaction(BaseModel):
+ """Configuration for Redaction
+
+ Attributes:
+ pattern (str) : pattern detected in output to redact
+ """
+
+ pattern: str = ""
+
+
+class CedarConfig(BaseModel):
+ """Configuration for the Cedar plugin.
+
+ Attributes:
+ policy_land (str) : cedar or custom_dsl. If policy is represented in cedar mode or custom_dsl mode
+ policy (Union[list, str]): RBAC policy defined
+ policy_output_keywords (dict): this is to internally check if certain type of views are allowed for outputs
+ policy_redaction_spec (Redaction) : pattern or other parameters provided to redact the output
+ """
+
+ policy_lang: str = "None"
+ policy: Union[list, str] = None
+ policy_output_keywords: Optional[dict] = None
+ policy_redaction_spec: Optional[Redaction] = None
diff --git a/plugins/external/cedar/examples/config-dsl.yaml b/plugins/external/cedar/examples/config-dsl.yaml
new file mode 100644
index 000000000..e1584c8af
--- /dev/null
+++ b/plugins/external/cedar/examples/config-dsl.yaml
@@ -0,0 +1,43 @@
+plugins:
+ - name: "CedarPolicyPlugin"
+ kind: "cedarpolicyplugin.plugin.CedarPolicyPlugin"
+ description: "A plugin that does policy decision and enforcement using cedar"
+ version: "0.1.0"
+ author: "Shriti Priya"
+ hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"]
+ tags: ["plugin"]
+ mode: "enforce" # enforce | permissive | disabled
+ priority: 150
+ conditions:
+ # Apply to specific tools/servers
+ - server_ids: [] # Apply to all servers
+ tenant_ids: [] # Apply to all tenants
+ config:
+ policy_lang: custom_dsl
+ policy_output_keywords:
+ view_full: "view_full_output"
+ view_redacted: "view_redacted_output"
+ policy_redaction_spec:
+ pattern: '"\$\d{1,}(,\d{1,})*"' # provide regex, if none, then replace all
+ policy: |
+ [role:hr:server/hr_tool]
+ update_payroll
+
+ [role:admin:resource/example.com/data]
+ view_full_output
+
+ [role:admin:prompt/judge_prompts]
+ view_full_output
+
+
+# Plugin directories to scan
+plugin_dirs:
+ - "cedarpolicyplugin"
+
+# Global plugin settings
+plugin_settings:
+ parallel_execution_within_band: true
+ plugin_timeout: 30
+ fail_on_plugin_error: false
+ enable_plugin_api: true
+ plugin_health_check_interval: 60
diff --git a/plugins/external/cedar/pyproject.toml b/plugins/external/cedar/pyproject.toml
new file mode 100644
index 000000000..334583a7a
--- /dev/null
+++ b/plugins/external/cedar/pyproject.toml
@@ -0,0 +1,99 @@
+# ----------------------------------------------------------------
+# 💡 Build system (PEP 517)
+# - setuptools ≥ 77 gives SPDX licence support (PEP 639)
+# - wheel is needed by most build front-ends
+# ----------------------------------------------------------------
+[build-system]
+requires = ["setuptools>=77", "wheel"]
+build-backend = "setuptools.build_meta"
+
+# ----------------------------------------------------------------
+# 📦 Core project metadata (PEP 621)
+# ----------------------------------------------------------------
+[project]
+name = "cedarpolicyplugin"
+version = "0.1.0"
+description = "A plugin that does policy decision and enforcement using cedar"
+keywords = ["MCP","API","gateway","tools",
+ "agents","agentic ai","model context protocol","multi-agent","fastapi",
+ "json-rpc","sse","websocket","federation","security","authentication"
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Framework :: FastAPI",
+ "Framework :: AsyncIO",
+ "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
+ "Topic :: Software Development :: Libraries :: Application Frameworks"
+]
+readme = "README.md"
+requires-python = ">=3.11,<3.14"
+license = "Apache-2.0"
+license-files = ["LICENSE"]
+
+maintainers = [
+ {name = "Shriti Priya", email = "shritip@ibm.com"}
+]
+
+authors = [
+ {name = "Shriti Priya", email = "shritip@ibm.com"}
+]
+
+dependencies = [
+ "mcp>=1.16.0",
+ "mcp-contextforge-gateway",
+ "cedarpy>=4.1.0"
+]
+
+# URLs
+[project.urls]
+Homepage = "https://ibm.github.io/mcp-context-forge/"
+Documentation = "https://ibm.github.io/mcp-context-forge/"
+Repository = "https://github.com/IBM/mcp-context-forge"
+"Bug Tracker" = "https://github.com/IBM/mcp-context-forge/issues"
+Changelog = "https://github.com/IBM/mcp-context-forge/blob/main/CHANGELOG.md"
+
+[tool.uv.sources]
+mcp-contextforge-gateway = { git = "https://github.com/IBM/mcp-context-forge.git", rev = "main" }
+
+# ----------------------------------------------------------------
+# Optional dependency groups (extras)
+# ----------------------------------------------------------------
+[project.optional-dependencies]
+dev = [
+ "black>=25.1.0",
+ "pytest>=8.4.1",
+ "pytest-asyncio>=1.1.0",
+ "pytest-cov>=6.2.1",
+ "pytest-dotenv>=0.5.2",
+ "pytest-env>=1.1.5",
+ "pytest-examples>=0.0.18",
+ "pytest-md-report>=0.7.0",
+ "pytest-rerunfailures>=15.1",
+ "pytest-trio>=0.8.0",
+ "pytest-xdist>=3.8.0",
+ "ruff>=0.12.9",
+ "unimport>=1.2.1",
+ "uv>=0.8.11",
+]
+
+# --------------------------------------------------------------------
+# 🔧 setuptools-specific configuration
+# --------------------------------------------------------------------
+[tool.setuptools]
+include-package-data = true # ensure wheels include the data files
+
+# Automatic discovery: keep every package that starts with "cedarpolicyplugin"
+[tool.setuptools.packages.find]
+include = ["cedarpolicyplugin*"]
+exclude = ["tests*"]
+
+## Runtime data files ------------------------------------------------
+[tool.setuptools.package-data]
+cedarpolicyplugin = [
+ "resources/plugins/config.yaml",
+]
diff --git a/plugins/external/cedar/resources/plugins/config.yaml b/plugins/external/cedar/resources/plugins/config.yaml
new file mode 100644
index 000000000..23d048311
--- /dev/null
+++ b/plugins/external/cedar/resources/plugins/config.yaml
@@ -0,0 +1,102 @@
+plugins:
+ - name: "CedarPolicyPlugin"
+ kind: "cedarpolicyplugin.plugin.CedarPolicyPlugin"
+ description: "A plugin that does policy decision and enforcement using cedar"
+ version: "0.1.0"
+ author: "Shriti Priya"
+ hooks: ["prompt_pre_fetch", "prompt_post_fetch", "tool_pre_invoke", "tool_post_invoke"]
+ tags: ["plugin"]
+ mode: "enforce" # enforce | permissive | disabled
+ priority: 150
+ conditions:
+ # Apply to specific tools/servers
+ - server_ids: [] # Apply to all servers
+ tenant_ids: [] # Apply to all tenants
+ config:
+ policy_lang: cedar
+ policy_output_keywords:
+ view_full: "view_full_output"
+ view_redacted: "view_redacted_output"
+ policy_redaction_spec:
+ pattern: '"\$\d{1,}(,\d{1,})*"' # provide regex, if none, then replace all
+ policy:
+ - id: allow-employee-basic-access
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"get_leave_balance" #tool name
+ - Action::"request_certificate"
+ resource:
+ - Server::"askHR" # mcp-server name
+ - Agent::"employee_agent" # agent name
+
+ - id: allow-manager-full-access
+ effect: Permit
+ principal: Role::"manager"
+ action:
+ - Action::"get_leave_balance"
+ - Action::"approve_leave"
+ - Action::"promote_employee"
+ - Action::"view_performance"
+ - Action::"view_full_output"
+ resource:
+ - Agent::"manager_agent"
+ - Server::"payroll_tool"
+
+ - id: allow-hr-hr_tool
+ effect: Permit
+ principal: Role::"hr"
+ action:
+ - Action::"update_payroll"
+ - Action::"view_performance"
+ - Action::"view_full_output"
+ resource: Server::"hr_tool"
+
+ - id: redact-non-manager-views
+ effect: Permit
+ principal: Role::"employee"
+ action: Action::"view_redacted_output"
+ resource:
+ - Server::"payroll_tool"
+ - Agent::"manager_agent"
+ - Server::"askHR"
+
+ - id: allow-admin-resources # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Resource::""https://example.com/data"" #Resource::
+
+ - id: allow-employee-redacted-resources # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Resource::""https://example.com/data"" #Resource::
+
+ - id: allow-admin-prompts # policy for resources
+ effect: Permit
+ principal: Role::"admin"
+ action:
+ - Action::"view_full_output"
+ resource: Prompts::"judge_prompts" #Prompt::
+
+ - id: allow-employee-redacted-prompts # policy for resources
+ effect: Permit
+ principal: Role::"employee"
+ action:
+ - Action::"view_redacted_output"
+ resource: Prompts::"judge_prompts" #Prompt::
+
+# Plugin directories to scan
+plugin_dirs:
+ - "cedarpolicyplugin"
+
+# Global plugin settings
+plugin_settings:
+ parallel_execution_within_band: true
+ plugin_timeout: 30
+ fail_on_plugin_error: false
+ enable_plugin_api: true
+ plugin_health_check_interval: 60
diff --git a/plugins/external/cedar/resources/runtime/config.yaml b/plugins/external/cedar/resources/runtime/config.yaml
new file mode 100644
index 000000000..5b26791f5
--- /dev/null
+++ b/plugins/external/cedar/resources/runtime/config.yaml
@@ -0,0 +1,71 @@
+# config.yaml
+host:
+ name: "cedarpolicyplugin"
+ log_level: "INFO"
+
+server:
+ type: "streamable-http" # "stdio" or "sse" or "streamable-http"
+ #auth: "bearer" # this line is needed to enable bearer auth
+
+# Logging configuration - controls all logging behavior
+logging:
+ level: "WARNING" # Changed from INFO to WARNING for quieter default
+ format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ reset_handlers: true
+ quiet_libraries: true
+
+ # Specific logger overrides to silence noisy components
+ loggers:
+ # Your existing overrides
+ "chuk_mcp_runtime.proxy": "WARNING"
+ "chuk_mcp_runtime.proxy.manager": "WARNING"
+ "chuk_mcp_runtime.proxy.tool_wrapper": "WARNING"
+ "chuk_tool_processor.mcp.stream_manager": "WARNING"
+ "chuk_tool_processor.mcp.register": "WARNING"
+ "chuk_tool_processor.mcp.setup_stdio": "WARNING"
+ "chuk_mcp_runtime.common.tool_naming": "WARNING"
+ "chuk_mcp_runtime.common.openai_compatibility": "WARNING"
+
+ # NEW: Add the noisy loggers you're seeing
+ "chuk_sessions.session_manager": "ERROR"
+ "chuk_mcp_runtime.session.native": "ERROR"
+ "chuk_mcp_runtime.tools.artifacts": "ERROR"
+ "chuk_mcp_runtime.tools.session": "ERROR"
+ "chuk_artifacts.store": "ERROR"
+ "chuk_mcp_runtime.entry": "WARNING" # Keep some info but less chatty
+ "chuk_mcp_runtime.server": "WARNING" # Server start/stop messages
+
+# optional overrides
+sse:
+ host: "0.0.0.0"
+ port: 8000
+ sse_path: "/sse"
+ message_path: "/messages/"
+ health_path: "/health"
+ log_level: "info"
+ access_log: true
+
+streamable-http:
+ host: "0.0.0.0"
+ port: 8000
+ mcp_path: "/mcp"
+ stateless: true
+ json_response: true
+ health_path: "/health"
+ log_level: "info"
+ access_log: true
+
+proxy:
+ enabled: false
+ namespace: "proxy"
+ openai_compatible: false # ← set to true if you want underscores
+
+# Session tools (disabled by default - must enable explicitly)
+session_tools:
+ enabled: false # Must explicitly enable
+
+# Artifact storage (disabled by default - must enable explicitly)
+artifacts:
+ enabled: false # Must explicitly enable
+ storage_provider: "filesystem"
+ session_provider: "memory"
diff --git a/plugins/external/cedar/run-server.sh b/plugins/external/cedar/run-server.sh
new file mode 100755
index 000000000..d73f57de5
--- /dev/null
+++ b/plugins/external/cedar/run-server.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+#───────────────────────────────────────────────────────────────────────────────
+# Script : run-server.sh
+# Purpose: Launch the MCP Gateway's Plugin API
+#
+# Description:
+# This script launches an API server using
+# chuck runtime.
+#
+# Environment Variables:
+# API_SERVER_SCRIPT : Path to the server script (optional, auto-detected)
+# PLUGINS_CONFIG_PATH : Path to the plugin config (optional, default: ./resources/plugins/config.yaml)
+# CHUK_MCP_CONFIG_PATH : Path to the chuck-mcp-runtime config (optional, default: ./resources/runtime/config.yaml)
+#
+# Usage:
+# ./run-server.sh # Run server
+#───────────────────────────────────────────────────────────────────────────────
+
+# Exit immediately on error, undefined variable, or pipe failure
+set -euo pipefail
+
+#────────────────────────────────────────────────────────────────────────────────
+# SECTION 1: Script Location Detection
+# Determine the absolute path of the API server script
+#────────────────────────────────────────────────────────────────────────────────
+if [[ -z "${API_SERVER_SCRIPT:-}" ]]; then
+ API_SERVER_SCRIPT="$(python -c 'import mcpgateway.plugins.framework.external.mcp.server.runtime as server; print(server.__file__)')"
+ echo "✓ API server script path auto-detected: ${API_SERVER_SCRIPT}"
+else
+ echo "✓ Using provided API server script path: ${API_SERVER_SCRIPT}"
+fi
+
+#────────────────────────────────────────────────────────────────────────────────
+# SECTION 2: Run the API server
+# Run the API server from configuration
+#────────────────────────────────────────────────────────────────────────────────
+
+PLUGINS_CONFIG_PATH=${PLUGINS_CONFIG_PATH:-./resources/plugins/config.yaml}
+CHUK_MCP_CONFIG_PATH=${CHUK_MCP_CONFIG_PATH:-./resources/runtime/config.yaml}
+
+echo "✓ Using plugin config from: ${PLUGINS_CONFIG_PATH}"
+echo "✓ Running API server with config from: ${CHUK_MCP_CONFIG_PATH}"
+python ${API_SERVER_SCRIPT}
diff --git a/plugins/external/cedar/tests/__init__.py b/plugins/external/cedar/tests/__init__.py
new file mode 100644
index 000000000..2e033f69b
--- /dev/null
+++ b/plugins/external/cedar/tests/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+"""Location: ./tests/__init__.py
+Copyright 2025
+SPDX-License-Identifier: Apache-2.0
+Authors: Shriti Priya
+"""
diff --git a/plugins/external/cedar/tests/pytest.ini b/plugins/external/cedar/tests/pytest.ini
new file mode 100644
index 000000000..ff60648e6
--- /dev/null
+++ b/plugins/external/cedar/tests/pytest.ini
@@ -0,0 +1,13 @@
+[pytest]
+log_cli = false
+log_cli_level = INFO
+log_cli_format = %(asctime)s [%(module)s] [%(levelname)s] %(message)s
+log_cli_date_format = %Y-%m-%d %H:%M:%S
+log_level = INFO
+log_format = %(asctime)s [%(module)s] [%(levelname)s] %(message)s
+log_date_format = %Y-%m-%d %H:%M:%S
+addopts = --cov --cov-report term-missing
+env_files = .env
+pythonpath = . src
+filterwarnings =
+ ignore::DeprecationWarning:pydantic.*
diff --git a/plugins/external/cedar/tests/test_cedarpolicyplugin.py b/plugins/external/cedar/tests/test_cedarpolicyplugin.py
new file mode 100644
index 000000000..5be710f24
--- /dev/null
+++ b/plugins/external/cedar/tests/test_cedarpolicyplugin.py
@@ -0,0 +1,792 @@
+# -*- coding: utf-8 -*-
+"""Tests for plugin."""
+
+# Third-Party
+import pytest
+
+# First-Party
+from cedarpolicyplugin.plugin import CedarPolicyPlugin
+from mcpgateway.plugins.framework.models import (
+ PluginConfig,
+ PluginContext,
+ GlobalContext,
+)
+
+from mcpgateway.plugins.framework.hooks.resources import ResourcePreFetchPayload, ResourcePostFetchPayload
+from mcpgateway.plugins.framework.hooks.prompts import PromptPrehookPayload, PromptPosthookPayload
+from mcpgateway.plugins.framework.hooks.tools import ToolPostInvokePayload, ToolPreInvokePayload
+
+from mcpgateway.common.models import Message, ResourceContent, Role, TextContent, PromptResult
+
+
+# This test case is responsible for verifying cedarplugin functionality for post tool hooks in cdear native mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_post_tool_invoke_rbac():
+ """Test plugin for post tool invocation"""
+ policy_config = [
+ {
+ 'id': 'allow-employee-basic-access',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"get_leave_balance"', 'Action::"request_certificate"'],
+ 'resource': ['Server::"askHR"', 'Agent::"employee_agent"']
+ },
+ {
+ 'id': 'allow-manager-full-access',
+ 'effect': 'Permit',
+ 'principal': 'Role::"manager"',
+ 'action': ['Action::"get_leave_balance"', 'Action::"approve_leave"', 'Action::"promote_employee"', 'Action::"view_performance"', 'Action::"view_full_output"'],
+ 'resource': ['Agent::"manager_agent"', 'Server::"payroll_tool"']
+ },
+ {
+ 'id': 'allow-hr-hr_tool',
+ 'effect': 'Permit',
+ 'principal': 'Role::"hr"',
+ 'action': ['Action::"update_payroll"', 'Action::"view_performance"', 'Action::"view_full_output"'],
+ 'resource': ['Server::"hr_tool"']
+ },
+ {
+ 'id': 'redact-non-manager-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': ['Server::"payroll_tool"', 'Agent::"manager_agent"', 'Server::"askHR"']
+
+ },
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": r"\$\d{1,}(,\d{1,})*"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "action": "get_leave_balance", "resource": "askHR"},
+ {"user": "bob", "action": "view_performance", "resource": "payroll_tool"},
+ {"user": "carol", "action": "update_payroll", "resource": "hr_tool"},
+ {"user": "alice", "action": "update_payroll", "resource": "hr_tool"},
+ ]
+
+ redact_count = 0
+ allow_count = 0
+ deny_count = 0
+ for req in requests:
+ payload = ToolPostInvokePayload(name=req["action"], result={"text": "Alice has a salary of $250,000"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id=req["resource"], user=req["user"]))
+ result = await plugin.tool_post_invoke(payload, context)
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.result["text"]:
+ redact_count += 1
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert redact_count == 1
+ assert allow_count == 3
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for post tool invocation with policy in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_post_tool_invoke_custom_dsl_rbac():
+ """Test plugin for post tool invocation"""
+ policy_config = '[role:employee:server/askHR]\nget_leave_balance\nrequest_certificate\n\n\
+ [role:employee:agent/employee_agent]\nget_leave_balance\nrequest_certificate\n\n[role:manager:agent/manager_agent]\nget_leave_balance\napprove_leave\npromote_employee\nview_performance\nview_full_output\n\n[role:manager:server/payroll_tool]\
+ \nget_leave_balance\napprove_leave\npromote_employee\nview_performance\nview_full_output\n\n[role:hr:server/hr_tool]\nupdate_payroll\nview_performance\nview_full_output\n\n[role:employee:server/payroll_tool]\nview_redacted_output\n\n[role:employee:agent/manager_agent]\nview_redacted_output\n\n\
+ [role:employee:server/askHR]\nview_redacted_output'
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": r"\$\d{1,}(,\d{1,})*"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "action": "get_leave_balance", "resource": "askHR"},
+ {"user": "bob", "action": "view_performance", "resource": "payroll_tool"},
+ {"user": "carol", "action": "update_payroll", "resource": "hr_tool"},
+ {"user": "alice", "action": "update_payroll", "resource": "hr_tool"},
+ ]
+
+ redact_count = 0
+ allow_count = 0
+ deny_count = 0
+ for req in requests:
+ payload = ToolPostInvokePayload(name=req["action"], result={"text": "Alice has a salary of $250,000"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id=req["resource"], user=req["user"]))
+ result = await plugin.tool_post_invoke(payload, context)
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.result["text"]:
+ redact_count += 1
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert redact_count == 1
+ assert allow_count == 3
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for tool pre invoke in cedar native mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_pre_tool_invoke_cedar_rbac():
+ """Test plugin tool pre invoke hook."""
+ policy_config = [
+ {
+ 'id': 'allow-employee-basic-access',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"get_leave_balance"', 'Action::"request_certificate"'],
+ 'resource': ['Server::"askHR"', 'Agent::"employee_agent"']
+ },
+ {
+ 'id': 'allow-manager-full-access',
+ 'effect': 'Permit',
+ 'principal': 'Role::"manager"',
+ 'action': ['Action::"get_leave_balance"', 'Action::"approve_leave"', 'Action::"promote_employee"', 'Action::"view_performance"', 'Action::"view_full_output"'],
+ 'resource': ['Agent::"manager_agent"', 'Server::"payroll_tool"']
+ },
+ {
+ 'id': 'allow-hr-hr_tool',
+ 'effect': 'Permit',
+ 'principal': 'Role::"hr"',
+ 'action': ['Action::"update_payroll"', 'Action::"view_performance"', 'Action::"view_full_output"'],
+ 'resource': ['Server::"hr_tool"']
+ },
+ {
+ 'id': 'redact-non-manager-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': ['Server::"payroll_tool"', 'Agent::"manager_agent"', 'Server::"askHR"']
+
+ },
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": r"\$\d{1,}(,\d{1,})*"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "action": "get_leave_balance", "resource": "askHR"},
+ {"user": "bob", "action": "view_performance", "resource": "payroll_tool"},
+ {"user": "carol", "action": "update_payroll", "resource": "hr_tool"},
+ {"user": "alice", "action": "update_payroll", "resource": "hr_tool"},
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ for req in requests:
+ payload = ToolPreInvokePayload(name=req["action"], args={"arg1": "sample arg"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id=req["resource"], user=req["user"]))
+ result = await plugin.tool_pre_invoke(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 3
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for tool pre invoke in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_pre_tool_invoke_custom_dsl_rbac():
+ """Test plugin tool pre invoke."""
+ policy_config = '[role:employee:server/askHR]\nget_leave_balance\nrequest_certificate\n\n[role:employee:agent/employee_agent]\n\
+ get_leave_balance\nrequest_certificate\n\n[role:manager:agent/manager_agent]\nget_leave_balance\napprove_leave\npromote_employee\n\
+ view_performance\nview_full_output\n\n[role:manager:server/payroll_tool]\nget_leave_balance\napprove_leave\npromote_employee\nview_performance\n\
+ view_full_output\n\n[role:hr:server/hr_tool]\nupdate_payroll\nview_performance\nview_full_output\n\n[role:employee:server/payroll_tool]\n\
+ view_redacted_output\n\n[role:employee:agent/manager_agent]\nview_redacted_output\n\n[role:employee:server/askHR]\nview_redacted_output'
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": r"\$\d{1,}(,\d{1,})*"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "action": "get_leave_balance", "resource": "askHR"},
+ {"user": "bob", "action": "view_performance", "resource": "payroll_tool"},
+ {"user": "carol", "action": "update_payroll", "resource": "hr_tool"},
+ {"user": "alice", "action": "update_payroll", "resource": "hr_tool"},
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ for req in requests:
+ payload = ToolPreInvokePayload(name=req["action"], args={"arg1": "sample arg"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id=req["resource"], user=req["user"]))
+ result = await plugin.tool_pre_invoke(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 3
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for prompt pre fetch in cedar mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_prompt_pre_fetch_rbac():
+ """Test plugin prompt prefetch hook."""
+ policy_config = [
+ {
+ 'id': 'redact-non-admin-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': 'Prompt::"judge_prompts"'
+
+ },
+ {
+ 'id': 'allow-admin-prompts', # policy for resources
+ 'effect': 'Permit',
+ 'principal': 'Role::"admin"',
+ 'action': ['Action::"view_full_output"'],
+ 'resource': 'Prompt::"judge_prompts"' # Prompt::
+ }
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "all"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "judge_prompts"}, # allow
+ {"user": "robert", "resource": "judge_prompts"}, # allow
+ {"user": "carol", "resource": "judge_prompts"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+
+ for req in requests:
+
+ # Prompt pre hook input
+ payload = PromptPrehookPayload(prompt_id=req["resource"], args={"text": "You are curseword"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.prompt_pre_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for prompt pre fetch in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_prompt_pre_fetch_custom_dsl_rbac():
+ """Test plugin prompt prefetch hook."""
+ policy_config = '[role:employee:prompt/judge_prompts]\nview_redacted_output\n\n[role:admin:prompt/judge_prompts]\nview_full_output'
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "all"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "judge_prompts"}, # allow
+ {"user": "robert", "resource": "judge_prompts"}, # allow
+ {"user": "carol", "resource": "judge_prompts"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+
+ for req in requests:
+
+ # Prompt pre hook input
+ payload = PromptPrehookPayload(prompt_id=req["resource"], args={"text": "You are curseword"})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.prompt_pre_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for prompt post fetch in cedar native mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_prompt_post_fetch_cedar_rbac():
+ """Test plugin prompt postfetch hook."""
+ policy_config = [
+ {
+ 'id': 'redact-non-admin-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': 'Prompt::"judge_prompts"'
+
+ },
+ {
+ 'id': 'allow-admin-prompts', # policy for resources
+ 'effect': 'Permit',
+ 'principal': 'Role::"admin"',
+ 'action': ['Action::"view_full_output"'],
+ 'resource': 'Prompt::"judge_prompts"' # Prompt::
+ }
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "all"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "judge_prompts"}, # allow
+ {"user": "robert", "resource": "judge_prompts"}, # allow
+ {"user": "carol", "resource": "judge_prompts"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ redact_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ message = Message(content=TextContent(type="text", text="abc"), role=Role.USER)
+ prompt_result = PromptResult(messages=[message])
+ payload = PromptPosthookPayload(prompt_id=req["resource"], result=prompt_result)
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.prompt_post_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.result.messages[0].content.text:
+ redact_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+ assert redact_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for prompt post fetch in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_prompt_post_fetch_custom_dsl_rbac():
+ """Test plugin prompt postfetch hook."""
+ policy_config = '[role:employee:prompt/judge_prompts]\nview_redacted_output\n\n[role:admin:prompt/judge_prompts]\nview_full_output'
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "all"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "judge_prompts"}, # allow
+ {"user": "robert", "resource": "judge_prompts"}, # allow
+ {"user": "carol", "resource": "judge_prompts"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ redact_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ message = Message(content=TextContent(type="text", text="abc"), role=Role.USER)
+ prompt_result = PromptResult(messages=[message])
+ payload = PromptPosthookPayload(prompt_id=req["resource"], result=prompt_result)
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.prompt_post_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.result.messages[0].content.text:
+ redact_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+ assert redact_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for resource pre fetch in cedar native mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_resource_pre_fetch_cedar_rbac():
+ """Test plugin resource prefetch hook."""
+ policy_config = [
+ {
+ 'id': 'redact-non-admin-resource-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': 'Resource::"https://example.com/data"'
+
+ },
+ {
+ 'id': 'allow-admin-resources', # policy for resources
+ 'effect': 'Permit',
+ 'principal': 'Role::"admin"',
+ 'action': ['Action::"view_full_output"'],
+ 'resource': 'Resource::"https://example.com/data"'
+ }
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "https://example.com/data"}, # allow
+ {"user": "robert", "resource": "https://example.com/data"}, # allow
+ {"user": "carol", "resource": "https://example.com/data"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ payload = ResourcePreFetchPayload(uri="https://example.com/data", metadata={})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.resource_pre_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for resource pre fetch in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_resource_pre_fetch_custom_dsl_rbac():
+ """Test plugin resource prefetch hook."""
+ policy_config = '[role:employee:resource/https://example.com/data]\nview_redacted_output\n\n[role:admin:resource/https://example.com/data]\nview_full_output'
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "https://example.com/data"}, # allow
+ {"user": "robert", "resource": "https://example.com/data"}, # allow
+ {"user": "carol", "resource": "https://example.com/data"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ payload = ResourcePreFetchPayload(uri="https://example.com/data", metadata={})
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.resource_pre_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for resource post fetch in cedar native mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_resource_post_fetch_cedar_rbac():
+ """Test plugin resource post fetch."""
+ policy_config = [
+ {
+ 'id': 'redact-non-admin-resource-views',
+ 'effect': 'Permit',
+ 'principal': 'Role::"employee"',
+ 'action': ['Action::"view_redacted_output"'],
+ 'resource': 'Resource::"https://example.com/data"'
+
+ },
+ {
+ 'id': 'allow-admin-resources', # policy for resources
+ 'effect': 'Permit',
+ 'principal': 'Role::"admin"',
+ 'action': ['Action::"view_full_output"'],
+ 'resource': 'Resource::"https://example.com/data"'
+ }
+ ]
+
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "cedar",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "https://example.com/data"}, # allow
+ {"user": "robert", "resource": "https://example.com/data"}, # allow
+ {"user": "carol", "resource": "https://example.com/data"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ redact_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ content = ResourceContent(
+ type="resource",
+ uri="test://large",
+ text="test://abc@example.com",
+ id="1"
+ )
+ payload = ResourcePostFetchPayload(uri="https://example.com/data", content=content)
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.resource_post_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.content.text:
+ redact_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+ assert redact_count == 1
+
+
+# This test case is responsible for verifying cedarplugin functionality for resource post fetch in custom dsl mode
+@pytest.mark.asyncio
+async def test_cedarpolicyplugin_resource_post_fetch_custom_dsl_rbac():
+ """Test plugin resource postfetch hook."""
+ policy_config = '[role:employee:resource/https://example.com/data]\nview_redacted_output\n\n[role:admin:resource/https://example.com/data]\nview_full_output'
+ policy_output_keywords = {"view_full": "view_full_output", "view_redacted": "view_redacted_output"}
+ policy_redaction_spec = {"pattern": "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"}
+ config = PluginConfig(
+ name="test",
+ kind="cedarpolicyplugin.CedarPolicyPlugin",
+ hooks=["tool_pre_invoke"],
+ config={
+ "policy_lang": "custom_dsl",
+ "policy" : policy_config,
+ "policy_output_keywords": policy_output_keywords,
+ "policy_redaction_spec": policy_redaction_spec
+ },
+ )
+ plugin = CedarPolicyPlugin(config)
+ info = {
+ "alice": "employee",
+ "bob": "manager",
+ "carol": "hr",
+ "robert": "admin"
+ }
+ plugin._set_jwt_info(info)
+ requests = [
+ {"user": "alice", "resource": "https://example.com/data"}, # allow
+ {"user": "robert", "resource": "https://example.com/data"}, # allow
+ {"user": "carol", "resource": "https://example.com/data"}, # deny
+ ]
+
+ allow_count = 0
+ deny_count = 0
+ redact_count = 0
+
+ for req in requests:
+
+ # Prompt post hook output
+ content = ResourceContent(
+ type="resource",
+ uri="test://large",
+ text="test://abc@example.com",
+ id="1"
+ )
+ payload = ResourcePostFetchPayload(uri="https://example.com/data", content=content)
+ context = PluginContext(global_context=GlobalContext(request_id="1", server_id="2", user=req["user"]))
+ result = await plugin.resource_post_fetch(payload, context)
+ if result.continue_processing:
+ allow_count += 1
+ if result.modified_payload and "[REDACTED]" in result.modified_payload.content.text:
+ redact_count += 1
+ if not result.continue_processing:
+ deny_count += 1
+
+ assert allow_count == 2
+ assert deny_count == 1
+ assert redact_count == 1