Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ An Envoy external processor (ext-proc) for configuring and invoking guardrails i

4. **Deploy to kind cluster**
```bash
# Replace nemocheck with a comma-separated list of plugins to include other plugins
# Replace plugin adapter, with nemocheck plugin loaded. Use a comma-separated list to include other plugins
make all PLUGIN_DEPS=nemocheck
```

Expand Down
3 changes: 1 addition & 2 deletions docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
### Automated Build

```bash
python3 -m venv .venv
source .venv/bin/activate
uv sync --group proto
./proto-build.sh
```

Expand Down
75 changes: 75 additions & 0 deletions plugins/examples/nemocheck/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,81 @@ async def prompt_pre_fetch(self, payload: PromptPrehookPayload, context: PluginC
Returns:
The result of the plugin's analysis.
"""

logger.debug(f"[NemoCheck] Starting prompt pre fetch hook with payload {payload}")

pmt_name = payload.prompt_id
assert payload.args is not None
check_nemo_payload = {
"model": self.model_name,
"guardrails": {"config_id": self.nemo_config_id},
"messages": [
{
"role": "assistant",
"tool_calls": [
{
"id": "call_plug_adap_nem_check_123",
"type": "function",
"function": {
"name": pmt_name,
"arguments": payload.args,
},
}
],
}
],
}

try:
response = requests.post(self.check_endpoint, headers=HEADERS, json=check_nemo_payload)

if response.status_code == 200:
data = response.json()
status = data.get("status", "blocked")
logger.debug(f"[NemoCheck] Rails reply: {data}")
metadata = data.get("rails_status")

if status == "success":
return PromptPrehookResult(continue_processing=True, metadata=metadata)
else:
logger.info(f"[NemoCheck] Prompt request blocked. Full NeMo response: {data}")
# Extract rail names from rails_status for more informative description
rails_run = list(metadata.keys()) if metadata else []
rails_info = f"Rails: {', '.join(rails_run)}" if rails_run else "No rails info"
violation = PluginViolation(
reason=f"Prompt fetch check failed: {status}",
description=f"{rails_info}",
code="NEMO_RAILS_BLOCKED",
details=metadata,
mcp_error_code=-32602, # Invalid params
)
return PromptPrehookResult(
continue_processing=False,
violation=violation,
metadata=metadata,
)
else:
violation = PluginViolation(
reason="Tool Check Unavailable",
description=(
f"Tool request check server returned error. "
f"Status code: {response.status_code}, Response: {response.text}"
),
code="NEMO_SERVER_ERROR",
details={"status_code": response.status_code},
)
return PromptPrehookResult(continue_processing=False, violation=violation)

except Exception as e:
logger.error(f"[NemoCheck] Error checking tool request: {e}")
violation = PluginViolation(
reason="Tool Check Error",
description=f"Failed to connect to check server: {str(e)}",
code="NEMO_CONNECTION_ERROR",
details={"error": str(e)},
)
return PromptPrehookResult(continue_processing=False, violation=violation)

return PromptPrehookResult(continue_processing=True)

async def prompt_post_fetch(self, payload: PromptPosthookPayload, context: PluginContext) -> PromptPosthookResult:
Expand Down
2 changes: 1 addition & 1 deletion resources/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins:
kind: "plugins.examples.nemocheck.plugin.NemoCheck"
description: "Adapter for nemo check server"
version: "0.1.0"
hooks: ["tool_pre_invoke", "tool_post_invoke"]
hooks: ["tool_pre_invoke", "tool_post_invoke", "prompt_pre_fetch", "prompt_post_fetch"]
mode: "sequential" # enforce | permissive | disabled
config:
nemo_guardrails_url: "http://nemo-guardrails-service:8000"
Expand Down
5 changes: 3 additions & 2 deletions src/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ async def getPromptPreFetchResponse(body):
Invokes plugins before a prompt is fetched, allowing for argument validation,
modification, or blocking of the prompt request.
"""
logger.debug(f"**** prompt pre-fetch body: {body} ****")
prompt = PromptPrehookPayload(prompt_id=body["params"]["name"], args=body["params"]["arguments"])
# TODO: hard-coded ids
global_context = GlobalContext(request_id="1", server_id="2")
Expand All @@ -213,14 +214,14 @@ async def getPromptPreFetchResponse(body):
if not result.continue_processing:
body_resp = create_mcp_immediate_error_response(
body,
error_message="Tool response forbidden",
error_message="Prompt Fetch forbidden",
violation=result.violation,
)
else:
result_payload = result.modified_payload
body_mutation = ep.BodyResponse(response=ep.CommonResponse())
if result_payload is not None and result_payload.args is not None:
body["params"]["arguments"] = result_payload.args["tool_args"]
body["params"]["arguments"] = result_payload.args["prompt_args"]
body_mutation = get_modified_response(body)
else:
logger.debug("No change in prompt")
Expand Down
37 changes: 37 additions & 0 deletions tests/single-server-filter.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: single-server-plugins-adapter-filter
namespace: istio-system
spec:
workloadSelector:
labels:
app: pod-blog # Changed from istio: ingressgateway
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_INBOUND # Changed from GATEWAY
listener:
portNumber: 50052 # Changed from 8080
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.filters.http.ext_proc
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_proc.v3.ExternalProcessor
failure_mode_allow: false
mutation_rules:
allow_all_routing: true
processing_mode:
request_header_mode: 'SEND'
response_header_mode: 'SEND'
request_body_mode: 'BUFFERED'
response_body_mode: 'BUFFERED'
request_trailer_mode: 'SKIP'
response_trailer_mode: 'SKIP'
grpc_service:
envoy_grpc:
cluster_name: outbound|50052||plugins-adapter-service.istio-system.svc.cluster.local
Loading