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
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. 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.
Agentgateway model routing sets up the routing that this example governs. Read that page first, because the steps here extend 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 it creates.
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, underspec.policies.promptGuard. The guard applies to that one model, and no other resource is involved. This example uses this form, because the routing thatbyo-agentgateway.mddocuments attaches models straight to aGatewaylistener. - On an
AgentgatewayPolicythat targets anHTTPRoute. AnAgentgatewayPolicycannot name anAgentgatewayModelin itstargetRefs, so this form requires the model to hang off an HTTPRoute rather than off the listener. For that variant, see Attach the guard to a route instead.
The following AgentgatewayModel carries the guard that the rest of this example applies. provider and parentRefs route the model, and policies adds the guard.
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:
promptGuard:
request:
- regex:
builtins:
- Email
- Ssn
- CreditCard
action: Reject
response:
message: The prompt contained personally identifiable information.A guard needs only regex. The remaining fields take defaults, and those defaults are permissive, so a guard that omits action masks the match rather than rejecting the request.
| Field | Description |
|---|---|
policies.promptGuard.request[] | The guards to apply to requests that the agent sends. A separate response list guards what the provider sends back. |
regex.builtins | Built-in patterns for common personally identifiable information (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.action | What to do with a match, either Reject or Mask. Omit to default to Mask, which is also the safer choice for an agent. A Reject guard ends the conversation permanently, for the reason described in Mask instead of reject. |
response.message | The 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
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-harnessthat admits the labelkagent.dev/harness: my-first-harness.Set up agentgateway model routing, which installs agentgateway with
--set agentgatewayModels.enabled=trueand creates aGatewaynamedagentgateway-proxyand anAgentgatewayModelnamedgpt-4o-mini.Create the ModelConfig that points at the gateway. This example uses a ModelConfig named
agentgateway-model-configin thekagentnamespace, and the deployment that it reaches enforces no API key authentication.
Add the prompt guard to the model
Adding policies to a model that already routes traffic changes nothing about the routing, so you reapply the same resource with the guard attached.
Reapply the
AgentgatewayModelwith 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: promptGuard: request: - regex: builtins: - Email - Ssn - CreditCard action: Reject response: message: The prompt contained personally identifiable information. EOFConfirm 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 checks are worth running 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.
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_ADDRESSSend 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.Send a prompt that carries no PII, to confirm that ordinary traffic still reaches the provider.
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 ...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_IDSend 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_FAILEDThe 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
Masking is the better default for an agent, for the reason the preceding section demonstrates: a Reject guard ends the conversation for good, while a Mask guard lets the turn through with the matched text replaced. Reserve Reject for 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.
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"}}]}}}}'Send the same prompt again, to the same AgentInstance that the
Rejectguard 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
requestlist, so it also drops theresponse.messagethat the rejection used; aMaskguard 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 a good deal 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: MaskNote
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.
Create an
HTTPRoutewhose rule sends traffic to every model, and point the model at the route rather than at theGateway.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 EOFCreate the
AgentgatewayPolicythat 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. EOFConfirm that the policy attached to the route.
kubectl get agentgatewaypolicy block-pii -n agentgateway-systemThe resource prints an
ACCEPTEDcolumn for whether agentgateway admitted the policy, and anATTACHEDcolumn for whether the policy reached its target. Both reportTruewhen 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
Delete the AgentInstance and the AgentTemplate.
kagent delete agent-instance $INSTANCE_ID kubectl delete agenttemplate support-triage -n kagentRemove the guard from the model, leaving the routing and the provider credentials in place. The path names
promptGuardrather thanpolicies, becausepoliciesalso holds theauththat lets the gateway reach the provider.kubectl patch agentgatewaymodel gpt-4o-mini -n agentgateway-system --type json -p '[{"op":"remove","path":"/spec/policies/promptGuard"}]'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