diff --git a/README.md b/README.md index c8f377b..4919a87 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/docs/build.md b/docs/build.md index e756237..acc99aa 100644 --- a/docs/build.md +++ b/docs/build.md @@ -5,8 +5,7 @@ ### Automated Build ```bash -python3 -m venv .venv -source .venv/bin/activate +uv sync --group proto ./proto-build.sh ``` diff --git a/plugins/examples/nemocheck/plugin.py b/plugins/examples/nemocheck/plugin.py index 6e588e3..37ef0a4 100644 --- a/plugins/examples/nemocheck/plugin.py +++ b/plugins/examples/nemocheck/plugin.py @@ -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: diff --git a/resources/config/config.yaml b/resources/config/config.yaml index 9a0a068..95eeb70 100644 --- a/resources/config/config.yaml +++ b/resources/config/config.yaml @@ -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" diff --git a/src/server.py b/src/server.py index 9aabba9..ef3aee9 100644 --- a/src/server.py +++ b/src/server.py @@ -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") @@ -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") diff --git a/tests/single-server-filter.yaml b/tests/single-server-filter.yaml new file mode 100644 index 0000000..a764632 --- /dev/null +++ b/tests/single-server-filter.yaml @@ -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 \ No newline at end of file