diff --git a/content/docs/kubernetes/main/reference/cel/variables.md b/content/docs/kubernetes/main/reference/cel/variables.md index cf51292f8..9d29485f3 100644 --- a/content/docs/kubernetes/main/reference/cel/variables.md +++ b/content/docs/kubernetes/main/reference/cel/variables.md @@ -18,6 +18,8 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [Interactive CEL reference]({{< link-hextra path="/reference/cel/cel-context-interactive" >}}) page. +When an agent session can be identified, the session is available as `request.agent.session`. The session is detected from agent headers such as `x-claude-code-session-id` and `x-session-id`. + When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. > [!NOTE] @@ -36,6 +38,8 @@ Depending on the policy, different fields are accessible based on when in the re |HTTP Authorization| `source`, `request`, `jwt`, `mcp`, `backend` | |External Authorization| `source`, `request`, `jwt`, `mcp`, `backend` | |MCP Authorization| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | +|MCP guardrails (CEL request processor)| `source`, `request`, `jwt`, `mcp`, `backend` — `mcp.params` contains the current MCP request parameters. | +|MCP guardrails (CEL response processor)| Same as the request processor, except that `mcp.result` replaces `mcp.params`. `mcp.result` contains the current MCP response result. | |Logging| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Tracing| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| |Metrics| `source`, `request`, `jwt`, `mcp`, `backend`, `extauthz`, `response`, `llm`| diff --git a/content/docs/kubernetes/main/release-notes/release-notes.md b/content/docs/kubernetes/main/release-notes/release-notes.md index 328fc8382..41a978cee 100644 --- a/content/docs/kubernetes/main/release-notes/release-notes.md +++ b/content/docs/kubernetes/main/release-notes/release-notes.md @@ -17,12 +17,19 @@ Review the release notes for agentgateway on Kubernetes. - **Optional chart-created RBAC**: Set `rbac.create` to `false` in the controller Helm chart to skip the ClusterRoles, Role, ClusterRoleBindings, and RoleBindings that the chart creates, for clusters that manage role-based access control (RBAC) outside of Helm. For more information, see [Chart-created RBAC resources]({{< link-hextra path="/documentation/install/advanced/#rbac-create" >}}). + ### MCP {#v17-mcp} - **Conditional virtual MCP targets**: Set `spec.mcp.targets[].condition` on an {{< reuse "agw-docs/snippets/backend.md" >}} to a CEL expression to select which targets take part in a request before agentgateway initializes or contacts them. A backend whose only target is a `static` target cannot set a condition. For more information, see [Select targets with conditions]({{< link-hextra path="/documentation/mcp/virtual/#target-conditions" >}}). +### Observability {#v17-observability} + + + +- **Agent session detection**: CEL expressions can read the agent session of a request as `request.agent.session`. The session is detected from agent headers such as `x-claude-code-session-id`. + ## 🐛 Fixes {#v17-fixes} diff --git a/content/docs/standalone/main/documentation/llm/playground.md b/content/docs/standalone/main/documentation/llm/playground.md index 11cb18185..756e65850 100644 --- a/content/docs/standalone/main/documentation/llm/playground.md +++ b/content/docs/standalone/main/documentation/llm/playground.md @@ -38,6 +38,10 @@ Use the built-in LLM playground in the agentgateway UI to send a chat completion {{< reuse-image-light src="img/ui-llm-playground.png" >}} {{< reuse-image-dark srcDark="img/ui-llm-playground-dark.png" >}} +## Authentication behavior + +The playground sends the request from your browser, but does not forward browser credentials, such as cookies or HTTP basic authentication, to the LLM gateway. If the gateway requires a bearer token, select a saved key in the **Virtual API key** list. Or, select **Raw value** and enter the token in the **Raw API key** field. The playground sends the key in an `Authorization: Bearer` header. The MCP playground also omits browser credentials and sends the value of its **Bearer token** field instead. + ## Next steps - [Observe LLM traffic]({{< link-hextra path="/documentation/llm/observability/" >}}) with metrics, logs, and traces. diff --git a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md index 3ba3c76ac..c8f292470 100644 --- a/content/docs/standalone/main/documentation/mcp/guardrails/setup.md +++ b/content/docs/standalone/main/documentation/mcp/guardrails/setup.md @@ -1,15 +1,15 @@ --- title: Set up MCP guardrails weight: 20 -description: Gate and mutate MCP method calls with an external ExtMCP policy server. +description: Gate and mutate MCP method calls with an external ExtMCP policy server or in-process CEL expressions. test: mcp-guardrails: - path: mcp-guardrails --- -Gate and mutate Model Context Protocol (MCP) method calls with an external policy server. For more information about how MCP guardrails work, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). +Gate and mutate Model Context Protocol (MCP) method calls with an external policy server or in-process Common Expression Language (CEL) expressions. For more information about how MCP guardrails work, see [About MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/about" >}}). -In this guide, you route `tools/call` and `tools/list` requests through a sample ExtMCP server that denies any tool whose name contains `forbidden` and annotates each tool description in `tools/list` responses. +In this guide, you route `tools/call` and `tools/list` requests through a sample ExtMCP server that denies any tool whose name contains `forbidden` and annotates each tool description in `tools/list` responses. Then, you add CEL expression processors that reject and mask tool calls without a policy server. ## Before you begin @@ -249,6 +249,120 @@ EC=$(curl -s http://localhost:3000/mcp \ echo "$EC" | grep -q 'Echo: hello' && echo "✓ allowed tool call (echo) succeeded" || { echo "FAIL: echo call did not succeed: $EC"; exit 1; } {{< /doc-test >}} +## Add CEL expression processors {#expression-processors} + +You can also run a processor as a CEL expression inside agentgateway, without a policy server. Set `kind: expression` instead of `kind: remote`. One `mcpGuardrails` policy can mix both kinds. Processors run in list order, each processor sees the changes from earlier processors, and the first rejection stops the chain. + +1. Update the `config.yaml` file to add three `expression` processors after the `remote` processor. The first processor rejects `echo` calls whose message contains `DO_NOT_SEND`. The second processor masks email addresses in the `echo` message before the call reaches the MCP backend. The third processor masks `demo-secret-` tokens in the text content of tool results. + ```yaml + # yaml-language-server: $schema=https://agentgateway.dev/schema/config + gateways: + default: + port: 3000 + routes: + - policies: + cors: + allowOrigins: + - "*" + allowHeaders: + - mcp-protocol-version + - content-type + - mcp-session-id + exposeHeaders: + - "Mcp-Session-Id" + mcpGuardrails: + processors: + - kind: remote + host: "localhost:9001" + failureMode: failClosed + methods: + tools/call: request + tools/list: response + # Reject before masking, so this processor sees the original message. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) && + mcp.params.arguments.message.contains('DO_NOT_SEND') + reject: "Messages containing DO_NOT_SEND may not be sent upstream" + # Mask every email-like match in the echo message. + - kind: expression + methods: {"tools/call": request} + condition: >- + mcp.params.name == 'echo' && + has(mcp.params.arguments) && + has(mcp.params.arguments.message) + transform: >- + mcp.params.with(p, p.merge({ + 'arguments': p.arguments.merge({ + 'message': p.arguments.message.regexReplaceAll( + '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}', + '[EMAIL]' + ) + }) + })) + # Mask demo secrets in all text blocks that tools return. + - kind: expression + methods: {"tools/call": response} + condition: has(mcp.result.content) + transform: >- + mcp.result.with(r, r.merge({ + 'content': r.content.map(item, + item.type == 'text' + ? item.merge({'text': item.text.regexReplaceAll( + 'demo-secret-[A-Za-z0-9]+', '[SECRET]' + )}) + : item + ) + })) + backends: + - mcp: + targets: + - name: everything + stdio: + cmd: npx + args: ["@modelcontextprotocol/server-everything"] + ``` + + Review the following table to understand the `expression` settings. For the variables that an expression can read, see [CEL variables]({{< link-hextra path="/reference/cel/variables" >}}). + + | Setting | Description | + |---------|-------------| + | `kind: expression` | Run the processor as a CEL expression inside agentgateway instead of calling a remote ExtMCP server. Expression processors have no `failureMode`. | + | `condition` | Optional. A CEL expression that must return `true` for the action to run. If you omit `condition`, the action runs for every method that the processor matches. Check optional fields with `has()` first because an expression that fails to evaluate rejects the call with a JSON-RPC internal error. | + | `reject` | The message to return when the processor rejects a call. When the processor rejects a `tools/call` request in the request phase, the client receives a tool result with `isError: true` and this message as its text. Other rejections return a JSON-RPC error with code `-32001`. Set exactly one of `reject` or `transform`. | + | `transform` | A CEL expression that returns a new version of the MCP message. In the request phase, the expression reads the current request parameters from the `mcp.params` variable and the value that the expression returns becomes the new request parameters. In the response phase, the expression reads the current response result from the `mcp.result` variable and the value that the expression returns becomes the new response result. To change one field and keep the others, use `merge` on the variable, as in the example. To leave the message unchanged, return `null`. If the output is not a valid message for the method, agentgateway rejects the call. | + | `methods` | The MCP methods and phases that the processor runs for, the same as for a `remote` processor. | + +2. Stop agentgateway with `Ctrl+C` and run it again with the updated configuration file. + ```sh + agentgateway -f config.yaml + ``` + +3. Start a new MCP session. Repeat steps 1 and 2 in [Verify the guardrails](#verify-the-guardrails). + +4. Call the `echo` tool with a message that contains `DO_NOT_SEND`. Verify that the response is a tool result with `isError: true` and the text `Messages containing DO_NOT_SEND may not be sent upstream`. The call does not reach the MCP backend. + ```sh + curl -s http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -H "mcp-session-id: $MCP_SESSION_ID" \ + -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"echo","arguments":{"message":"DO_NOT_SEND this"}}}' + ``` + +5. Call the `echo` tool with a message that contains an email address and a demo secret. Verify that the result text is `Echo: Contact [EMAIL] with [SECRET]`. The second processor masked the email address in the request, and the third processor masked the secret in the response. + ```sh + curl -s http://localhost:3000/mcp \ + -H "Content-Type: application/json" \ + -H "Accept: application/json, text/event-stream" \ + -H "MCP-Protocol-Version: 2025-03-26" \ + -H "mcp-session-id: $MCP_SESSION_ID" \ + -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Contact alice@example.com with demo-secret-abc123"}}}' + ``` + ## Cleanup {{< reuse "agw-docs/snippets/cleanup.md" >}} diff --git a/content/docs/standalone/main/documentation/observability/access-logs/view.md b/content/docs/standalone/main/documentation/observability/access-logs/view.md index 4e4612798..b744c928f 100644 --- a/content/docs/standalone/main/documentation/observability/access-logs/view.md +++ b/content/docs/standalone/main/documentation/observability/access-logs/view.md @@ -121,6 +121,16 @@ frontendPolicies: cost: 'string(llm.cost)' ``` +For requests from agent harnesses, you can also log the detected agent session. The `request.agent.session` value comes from common agent headers such as `x-claude-code-session-id` and `x-session-id`, unless you override that value with `config.standardAttributes.session`. + +```yaml +# yaml-language-server: $schema=https://agentgateway.dev/schema/config +frontendPolicies: + accessLog: + add: + agent_session: 'request.agent.session' +``` + For the full list of available fields, see the [CEL variables reference]({{< link-hextra path="/reference/cel/variables/" >}}). ### Log the selected backend endpoint {#backend-endpoint} diff --git a/content/docs/standalone/main/reference/cel/variables.md b/content/docs/standalone/main/reference/cel/variables.md index ac5a56689..196aefc02 100644 --- a/content/docs/standalone/main/reference/cel/variables.md +++ b/content/docs/standalone/main/reference/cel/variables.md @@ -16,6 +16,8 @@ Each policy execution consistently gets the current view of the request and resp For the full list of fields and types on every top-level object, see the [CEL reference]({{< link-hextra path="/reference/cel/cel-context" >}}) page. It is generated from the [agentgateway CEL schema](https://github.com/agentgateway/agentgateway/blob/main/schema/cel.md) and is the source of truth for nested fields (for example, `source.address` or `llm.inputTokens`). +When an agent session can be identified, the session is available as `request.agent.session`. By default, the session is detected from agent headers such as `x-claude-code-session-id` and `x-session-id`. To set the session yourself, set `config.standardAttributes.session` to a CEL expression that returns a string. Return `null` or an empty string to mark a request as having no agent session. If the expression fails or returns another type, the detected session is used. + When a policy can run against both directly addressed and Service backends, check `has(backend.endpoint)` before reading `backend.endpoint`. The field is populated only for directly addressed backends, such as static hostnames. Service backends leave `backend.endpoint` unset because workload endpoints are selected separately. > [!NOTE] @@ -36,6 +38,8 @@ Depending on the policy, different top-level variables are bound when CEL runs. | Network authorization | `env`, `source` [^3] | | External Authorization | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — some expressions run after the authorization service returns and can read `response`. [^4] | | MCP Authorization | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.methodName` distinguishes methods such as `tools/list` and `tools/call`. For list methods, rules run once per listed item. `mcp.sessionId` and `mcp.tool.arguments` aren't set. | +| MCP guardrails (CEL request processor) | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` — `mcp.params` contains the current MCP request parameters. | +| MCP guardrails (CEL response processor) | Same as the request processor, except that `mcp.result` replaces `mcp.params`. `mcp.result` contains the current MCP response result. | | External processing (ExtProc) | Request-phase rules: same as Transformation (request). Response-phase rules: same as Transformation (response). | | LLM policy | `request`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `llmRequest`, `source`, `backend`, `extauthz`, `extproc`, `metadata` — `llmRequest` is the raw JSON body during LLM request handling (not `mcp`). [^5] | | Logging | `request`, `response`, `env`, `jwt`, `apiKey`, `basicAuth`, `llm`, `source`, `mcp`, `backend`, `extauthz`, `extproc`, `metadata` [^6] | diff --git a/content/docs/standalone/main/release-notes/release-notes.md b/content/docs/standalone/main/release-notes/release-notes.md index 627231a67..9694c2e64 100644 --- a/content/docs/standalone/main/release-notes/release-notes.md +++ b/content/docs/standalone/main/release-notes/release-notes.md @@ -12,12 +12,24 @@ Review the release notes for agentgateway standalone. ## 🌟 New features {#v17-new-features} +### MCP {#v17-mcp} + + + +- **CEL MCP guardrails**: MCP guardrail processors can now use `kind: expression` to reject matching MCP operations, or to transform the request `mcp.params` or response `mcp.result` body with CEL, without a remote ExtMCP server. For more information, see [Set up MCP guardrails]({{< link-hextra path="/documentation/mcp/guardrails/setup/#expression-processors" >}}). + ### Traffic management and operations {#v17-operations} - **Standalone Helm session key Secret**: Set `session.secretName` to load the AES-256-GCM session key from an existing Kubernetes Secret. The chart passes the Secret's `key` entry to the proxy as the `SESSION_KEY` environment variable, which takes precedence over `config.session.key`. For more information, see [Use an existing session key Secret]({{< link-hextra path="/documentation/setup/install/helm/#helm-session-secret" >}}). +### Observability {#v17-observability} + + + +- **Agent session detection**: CEL expressions can read the agent session of a request as `request.agent.session`. The session is detected from agent headers such as `x-claude-code-session-id`, or set by a CEL expression in `config.standardAttributes.session`. + ## 🐛 Fixes {#v17-fixes}