Skip to content
This documentation covers the kagent 1.0 alpha. For the latest 0.x release, see the 0.x docs.

For the complete documentation index, see llms.txt. Markdown versions of all docs pages are available by appending .md to any docs URL.

Block PII in an agent's model requests

Page as Markdown

Add a prompt guard to an agentgateway model so that a prompt carrying personally identifiable information never reaches the provider.

An agent sends every turn of a conversation to a model provider, and each of those requests carries whatever the person typed, personally identifiable information (PII) included. When agentgateway routes that traffic, the gateway sees each request before the provider does, so you can inspect and stop a prompt at the gateway. This example adds a prompt guard to the model that an agent calls, then watches the gateway reject a prompt that carries an email address.

This guide builds on the routing configured in Agentgateway model routing, including extending the AgentgatewayModel and the ModelConfigModelConfigA Kubernetes custom resource naming one model at one provider, along with the credentials to reach it. An AgentTemplate references one by name, and every agent compiled from that template calls the model that it names.Learn more that those steps create.

About prompt guards on a model

A prompt guard inspects the body of an OpenAI-compatible request as the request passes through the gateway, and either rejects the request or masks the matched text. Because the guard runs at the gateway, no change to an AgentTemplateAgentTemplateA Kubernetes custom resource defining what an agent does: its model, system prompt, tools, skills, and plugins. It runs only once a Harness accepts it.Learn more is needed, and every agent that shares the model inherits the guard.

A guard can live in two places, and the choice follows the routing that you already use:

  • On the AgentgatewayModel, under spec.policies.promptGuard. The guard applies to that one model, and no other resource is involved. This example uses this form, because agentgateway model routing attaches models straight to a Gateway listener.
  • On an AgentgatewayPolicy that targets an HTTPRoute. An AgentgatewayPolicy cannot name an AgentgatewayModel in its targetRefs, so this form requires the model to attach to an HTTPRoute rather than to the listener. For more information about this variant, see Attach the guard to a route instead.

The following AgentgatewayModel carries the guard that the rest of this example applies.

apiVersion: agentgateway.dev/v1alpha1
kind: AgentgatewayModel
metadata:
  name: gpt-4o-mini
  namespace: agentgateway-system
spec:
  parentRefs:
  - group: gateway.networking.k8s.io
    kind: Gateway
    name: agentgateway-proxy
    sectionName: http
  provider: OpenAI
  policies:
    auth:
      secretRef:
        name: openai-key
    promptGuard:
      request:
      - regex:
          builtins:
          - Email
          - Ssn
          - CreditCard
          action: Reject
        response:
          message: The prompt contained personally identifiable information.

Only policies.promptGuard is new in this example. The routing fields and policies.auth come from the prerequisite setup, and they appear here because a reapply must carry them.

FieldDescription
parentRefs and providerRoute the model to a Gateway listener, and name the provider that agentgateway calls.
policies.authThe Secret holding the credentials that agentgateway uses to reach the provider.
policies.promptGuard.request[]The guards to apply to requests that the agent sends. Each entry sets exactly one guard kind: regex matches patterns in the gateway itself, while webhook, openAIModeration, bedrockGuardrails, and googleModelArmor hand the content to an external service instead. A separate response list guards what the provider sends back.
regex.builtinsBuilt-in patterns for common kinds of PII. The five values are Email, Ssn, CreditCard, PhoneNumber, and CaSin. To add your own patterns, use regex.matches, which holds a list of regular expressions and is additive with builtins.
regex.actionWhat to do with a match, one of Mask, Reject, and Audit. Omit to default to Mask, which is also the safer choice for an agent. Audit records the action that the guard would have taken and lets the content through, so use it to trial a guard before you enforce it. A Reject guard ends the conversation permanently, for the reason described in Mask instead of reject.
response.messageThe message that the gateway returns to the caller on a rejection. Omit to default to The request was rejected due to inappropriate content. A sibling response.statusCode field sets the status code, and defaults to 403.

Important

A request guard inspects the system prompt as well as the messages. The default scope is SystemPrompt and Messages, so an AgentTemplate whose systemPrompt contains an example email address fails every turn rather than only the turns where a person types one. Check the system prompts of the agents that share a model before you turn on Reject.

Before you begin

  1. Install kagent, and create your first agent so that you have a HarnessHarnessA Kubernetes custom resource defining how an agent is allowed to run: its runtime, workload image, WorkerPool and snapshot storage, and which AgentTemplates it accepts.Learn more and know which label it admits. This example uses a Harness named my-first-harness that admits the label kagent.dev/harness: my-first-harness.

  2. Set up agentgateway model routing, which installs agentgateway with --set agentgatewayModels.enabled=true and creates a Gateway named agentgateway-proxy and an AgentgatewayModel named gpt-4o-mini.

  3. Create the ModelConfig that points at the gateway. This example uses a ModelConfig named agentgateway-model-config in the kagent namespace, and the deployment that it reaches enforces no API key authentication.

Add the prompt guard to the model

Adding promptGuard to a model that already routes traffic changes nothing about the routing, so you reapply the same resource with the guard attached. Keep policies.auth in the resource that you reapply, because an apply replaces policies wholesale. A model that loses its auth is still accepted and programmed, and every request through it returns the provider’s own 401.

  1. Reapply the AgentgatewayModel with a request guard that rejects three kinds of PII.

    kubectl apply -f - <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayModel
    metadata:
      name: gpt-4o-mini
      namespace: agentgateway-system
    spec:
      parentRefs:
      - group: gateway.networking.k8s.io
        kind: Gateway
        name: agentgateway-proxy
        sectionName: http
      provider: OpenAI
      policies:
        auth:
          secretRef:
            name: openai-key
        promptGuard:
          request:
          - regex:
              builtins:
              - Email
              - Ssn
              - CreditCard
              action: Reject
            response:
              message: The prompt contained personally identifiable information.
    EOF
  2. Confirm that agentgateway accepted the model. A guard that fails validation leaves the model unaccepted, and the gateway keeps serving the previous configuration.

    kubectl get agentgatewaymodel gpt-4o-mini -n agentgateway-system \
      -o jsonpath='{range .status.parents[0].conditions[?(@.type=="Accepted")]}{.status} {.reason} {.message}{end}'

    Example output:

    True Accepted Successfully accepted AgentgatewayModel
    

Watch the gateway reject a prompt

Two paths are worth checking in order. Calling the gateway directly isolates the guard from anything that kagent does, and sending the same content through an agent then shows what a person talking to the agent experiences.

  1. Reach the gateway. Choose the tab that matches your cluster.

    export AGENTGATEWAY_ADDRESS=$(kubectl get svc -n agentgateway-system agentgateway-proxy -o jsonpath="{.status.loadBalancer.ingress[0]['hostname','ip']}")
    echo $AGENTGATEWAY_ADDRESS
  2. Send a prompt that contains an email address.

    curl -i http://$AGENTGATEWAY_ADDRESS/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Summarize the ticket from alex@example.com"}]}'

    The gateway returns the status code and message from the guard, and the provider never receives the request. Example output:

    HTTP/1.1 403 Forbidden
    
    The prompt contained personally identifiable information.
    
  3. To confirm that ordinary traffic still reaches the provider, send a prompt that carries no PII.

    curl -i http://$AGENTGATEWAY_ADDRESS/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Summarize the most recent ticket"}]}'

    The response is an ordinary completion. Example output, truncated:

    HTTP/1.1 200 OK
    content-type: application/json
    
    {"model":"gpt-4o-mini-2024-07-18","usage":{"prompt_tokens":14,"completion_tokens":33,...},"choices":[{"message":{"content":"I'm sorry, but I don't have access to specific ...
    
  4. Create an agent that uses the guarded model, and an AgentInstanceAgentInstanceA running, conversational pairing of a Harness and an AgentTemplate. Unlike the two, it is not a Kubernetes resource: kagent's gRPC API creates it and its database tracks it.Learn more to talk to it.

    kubectl apply -f - <<EOF
    apiVersion: kagent.dev/v1alpha3
    kind: AgentTemplate
    metadata:
      name: support-triage
      namespace: kagent
      labels:
        kagent.dev/harness: my-first-harness
    spec:
      description: Summarizes support tickets.
      modelConfig:
        name: agentgateway-model-config
      systemPrompt: |
        You summarize support tickets in two sentences.
    EOF
    kagent create agent-instance --harness my-first-harness --agent-template support-triage
    export INSTANCE_ID=$(kagent get agent-instance -o json \
      | jq -r '[.agentInstances[] | select(.agentTemplate.name == "support-triage")] | sort_by(.createdAt) | last | .id')
    echo $INSTANCE_ID
  5. Send the agent a task that carries an email address.

    kagent invoke --agent-instance $INSTANCE_ID \
      --task "Summarize the ticket from alex@example.com"

    The turn fails rather than returning a summary. Example output:

    llm error response (code STREAM_ERROR): "POST \"http://agentgateway-proxy.agentgateway-system.svc.cluster.local/v1/chat/completions\": 403 Forbidden "
    Error: AgentInstance task 01a0b011-a684-7416-af8f-526242a5c07f ended in TASK_STATE_FAILED
    

    The error names the model call and the status code that the gateway returned. Ask the same question without the address, and the agent answers normally.

Note

The guard’s response.message does not reach the person talking to the agent. A caller who reads the gateway’s response directly sees the message, as the curl steps show, but kagent surfaces only the status code and the failed task. Treat response.message as something for an operator reading gateway logs rather than as an explanation for the end user, and put the explanation your users need in the agent’s system prompt instead.

Caution

A rejected turn strands the conversation permanently. An agent is not a single-shot chat client: the transcriptTranscriptThe record of an AgentInstance's conversation, held server-side and append-only. It survives the Actor suspending between turns, and a resumed runtime cannot shrink it. is append-only, and a failed turn does not remove the message that failed. The AgentInstance resends the whole conversation on every turn, so the prompt that tripped the guard is sent again, and rejected again, for the rest of that AgentInstance’s life.

The following sequence reproduces it. A fresh AgentInstance answers Summarize the most recent ticket normally. Send Summarize the ticket from alex@example.com next, and that turn fails with the 403. Then send What is 2+2?, which carries no PII of its own: that turn fails with a 403 as well, and so does every turn after it.

Creating a new AgentInstance is the only way to recover a conversation that a Reject guard has stopped. For agents, that cost is the strongest argument for masking instead.

Mask instead of reject

A Reject guard ends the conversation for good, as the previous section demonstrates. A Mask guard instead lets the turn through with the matched text replaced. Masking is therefore the better default for an agent, and Reject belongs to a policy that forbids PII outright and accepts a dead conversation as the price.

Masking also repairs a conversation that a Reject guard already stopped. Changing the action re-masks the offending message on the next turn rather than rejecting it, so the stranded AgentInstance answers again without being recreated.

  1. Change the action on the request guard to Mask.

    kubectl patch agentgatewaymodel gpt-4o-mini -n agentgateway-system --type merge -p \
      '{"spec":{"policies":{"promptGuard":{"request":[{"regex":{"builtins":["Email","Ssn","CreditCard"],"action":"Mask"}}]}}}}'
  2. Send the same prompt again, to the same AgentInstance that the Reject guard stranded.

    kagent invoke --agent-instance $INSTANCE_ID \
      --task "Summarize this ticket: alex@example.com reports that checkout returns 503 errors during peak hours."

    The turn succeeds this time. The agent answers, or asks a follow-up question, and the address never reaches the provider. The patch replaces the whole request list, so it also drops the response.message that the rejection used. A Mask guard returns no message, because it rejects nothing.

Warning

A guard on the response does not inspect streamed content unless you enable it. Prompt guards default to skipping streaming responses to preserve throughput. Set policies.promptGuard.streaming: Enabled to guard them, and note that Mask is never applied to a streamed response even then: a guard can reject streamed content, and matched text in a stream that is not rejected passes through unmodified.

Guard what an agent’s tools send

An agent differs from a chat client in that much of its traffic originates from tools rather than from a person. A tool that reads a ticket, a database row, or a Kubernetes resource can feed PII back to the model, and the default scope does not inspect that content.

Add scope to the guard to cover tool traffic. ToolOutput covers the results that a tool feeds back to the model, and ToolInput covers the arguments that the model produces for a tool call.

  policies:
    promptGuard:
      request:
      - scope:
        - SystemPrompt
        - Messages
        - ToolOutput
        regex:
          builtins:
          - Email
          - Ssn
          - CreditCard
          action: Mask

Note

Listing scope replaces the default rather than adding to it, so name SystemPrompt and Messages alongside the tool scopes to keep guarding the conversation. In an API that sends tool arguments as opaque JSON, a Mask action on ToolInput can rewrite those arguments into invalid JSON, so prefer ToolOutput unless you have a reason to inspect the arguments.

Attach the guard to a route instead

An AgentgatewayPolicy holds the same promptGuard configuration, and one policy can govern every model behind a route rather than one model at a time. Because targetRefs accepts an HTTPRoute and not an AgentgatewayModel, this form needs the model to attach to a route instead of to the listener.

  1. Create an HTTPRoute whose rule sends traffic to every model, and point the model at the route rather than at the Gateway.

    kubectl apply -f - <<EOF
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: model-traffic
      namespace: agentgateway-system
    spec:
      parentRefs:
      - name: agentgateway-proxy
        sectionName: http
      rules:
      - name: models
        backendRefs:
        - group: agentgateway.dev
          kind: AgentgatewayModel
          name: "*"
    ---
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayModel
    metadata:
      name: gpt-4o-mini
      namespace: agentgateway-system
    spec:
      parentRefs:
      - group: gateway.networking.k8s.io
        kind: HTTPRoute
        name: model-traffic
      provider: OpenAI
      policies:
        auth:
          secretRef:
            name: openai-key
    EOF

    The model keeps policies.auth and drops policies.promptGuard, because the AgentgatewayPolicy that you create next carries the guard. The parentRef needs no sectionName, because the route has exactly one rule.

  2. Create the AgentgatewayPolicy that targets the route rule.

    kubectl apply -f - <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayPolicy
    metadata:
      name: block-pii
      namespace: agentgateway-system
    spec:
      targetRefs:
      - group: gateway.networking.k8s.io
        kind: HTTPRoute
        name: model-traffic
        sectionName: models
      backend:
        ai:
          promptGuard:
            request:
            - regex:
                builtins:
                - Email
                - Ssn
                - CreditCard
                action: Reject
              response:
                message: The prompt contained personally identifiable information.
    EOF
  3. Confirm that the policy attached to the route.

    kubectl get agentgatewaypolicy block-pii -n agentgateway-system

    The resource prints an ACCEPTED column for whether agentgateway admitted the policy, and an ATTACHED column for whether the policy reached its target. Both report True when the policy governs the route.

Note

The listener must allow the HTTPRoute kind for this variant. The Gateway that agentgateway model routing creates already allows both HTTPRoute and AgentgatewayModel, so no change to the Gateway is needed.

Clean up

  1. Delete the AgentInstance and the AgentTemplate.

    kagent delete agent-instance $INSTANCE_ID
    kubectl delete agenttemplate support-triage -n kagent
  2. Remove the guard from the model, leaving the routing and the provider credentials in place. The patch names promptGuard rather than policies, because policies also holds the auth that lets the gateway reach the provider. A merge patch also succeeds when the model carries no guard, which is the case if you followed Attach the guard to a route instead.

    kubectl patch agentgatewaymodel gpt-4o-mini -n agentgateway-system --type merge -p '{"spec":{"policies":{"promptGuard":null}}}'
  3. If you followed Attach the guard to a route instead, delete the policy and the route, and repoint the model at the Gateway listener.

    kubectl delete agentgatewaypolicy block-pii -n agentgateway-system
    kubectl delete httproute model-traffic -n agentgateway-system
    kubectl apply -f - <<EOF
    apiVersion: agentgateway.dev/v1alpha1
    kind: AgentgatewayModel
    metadata:
      name: gpt-4o-mini
      namespace: agentgateway-system
    spec:
      parentRefs:
      - group: gateway.networking.k8s.io
        kind: Gateway
        name: agentgateway-proxy
        sectionName: http
      provider: OpenAI
      policies:
        auth:
          secretRef:
            name: openai-key
    EOF

Next steps